Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I/O operation on closed file #391

Open
philippeluickx opened this issue Oct 25, 2016 · 65 comments
Open

I/O operation on closed file #391

philippeluickx opened this issue Oct 25, 2016 · 65 comments

Comments

@philippeluickx
Copy link

Getting this issue when creating a new image. I simply have a few specs on an ImageField like this:

    big_image = ImageSpecField(
        [SmartResize(width=900, height=600)],
        source='original_image',
        format='JPEG',
        options={'quality': 75}
    )

Exception:

ValueError` at /api/v1/pictures/pictures/

I/O operation on closed file.
Request Method: POST
Request URL:    https://foobar.com/api/v1/pictures/pictures/
Django Version: 1.10.2
Exception Type: ValueError
Exception Value:    
I/O operation on closed file.
Exception Location: /home/foobar/.virtualenvs/foobar/local/lib/python2.7/site-packages/imagekit/cachefiles/__init__.py in _generate, line 102

Later part of the stack trace, somehow the "File" gets set to none?:

/home/foobar/.virtualenvs/foobar/local/lib/python2.7/site-packages/imagekit/cachefiles/backends.py in generate_now
            file._generate()
Local Vars

Variable    Value
file    
<ImageCacheFile: CACHE/images/pictures/2016/10/25/foobar.jpg>
force   
False
self    
<imagekit.cachefiles.backends.Simple object at 0x7f9e2e91ce50>
/home/foobar/.virtualenvs/foobar/local/lib/python2.7/site-packages/imagekit/cachefiles/__init__.py in _generate
        content.seek(0)
Local Vars

Variable    Value
actual_name 
u'CACHE/images/pictures/2016/10/25/foobar.jpg'
content 
<File: None>
self    
<ImageCacheFile: CACHE/images/pictures/2016/10/25/foobar.jpg>

I have a setup with S3 and this started happening when I updated the boto library to boto3.
Reverting back seemed to help. I can provide more info if needed.

@philippeluickx
Copy link
Author

Linked to #369 or #350 or #365 ?

@vstoykov
Copy link
Collaborator

Finally some clue. It looks like smething changed in boto3 and the way how django imagekit is playing with files is not very usefull for boto3.

Currently I'm very busy with a big project and does not have enough time to investigate the problem with boto.

If you can investigate it and prepare a pull request I will be be grateful.

@philippeluickx
Copy link
Author

I will definitely spend some time to investigate, but this feels like it goes beyond my capabilities. Nonetheless, will keep this ticket updated if I find anything.

@philippeluickx
Copy link
Author

Some findings:

  • It sometimes breaks with 1 ImageSpecField and default settings
  • It sometimes breaks when having 2 ImageSpecFields and default settings
  • It sometimes breaks when having 1 ImageSpecField and IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'imagekit.cachefiles.strategies.Optimistic'

The following line closes the file:

actual_name = self.storage.save(self.name, content)

@philippeluickx
Copy link
Author

Narrowed it down to
https://github.com/boto/boto3/blob/develop/boto3/s3/inject.py#L372 > https://github.com/boto/s3transfer/blob/develop/s3transfer/manager.py#L242
where the file gets closed (I assume).

Could it be that boto3 closes the file because it uses a future?
:returns: Transfer future representing the upload

The quickfix would be to simply re-open the file again, but that might be bad design?

@vstoykov
Copy link
Collaborator

vstoykov commented Nov 9, 2016

@philippeluickx After this fix in django-s3-storage etianen/django-s3-storage@0286570 this issue must be gone. Check with django-s3-storrage==0.9.11

@philippeluickx
Copy link
Author

@vstoykov I am however using (the more popular / mature) https://github.com/jschneier/django-storages

@vstoykov
Copy link
Collaborator

vstoykov commented Nov 10, 2016

Now when I see in django-storages source code I see that the storage does not support reopening of the file. Also as stated in this issue jschneier/django-storages#64 there may be a problem with closed attribute to not return True when the file is closed.

Is it possible to test with django-s3-storage to see if your problem is still there or not? If it is gone then the issue in django-storages need to be opened for this.

@jkbrzt
Copy link

jkbrzt commented Nov 29, 2016

Here's my monkeypatch.py (imported by urls.py) that works around it the dirty way for now:

import logging


LOG = logging.getLogger(__name__)


def PATCH_IMAGEKIT_IMAGECACHEFILE_GENERATE():
    """
    Only needed until this issue gets fixed.
    <https://github.com/matthewwithanm/django-imagekit/issues/391>

    """
    from imagekit.cachefiles import ImageCacheFile
    from imagekit.utils import generate
    from django.core.files import File
    from imagekit.utils import get_logger

    def PATCHED_GENERATE(self):
        # Generate the file
        content = generate(self.generator)

        # PATCHED
        def PATCHED_CLOSE():
            """Protect file from being closed"""
            LOG.warning('patched close() called - ignoring', stack_info=False)

        ORIG_CLOSE = content.close
        content.close = PATCHED_CLOSE

        # Here content.close() gets called which is what we don't want
        actual_name = self.storage.save(self.name, content)

        # restore
        content.close = ORIG_CLOSE
        # /PATCHED

        # We're going to reuse the generated file, so we need to reset the pointer.

        content.seek(0)

        # Store the generated file. If we don't do this, the next time the
        # "file" attribute is accessed, it will result in a call to the storage
        # backend (in ``BaseIKFile._get_file``). Since we already have the
        # contents of the file, what would the point of that be?
        self.file = File(content)

        if actual_name != self.name:
            get_logger().warning(
                'The storage backend %s did not save the file with the'
                ' requested name ("%s") and instead used "%s". This may be'
                ' because a file already existed with the requested name. If'
                ' so, you may have meant to call generate() instead of'
                ' generate(force=True), or there may be a race condition in the'
                ' file backend %s. The saved file will not be used.' % (
                    self.storage,
                    self.name, actual_name,
                    self.cachefile_backend
                )
            )

    # Apply main patch
    LOG.warning('PATCHING ImageCacheFile._generate from imagekit')
    ImageCacheFile._generate = PATCHED_GENERATE


PATCH_IMAGEKIT_IMAGECACHEFILE_GENERATE()

Works with:

boto3==1.4.0
botocore==1.4.78
Django==1.10.2
django-imagekit==3.3
django-storages==1.5.1

@vstoykov
Copy link
Collaborator

vstoykov commented Nov 30, 2016

@jkbrzt can you test with latest version of django-s3-storage instead of django-storages to see if your problem still persist without monkey patching?
From what I see the problem is not in django-imagekit but in the underlying storage backend that do not support reopening (which by default is available in the default storage backend from Django).
With your "patch" you can leak open files and at the end to have this problem:

IOError: [Errno 24] Too many open files

More info here #335

@jkbrzt
Copy link

jkbrzt commented Dec 1, 2016

@vstoykov unfortunately, this specific project is pretty tied to django-storages so django-s3-storage is not an option at the moment.

As for the Too many open files issue, I assumed django-imagekit closes the files whenever it's finished with them so thanks for bringing it up.

@belek
Copy link

belek commented Jan 19, 2017

Same error happens on saving inlineformset with ImageSpecField.
I have CreateView with form and inlineformset. Formset has 3 ImageSpecField fields and this error occurs on saving the second one.

@vstoykov
Copy link
Collaborator

@shapoglyk are you also using django-storages and storing files on S3 or your case is different? If it is the same can you replace django-storages with django-s3-storage?

@belek
Copy link

belek commented Jan 19, 2017

@vstoykov , no I am using default django file storage.

@vstoykov
Copy link
Collaborator

That's interesting.
Can you install from git to confirm that the issue is still there or is fixed but unreleased?

@leonsmith
Copy link

leonsmith commented Jan 26, 2017

As philippeluickx pointed out, this seems to be boto3 related as the upload_fileobj closes the file you pass to it.

boto/boto3#929
boto/s3transfer#80

I managed to get around this by passing a clone of the file object to boto3 that it can close and cleanup but keeping the original around & unclosed.

class CustomS3Boto3Storage(S3Boto3Storage):
    """
    This is our custom version of S3Boto3Storage that fixes a bug in boto3 where the passed in file is closed upon upload.

    https://github.com/boto/boto3/issues/929
    https://github.com/matthewwithanm/django-imagekit/issues/391
    """
    
    def _save_content(self, obj, content, parameters):
        """
        We create a clone of the content file as when this is passed to boto3 it wrongly closes
        the file upon upload where as the storage backend expects it to still be open
        """
        # Seek our content back to the start
        content.seek(0, os.SEEK_SET)

        # Create a temporary file that will write to disk after a specified size
        content_autoclose = SpooledTemporaryFile()

        # Write our original content into our copy that will be closed by boto3
        content_autoclose.write(content.read())

        # Upload the object which will auto close the content_autoclose instance
        super(CustomS3Boto3Storage, self)._save_content(obj, content_autoclose, parameters)
        
        # Cleanup if this is fixed upstream our duplicate should always close        
        if not content_autoclose.closed:
            content_autoclose.close()

@charlesthk
Copy link

@leonsmith I confirm that your custom storage class works.

I'll be using it until a fix is made.

Thanks.

@vstoykov
Copy link
Collaborator

Hi guys. Can you check if the latest changes in develop (changes from #405) will fix the issue for you?

@charlesthk @shapoglyk @jkbrzt @philippeluickx

@cedriccarrard
Copy link

@vstoykov I have the same problem with:

django==1.10.5
django-storages==1.5.2
boto3==1.4.4
django-imagekit==4.0

@vstoykov
Copy link
Collaborator

@cedriccarrard thank you for reporting. Can you paste some traceback.

@cedriccarrard
Copy link

cedriccarrard commented Feb 28, 2017

@vstoykov
I have use imagekit.models.ProcessedImageField
I have this problem when I submit a form with my image upload on s3

"/usr/local/lib/python3.4/site-packages/django/template/base.py"
current = current[bit]
TypeError: 'ImageCacheFile' object is not subscriptable
File "/usr/local/lib/python3.4/site-packages/imagekit/cachefiles/strategies.py", line 15, in on_existence_required file.generate()
File "/usr/local/lib/python3.4/site-packages/imagekit/cachefiles/__init__.py", line 93, in generate
self.cachefile_backend.generate(self, force)
File "/usr/local/lib/python3.4/site-packages/imagekit/cachefiles/backends.py", line 109, in generate
self.generate_now(file, force=force)
File "/usr/local/lib/python3.4/site-packages/imagekit/cachefiles/backends.py", line 96, in generate_now 
file._generate()
File "/usr/local/lib/python3.4/site-packages/imagekit/cachefiles/__init__.py", line 102, in _generate
content.seek(0)
File "/usr/local/lib/python3.4/tempfile.py", line 417, in func_wrapper
return func(*args, **kwargs)
ValueError: seek of closed file

After the submit of my form I use

{% generateimage 'apptest:Mythumbmail' source=object.picture %}

The problem comes from this

@cedriccarrard
Copy link

cedriccarrard commented Feb 28, 2017

@vstoykov

I fixed my problem:

IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'app.example.imagegenerators.FixJustInTime'

class FixJustInTime:

    def on_content_required(self, file):
        try:
            file.generate()
        except:
            pass

    def on_existence_required(self, file):
        try:
            file.generate()
        except:
            pass

You have a better solution ?

@vstoykov
Copy link
Collaborator

vstoykov commented Mar 2, 2017

@cedriccarrard did you try the workaround from #391 (comment) ?

Your solution looks too hacky but if it works for you probably you can still use it.

@ofassley
Copy link

I'm experiencing the same issue. It occurs when I add an ImageSpecField to a model that already has a ProcessedImageField.

self = <ImageCacheFile: CACHE/avatars/a0cbd7767bf54e649f4952e8c3407418/9dd51a8dd80342ae7576d1c376b51e43.jpg>

    def _generate(self):
        # Generate the file
        content = generate(self.generator)

        actual_name = self.storage.save(self.name, content)

        # We're going to reuse the generated file, so we need to reset the pointer.
>       content.seek(0)
E       ValueError: I/O operation on closed file.

actual_name = 'CACHE/avatars/a0cbd7767bf54e649f4952e8c3407418/9dd51a8dd80342ae7576d1c376b51e43.jpg'
content    = <File: None>
self       = <ImageCacheFile: CACHE/avatars/a0cbd7767bf54e649f4952e8c3407418/9dd51a8dd80342ae7576d1c376b51e43.jpg>

../../../.virtualenvs/myapp/lib/python3.5/site-packages/imagekit/cachefiles/__init__.py:102: ValueError

The workaround posted in #391 (comment) works for me. Thanks!

@mgrdcm
Copy link

mgrdcm commented Mar 24, 2017

Just another note - I'm having the same issue.

@vstoykov
Copy link
Collaborator

@mgrdcm Is the workaround in #391 (comment) works for you?

@edtz
Copy link

edtz commented Mar 25, 2017

Encountered this problem on latest django/imagekit/storages, can confirm workaround #391 worked for me.

@KevinGrahamFoster
Copy link

#391 (comment) worked for me too.

philgyford added a commit to philgyford/django-hines that referenced this issue Jul 31, 2019
It was the good old "I/O operation on closed file" which I've had before
when uploading images to S3 and wanting to get imagekit to make
thumbnails of them.

The solution, as last time was to create a custom class based on
`S3Boto3Storage`:
matthewwithanm/django-imagekit#391 (comment)
@pasevin

This comment has been minimized.

@vstoykov

This comment has been minimized.

@mannpy
Copy link

mannpy commented Feb 29, 2020

Thank you @pasevin @vstoykov!
Here is the fixed version:

class CustomS3Boto3Storage(S3Boto3Storage, ABC):
    """
    This is our custom version of S3Boto3Storage that fixes a bug in
    boto3 where the passed in file is closed upon upload.
    From:
    https://github.com/matthewwithanm/django-imagekit/issues/391#issuecomment-275367006
    https://github.com/boto/boto3/issues/929
    https://github.com/matthewwithanm/django-imagekit/issues/391
    """

    def _save(self, name, content):
        """
        We create a clone of the content file as when this is passed to
        boto3 it wrongly closes the file upon upload where as the storage
        backend expects it to still be open
        """
        # Seek our content back to the start
        content.seek(0, os.SEEK_SET)

        # Create a temporary file that will write to disk after a specified
        # size. This file will be automatically deleted when closed by
        # boto3 or after exiting the `with` statement if the boto3 is fixed
        with SpooledTemporaryFile() as content_autoclose:

            # Write our original content into our copy that will be closed by boto3
            content_autoclose.write(content.read())

            # Upload the object which will auto close the
            # content_autoclose instance
            return super(CustomS3Boto3Storage, self)._save(name, content_autoclose)

@philgyford
Copy link

I assume the ABC class inherited by the CustomS3Boto3Storage class is from someone's existing code and shouldn't be in these examples?

@philgyford
Copy link

philgyford commented Mar 8, 2020

Note that although the two classes posted by @vstoykov and @mannpy look identical apart from the syntax highlighting, the one from @vstoykov is missing a return on the final line, which results in it not attaching the uploaded image to your model. This caught me out :) Thanks everyone for finding a solution!

@vstoykov
Copy link
Collaborator

vstoykov commented Mar 9, 2020

@philgyford thank you for your clarification. I just rewrote the @pasevin variant without testing it in real life.

@mannpy thank you for fixing the snippet to work correctly. I marked my comment as outdated to stay only your variant because people can copy mine by accident and will struggle with errors.

@kmahelona
Copy link

kmahelona commented May 11, 2020

Hey, just chiming in on this issue. While #391 (comment) works great, it doesn't work well for large files (e.g. > 1GB) unless your machine has a lot of RAM. In that case you'll want to specifically use this custom storage for ImageKit only via IMAGEKIT_DEFAULT_FILE_STORAGE

To avoid this issue completely, could you defer image generation? Is the root of this problem that the backend storage finishes up before ImageKit is done processing?

@Spaudel79
Copy link

@

Getting this issue when creating a new image. I simply have a few specs on an ImageField like this:

    big_image = ImageSpecField(
        [SmartResize(width=900, height=600)],
        source='original_image',
        format='JPEG',
        options={'quality': 75}
    )

Exception:

ValueError` at /api/v1/pictures/pictures/

I/O operation on closed file.
Request Method: POST
Request URL:    https://foobar.com/api/v1/pictures/pictures/
Django Version: 1.10.2
Exception Type: ValueError
Exception Value:    
I/O operation on closed file.
Exception Location: /home/foobar/.virtualenvs/foobar/local/lib/python2.7/site-packages/imagekit/cachefiles/__init__.py in _generate, line 102

Later part of the stack trace, somehow the "File" gets set to none?:

/home/foobar/.virtualenvs/foobar/local/lib/python2.7/site-packages/imagekit/cachefiles/backends.py in generate_now
            file._generate()
Local Vars

Variable    Value
file    
<ImageCacheFile: CACHE/images/pictures/2016/10/25/foobar.jpg>
force   
False
self    
<imagekit.cachefiles.backends.Simple object at 0x7f9e2e91ce50>
/home/foobar/.virtualenvs/foobar/local/lib/python2.7/site-packages/imagekit/cachefiles/__init__.py in _generate
        content.seek(0)
Local Vars

Variable    Value
actual_name 
u'CACHE/images/pictures/2016/10/25/foobar.jpg'
content 
<File: None>
self    
<ImageCacheFile: CACHE/images/pictures/2016/10/25/foobar.jpg>

I have a setup with S3 and this started happening when I updated the boto library to boto3.
Reverting back seemed to help. I can provide more info if needed.

Can you tell me how to revert back to boto?? Do I uninstall boto3 and install boto?? Is that all??

@Spaudel79
Copy link

Thank you @pasevin @vstoykov!
Here is the fixed version:

Can you please tell me where to write this code?? I mean in which file?? and where is this located??

@philgyford
Copy link

Can you please tell me where to write this code?? I mean in which file?? and where is this located??

You choose. e.g. Make a file at myapp/storages.py containing this (the class is copied from @manpy's comment, with the ABC class removed, and the imports added):

import os
from storages.backends.s3boto3 import S3Boto3Storage
from tempfile import SpooledTemporaryFile

class CustomS3Boto3Storage(S3Boto3Storage):
    """
    This is our custom version of S3Boto3Storage that fixes a bug in
    boto3 where the passed in file is closed upon upload.
    From:
    https://github.com/matthewwithanm/django-imagekit/issues/391#issuecomment-275367006
    https://github.com/boto/boto3/issues/929
    https://github.com/matthewwithanm/django-imagekit/issues/391
    """

    def _save(self, name, content):
        """
        We create a clone of the content file as when this is passed to
        boto3 it wrongly closes the file upon upload where as the storage
        backend expects it to still be open
        """
        # Seek our content back to the start
        content.seek(0, os.SEEK_SET)

        # Create a temporary file that will write to disk after a specified
        # size. This file will be automatically deleted when closed by
        # boto3 or after exiting the `with` statement if the boto3 is fixed
        with SpooledTemporaryFile() as content_autoclose:

            # Write our original content into our copy that will be closed by boto3
            content_autoclose.write(content.read())

            # Upload the object which will auto close the
            # content_autoclose instance
            return super(CustomS3Boto3Storage, self)._save(name, content_autoclose)

Then in your settings.py add something like:

DEFAULT_FILE_STORAGE = "myapp.storages.CustomS3Boto3Storage"

with the path reflecting where you've put the storages.py file in relation to your settings file.

@bartkappenburg
Copy link

@philgyford & @mannpy This works for me as well! Thanks.

We use Digital Ocean Spaces (S3 compatible) for our media. It seems that the first call to the thumbnail is giving us the error (ie. the uploading), but next calls are fine.

@linkcrt
Copy link

linkcrt commented Mar 1, 2021

Can you please tell me where to write this code?? I mean in which file?? and where is this located??

You choose. e.g. Make a file at myapp/storages.py containing this (the class is copied from @manpy's comment, with the ABC class removed, and the imports added):

import os
from storages.backends.s3boto3 import S3Boto3Storage
from tempfile import SpooledTemporaryFile

class CustomS3Boto3Storage(S3Boto3Storage):
    """
    This is our custom version of S3Boto3Storage that fixes a bug in
    boto3 where the passed in file is closed upon upload.
    From:
    https://github.com/matthewwithanm/django-imagekit/issues/391#issuecomment-275367006
    https://github.com/boto/boto3/issues/929
    https://github.com/matthewwithanm/django-imagekit/issues/391
    """

    def _save(self, name, content):
        """
        We create a clone of the content file as when this is passed to
        boto3 it wrongly closes the file upon upload where as the storage
        backend expects it to still be open
        """
        # Seek our content back to the start
        content.seek(0, os.SEEK_SET)

        # Create a temporary file that will write to disk after a specified
        # size. This file will be automatically deleted when closed by
        # boto3 or after exiting the `with` statement if the boto3 is fixed
        with SpooledTemporaryFile() as content_autoclose:

            # Write our original content into our copy that will be closed by boto3
            content_autoclose.write(content.read())

            # Upload the object which will auto close the
            # content_autoclose instance
            return super(CustomS3Boto3Storage, self)._save(name, content_autoclose)

Then in your settings.py add something like:

DEFAULT_FILE_STORAGE = "myapp.storages.CustomS3Boto3Storage"

with the path reflecting where you've put the storages.py file in relation to your settings file.

Thanks you very much for sharing this :). It works with:
django-imagekit==4.0.2
django-storages==1.11.1
boto3==1.17.12
botocore==1.20.12

@FinnGu
Copy link

FinnGu commented Jul 21, 2021

@philgyford & @mannpy This works for me as well! Thanks.

We use Digital Ocean Spaces (S3 compatible) for our media. It seems that the first call to the thumbnail is giving us the error (ie. the uploading), but next calls are fine.

Did you resolve this? I have the same setup and it just broke after updating some versions.

I used a custom storageclass like the one @philgyford suggested, which worked fine with versions:

boto3==1.10.2
django-imagekit==4.0.2
django-storages==1.7.2
pilkit==2.0
Pillow==5.4.1
sorl-thumbnail==12.5.0

Currently it does not work for versions:

boto3==1.17.72
django-imagekit==4.0.2
django-storages==1.11.1
pilkit==2.0
Pillow==8.2.0
sorl-thumbnail==12.7.0

@philgyford
Copy link

@FinnGu, I'm currently using the same code as above successfully with:

boto3==1.17.73
django-imagekit==4.0.2
django-storages==1.11.1
pilkit==2.0
Pillow==8.2.0

I don't have sorl-thumbnail installed. Sorry that this isn't a solution to your problem!

@FinnGu
Copy link

FinnGu commented Jul 22, 2021

@FinnGu, I'm currently using the same code as above successfully with:

boto3==1.17.73
django-imagekit==4.0.2
django-storages==1.11.1
pilkit==2.0
Pillow==8.2.0

I don't have sorl-thumbnail installed. Sorry that this isn't a solution to your problem!

Well, this is embarrasing... I still used the old version that was proposed here and not the newer version and of course it is working now.

For others that have overlooked this as well:
At some point django-storages removed the _save_content() method and the new solution uses _save() instead. Also it is now required to return the super() call in the last line, otherwise nothing gets saved at all.

@python-gare
Copy link

Finally found the solution here. thank you, guys.

@mick88
Copy link

mick88 commented Dec 23, 2021

I have the same issue with default FileStorage in development. I serve thumbnail image using django-downloadview==2.1.1 and if thumbnail file does not exist yet and is generated, the view crashes.

I'm using the following solution to re-open the file if it was closed after generation:

class ImagekitCacheStrategy(JustInTime):
    def on_existence_required(self, file: ImageCacheFile):
        super().on_existence_required(file)
        if file.closed:
            file.open()

@luiscastillocr
Copy link

I have the same issue with default FileStorage in development. I serve thumbnail image using django-downloadview==2.1.1 and if thumbnail file does not exist yet and is generated, the view crashes.

I'm using the following solution to re-open the file if it was closed after generation:

class ImagekitCacheStrategy(JustInTime):
    def on_existence_required(self, file: ImageCacheFile):
        super().on_existence_required(file)
        if file.closed:
            file.open()

hey @mick88 I am curious where did you put this, i am facing the same issue with django-imagekit and the error pops right in that method, can you point me where i can put this to see if this works for me?

@mick88
Copy link

mick88 commented Dec 28, 2022

@nasir733
Copy link

any progress?

@marcoacierno
Copy link

marcoacierno commented Mar 29, 2023

I still have this issue, the fix with the custom storage class that we have above doesn't work for me on the latest versions of django-stoarges, so looking around and changing it like this seems to be working (I haven't tested it 100% yet, so make sure you do your own tests as well)

from storages.backends.s3boto3 import S3Boto3Storage, SpooledTemporaryFile


class CustomS3Boto3Storage(S3Boto3Storage):
    def _save(self, name, content):
        content.seek(0)
        with SpooledTemporaryFile() as tmp:
            tmp.write(content.read())
            return super()._save(name, tmp)

@luiscastillocr
Copy link

class CustomS3Boto3Storage(S3Boto3Storage, ABC):
    """
    This is our custom version of S3Boto3Storage that fixes a bug in
    boto3 where the passed in file is closed upon upload.
    From:
    https://github.com/matthewwithanm/django-imagekit/issues/391#issuecomment-275367006
    https://github.com/boto/boto3/issues/929
    https://github.com/matthewwithanm/django-imagekit/issues/391
    """

    def _save(self, name, content):
        """
        We create a clone of the content file as when this is passed to
        boto3 it wrongly closes the file upon upload where as the storage
        backend expects it to still be open
        """
        # Seek our content back to the start
        content.seek(0, os.SEEK_SET)

        # Create a temporary file that will write to disk after a specified
        # size. This file will be automatically deleted when closed by
        # boto3 or after exiting the `with` statement if the boto3 is fixed
        with SpooledTemporaryFile() as content_autoclose:
            # Write our original content into our copy that will be closed by boto3
            content_autoclose.write(content.read())

            # Upload the object which will auto close the
            # content_autoclose instance
            return super(CustomS3Boto3Storage, self)._save(name, content_autoclose)

I have it like this working with Django==2.2 and django-imagekit==4.1.0 hope this can help someone else

@mick88 the trick you suggested did not work for me, ended up implementing the whole custom class as you can see, thanks anyway 👍

@vstoykov
Copy link
Collaborator

vstoykov commented Mar 30, 2023

From the underlying issue's comment I can propose updated workaround which will not create temporary file.

from io import BufferedReader
from storages.backends.s3boto3 import S3Boto3Storage

class NonCloseableBufferedReader(BufferedReader):
    """
    Taken from https://github.com/boto/s3transfer/issues/80#issuecomment-482534256
    """
    def close(self):
        self.flush()

class CustomS3Boto3Storage(S3Boto3Storage):
    """
    This is our custom version of S3Boto3Storage that fixes a bug in
    boto3 where the passed in file is closed upon upload.
    From:
    https://github.com/matthewwithanm/django-imagekit/issues/391#issuecomment-275367006
    https://github.com/boto/boto3/issues/929
    https://github.com/matthewwithanm/django-imagekit/issues/391
    """

    def _save(self, name, content):
        """
        We create a clone of the content file as when this is passed to
        boto3 it wrongly closes the file upon upload where as the storage
        backend expects it to still be open
        """
        # Seek our content back to the start
        content.seek(0, os.SEEK_SET)
        return super(CustomS3Boto3Storage, self)._save(name, NonCloseableBufferedReader(content))

I did not test it, but just put it here for reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests