Search code examples
djangodjango-modelssorl-thumbnail

convert-engine issue with saving any thumbnail


So I have a model like this

from django.db import models
from sorl.thumbnail import get_thumbnail


class Upload(BaseModel):

    @staticmethod
    def upload_path_handler(instance, filename):
        return f'boxes/{instance.box.id}/uploads/{filename}'

    @staticmethod
    def thumbnail_path_handler(instance, filename):
        return f'boxes/{instance.box.id}/thumbnails/{filename}'

    def save(self, *args, **kwargs):
        if self._state.adding:
            # we cache the file size and store
            # it into the database to improve performance
            # we cannot edit the object's file so we don't
            # bother to modify the file size on updates
            self.size = self.file.size
            super(Upload, self).save(*args, **kwargs)
            thumbnail = get_thumbnail(self.file, '1280x720', crop='center')
            # sorl is not saving the thumbnails for non-image files
            return self.thumbnail.save(thumbnail.name, ContentFile(thumbnail.read()), True)
        super(Upload, self).save(*args, **kwargs)

    objects = api_managers.UploadManager()
    size = models.PositiveBigIntegerField()
    name = models.CharField(max_length=100, default='untitled', validators=[MinLengthValidator(2)])
    channel = models.ForeignKey('api_backend.Channel', on_delete=models.CASCADE, editable=False)
    box = models.ForeignKey('api_backend.Box', on_delete=models.CASCADE, editable=False)
    owner = models.ForeignKey('api_backend.User', on_delete=models.CASCADE, editable=False)
    thumbnail = models.ImageField(max_length=512, upload_to=thumbnail_path_handler.__func__, null=True, blank=True)
    file = models.FileField(max_length=512, upload_to=upload_path_handler.__func__)

    REQUIRED_FIELDS = [file, owner]

the file field can be literally any file, and I want sorl-thumbnail to make a thumbnail for the same and save it into the thumbnail field. I am on windows and am using ImageMagick. [python version- 32 bits]

this is the binary distribution I installed. https://imagemagick.org/script/download.php

ImageMagick-7.0.10-61-Q16-x86-dll.exe Win32 dynamic at 16 bits-per-pixel component

settings.py

THUMBNAIL_ENGINE = 'sorl.thumbnail.engines.convert_engine.Engine'

However, whenever an upload-model is saved, I get the following error.

FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\iyapp\\PycharmProjects\\rebox\\media\\cache\\db\\5a\\db5a88e1d6a08cdfa1afbc92e9b8cb47.jpg'

Full traceback:

Exception ignored in: <function TemporaryFile.__del__ at 0x04184610>
Traceback (most recent call last):
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\core\files\temp.py", line 61, in __del__
    self.close()
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\core\files\temp.py", line 49, in close
    if not self.close_called:
AttributeError: 'TemporaryFile' object has no attribute 'close_called'
__init__() got an unexpected keyword argument 'delete'
Traceback (most recent call last):
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\sorl\thumbnail\base.py", line 104, in get_thumbnail
    source_image = default.engine.get_image(source)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\sorl\thumbnail\engines\convert_engine.py", line 76, in get_image
    with NamedTemporaryFile(mode='wb', delete=False) as fp:
TypeError: __init__() got an unexpected keyword argument 'delete'
Remote file [boxes/2/uploads/a243bfbd00fdcb54982faf63cfc290b1dfcd47f1c0484facbd67c8b8ff606aff.jpg] at [1280x720] does not exist
exc:  [Errno 2] No such file or directory: 'C:\\Users\\iyapp\\PycharmProjects\\rebox\\media\\cache\\db\\5a\\db5a88e1d6a08cdfa1afbc92e9b8cb47.jpg'
Internal Server Error: /api/channels/1/uploads/
Traceback (most recent call last):
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\asgiref\sync.py", line 339, in thread_handler
    raise exc_info[1]
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\core\handlers\exception.py", line 38, in inner
    response = await get_response(request)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\core\handlers\base.py", line 233, in _get_response_async
    response = await wrapped_callback(request, *callback_args, **callback_kwargs)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\asgiref\sync.py", line 304, in __call__
    ret = await asyncio.wait_for(future, timeout=None)
  File "c:\python38\lib\asyncio\tasks.py", line 455, in wait_for
    return await fut
  File "c:\python38\lib\concurrent\futures\thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\asgiref\sync.py", line 343, in thread_handler
    return func(*args, **kwargs)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\views\decorators\csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\views\generic\base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\rest_framework\views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\rest_framework\views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\rest_framework\views.py", line 480, in raise_uncaught_exception
    raise exc
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\rest_framework\views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\rest_framework\generics.py", line 242, in post
    return self.create(request, *args, **kwargs)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\rest_framework\mixins.py", line 19, in create
    self.perform_create(serializer)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\rest_framework\mixins.py", line 24, in perform_create
    serializer.save()
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\rest_framework\serializers.py", line 205, in save
    self.instance = self.create(validated_data)
  File "C:\Users\iyapp\PycharmProjects\rebox\api_backend\serializers\partial.py", line 35, in create
    return super(PartialUploadSerializer, self).create(validated_data)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\rest_framework\serializers.py", line 939, in create
    instance = ModelClass._default_manager.create(**validated_data)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\db\models\manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\db\models\query.py", line 447, in create
    obj.save(force_insert=True, using=self.db)
  File "C:\Users\iyapp\PycharmProjects\rebox\api_backend\models\uploads.py", line 43, in save
    return self.thumbnail.save(thumbnail.name, ContentFile(thumbnail.read()), True)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\sorl\thumbnail\images.py", line 162, in read
    f = self.storage.open(self.name)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\core\files\storage.py", line 36, in open
    return self._open(name, mode)
  File "C:\Users\iyapp\Envs\rebox_django\lib\site-packages\django\core\files\storage.py", line 231, in _open
    return File(open(self.path(name), mode))
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\iyapp\\PycharmProjects\\rebox\\media\\cache\\db\\5a\\db5a88e1d6a08cdfa1afbc92e9b8cb47.jpg'

Can someone please help me fix this? thanks a lot!


Solution

  • as far as I know this is why the first exception arises.

    from django.core.files.temp import NamedTemporaryFile
    

    https://github.com/jazzband/sorl-thumbnail/blob/master/sorl/thumbnail/engines/convert_engine.py#L8 this import returns a TemporaryFile (see source code)

    """
    The temp module provides a NamedTemporaryFile that can be reopened in the same
    process on any platform. Most platforms use the standard Python
    tempfile.NamedTemporaryFile class, but Windows users are given a custom class.
    
    This is needed because the Python implementation of NamedTemporaryFile uses the
    O_TEMPORARY flag under Windows, which prevents the file from being reopened
    if the same flag is not provided [1][2]. Note that this does not address the
    more general issue of opening a file for writing and reading in multiple
    processes in a manner that works across platforms.
    
    The custom version of NamedTemporaryFile doesn't support the same keyword
    arguments available in tempfile.NamedTemporaryFile.
    
    1: https://mail.python.org/pipermail/python-list/2005-December/336957.html
    2: https://bugs.python.org/issue14243
    """
    
    import os
    import tempfile
    
    from django.core.files.utils import FileProxyMixin
    
    __all__ = ('NamedTemporaryFile', 'gettempdir',)
    
    
    if os.name == 'nt':
        class TemporaryFile(FileProxyMixin):
            """
            Temporary file object constructor that supports reopening of the
            temporary file in Windows.
    
            Unlike tempfile.NamedTemporaryFile from the standard library,
            __init__() doesn't support the 'delete', 'buffering', 'encoding', or
            'newline' keyword arguments.
            """
            def __init__(self, mode='w+b', bufsize=-1, suffix='', prefix='', dir=None):
                fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir)
                self.name = name
                self.file = os.fdopen(fd, mode, bufsize)
                self.close_called = False
    
            # Because close can be called during shutdown
            # we need to cache os.unlink and access it
            # as self.unlink only
            unlink = os.unlink
    
            def close(self):
                if not self.close_called:
                    self.close_called = True
                    try:
                        self.file.close()
                    except OSError:
                        pass
                    try:
                        self.unlink(self.name)
                    except OSError:
                        pass
    
            def __del__(self):
                self.close()
    
            def __enter__(self):
                self.file.__enter__()
                return self
    
            def __exit__(self, exc, value, tb):
                self.file.__exit__(exc, value, tb)
    
        NamedTemporaryFile = TemporaryFile
    else:
        NamedTemporaryFile = tempfile.NamedTemporaryFile
    
    gettempdir = tempfile.gettempdir
    

    and the TemporayFile class' init method doesn't take any parameter named delete. instead, only tempfile.NamedTemporaryFile does. Therefore, this chunk of code fails.

        def get_image(self, source):
            """
            Returns the backend image objects from a ImageFile instance
            """
            with NamedTemporaryFile(mode='wb', delete=False) as fp:
                fp.write(source.read())
            return {'source': fp.name, 'options': OrderedDict(), 'size': None}
    

    https://github.com/jazzband/sorl-thumbnail/blob/master/sorl/thumbnail/engines/convert_engine.py#L72

    I think that because of this the file isn't being saved at all. And at last, inside the save method of the model,

    We see the backend raise that the file doesn't exist.