Skip to content

Commit 45ef9ee

Browse files
authored
fix: issues with filer image crash when trying to generate thumbnail (#1384)
1 parent 4c2b9eb commit 45ef9ee

File tree

7 files changed

+83
-8
lines changed

7 files changed

+83
-8
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
CHANGELOG
33
=========
44

5+
unreleased
6+
==================
7+
8+
* Fix a bug that creates a server error when requesting a thumbnail from an
9+
invalid or missing file
10+
* Fix a bug that on some systems webp images were not recognized
11+
* Add missing css map files
12+
513
3.0.0 (2023-07-05)
614
==================
715

filer/admin/fileadmin.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from django import forms
22
from django.contrib.admin.utils import unquote
3+
from django.contrib.staticfiles.storage import staticfiles_storage
34
from django.http import Http404, HttpResponse, HttpResponseRedirect
45
from django.shortcuts import get_object_or_404
56
from django.urls import path, reverse
67
from django.utils.safestring import mark_safe
78
from django.utils.translation import gettext as _
89

10+
from easy_thumbnails.exceptions import InvalidImageFormatError
911
from easy_thumbnails.files import get_thumbnailer
1012
from easy_thumbnails.options import ThumbnailOptions
1113

@@ -174,10 +176,13 @@ def icon_view(self, request, file_id: int, size: int) -> HttpResponse:
174176
if not isinstance(file, BaseImage):
175177
raise Http404()
176178

177-
thumbnailer = get_thumbnailer(file)
178-
thumbnail_options = ThumbnailOptions({'size': (size, size), "crop": True})
179-
thumbnail = thumbnailer.get_thumbnail(thumbnail_options, generate=True)
180-
return HttpResponseRedirect(thumbnail.url)
179+
try:
180+
thumbnailer = get_thumbnailer(file)
181+
thumbnail_options = ThumbnailOptions({'size': (size, size), "crop": True})
182+
thumbnail = thumbnailer.get_thumbnail(thumbnail_options, generate=True)
183+
return HttpResponseRedirect(thumbnail.url)
184+
except InvalidImageFormatError:
185+
return HttpResponseRedirect(staticfiles_storage.url('filer/icons/file-missing.svg'))
181186

182187

183188
FileAdmin.fieldsets = FileAdmin.build_fieldsets()

filer/apps.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import mimetypes
2+
13
from django.apps import AppConfig
24
from django.core.exceptions import ImproperlyConfigured
35
from django.utils.translation import gettext_lazy as _
@@ -14,8 +16,14 @@ def register_optional_heif_supprt(self):
1416

1517
from .settings import IMAGE_EXTENSIONS, IMAGE_MIME_TYPES
1618

19+
# Register with easy_thumbnails
1720
register_heif_opener()
18-
IMAGE_EXTENSIONS += [".heic", ".heics", ".heif", ".heifs", ".hif"]
21+
HEIF_EXTENSIONS = [".heic", ".heics", ".heif", ".heifs", ".hif"]
22+
# Add extensions to python mimetypes which filer uses
23+
for ext in HEIF_EXTENSIONS:
24+
mimetypes.add_type("image/heic", ext)
25+
# Mark them as images
26+
IMAGE_EXTENSIONS += HEIF_EXTENSIONS
1927
IMAGE_MIME_TYPES.append("heic")
2028
except (ModuleNotFoundError, ImportError):
2129
# No heif support installed
@@ -52,5 +60,8 @@ def resolve_validators(self):
5260
self.FILE_VALIDATORS[mime_type] = functions
5361

5462
def ready(self):
63+
# Make webp mime type known to python (needed for python < 3.11)
64+
mimetypes.add_type("image/webp", ".webp")
65+
#
5566
self.resolve_validators()
5667
self.register_optional_heif_supprt()

tests/requirements/base.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# requirements from setup.py
22
Pillow
3+
pillow-heif
34
django-app-helper>=3.3.1
45

56
# other requirements

tests/test_admin.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from filer.models.filemodels import File
2222
from filer.models.foldermodels import Folder, FolderPermission
2323
from filer.models.virtualitems import FolderRoot
24-
from filer.settings import FILER_IMAGE_MODEL
24+
from filer.settings import DEFERRED_THUMBNAIL_SIZES, FILER_IMAGE_MODEL
2525
from filer.templatetags.filer_admin_tags import file_icon_url
2626
from filer.thumbnail_processors import normalize_subject_location
2727
from filer.utils.loader import load_model
@@ -254,9 +254,49 @@ class FilerImageAdminUrlsTests(TestCase):
254254
def setUp(self):
255255
self.superuser = create_superuser()
256256
self.client.login(username='admin', password='secret')
257+
self.img = create_image()
258+
self.image_name = 'test_file.jpg'
259+
self.filename = os.path.join(settings.FILE_UPLOAD_TEMP_DIR, self.image_name)
260+
self.img.save(self.filename, 'JPEG')
261+
with open(self.filename, 'rb') as upload:
262+
self.file_object = Image.objects.create(file=django.core.files.File(upload, name=self.image_name))
257263

258264
def tearDown(self):
259265
self.client.logout()
266+
os.remove(self.filename)
267+
268+
def test_icon_view_sizes(self):
269+
"""Tests if redirects are issued for accepted thumbnail sizes and 404 otherwise"""
270+
test_set = tuple((size, 302) for size in DEFERRED_THUMBNAIL_SIZES)
271+
test_set += (50, 404), (90, 404), (320, 404)
272+
for size, expected_status in test_set:
273+
url = reverse('admin:filer_file_fileicon', kwargs={
274+
'file_id': self.file_object.pk,
275+
'size': size,
276+
})
277+
response = self.client.get(url)
278+
self.assertEqual(response.status_code, expected_status)
279+
if response.status_code == 302: # redirect
280+
# Redirects to a media file
281+
self.assertIn("/media/", response["Location"])
282+
# Does not redirect to a static file
283+
self.assertNotIn("/static/", response["Location"])
284+
285+
def test_missing_file(self):
286+
image = Image.objects.create(
287+
owner=self.superuser,
288+
original_filename="some-image.jpg",
289+
)
290+
url = reverse('admin:filer_file_fileicon', kwargs={
291+
'file_id': image.pk,
292+
'size': 80,
293+
})
294+
# Make file unaccessible
295+
296+
response = self.client.get(url)
297+
298+
self.assertEqual(response.status_code, 302)
299+
self.assertIn("icons/file-missing.svg", response["Location"])
260300

261301

262302
class FilerClipboardAdminUrlsTests(TestCase):

tests/test_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import mimetypes
12
import os
23
from zipfile import ZipFile
34

45
from django.conf import settings
56
from django.core.files import File as DjangoFile
67
from django.test.testcases import TestCase
78

9+
from filer.settings import IMAGE_EXTENSIONS
810
from filer.utils.loader import load_object
911
from filer.utils.zip import unzip
1012
from tests.helpers import create_image
@@ -62,3 +64,11 @@ def tearDown(self):
6264
def test_unzipping_works(self):
6365
result = unzip(self.zipfilename)
6466
self.assertEqual(result[0][0].name, self.file.name)
67+
68+
69+
class MimeTypesTestCase(TestCase):
70+
def test_mime_types_known(self):
71+
"""Ensure that for all IMAGE_EXTENSIONS the mime types can be identified"""
72+
for ext in IMAGE_EXTENSIONS:
73+
self.assertIsNotNone(mimetypes.guess_type(f"file{ext}")[0],
74+
f"Mime type for extension {ext} unknown")

tests/test_validation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import os
22

3+
import django.core
34
from django.apps import apps
45
from django.conf import settings
5-
import django.core
66
from django.test import TestCase
77
from django.urls import reverse
88
from django.utils.crypto import get_random_string
99

1010
from filer.models import File, Folder
11-
from filer.validation import validate_upload, FileValidationError
11+
from filer.validation import FileValidationError, validate_upload
1212
from tests.helpers import create_superuser
1313

1414

0 commit comments

Comments
 (0)