Skip to content

Commit 6e41747

Browse files
authored
fix: Prevent server errors in image detail view if images do not exist (#1389)
1 parent fdd4d64 commit 6e41747

File tree

2 files changed

+70
-18
lines changed

2 files changed

+70
-18
lines changed

filer/templatetags/filer_admin_tags.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.utils.safestring import mark_safe
1010
from django.utils.translation import gettext_lazy as _
1111

12+
from easy_thumbnails.exceptions import InvalidImageFormatError
1213
from easy_thumbnails.files import get_thumbnailer
1314
from easy_thumbnails.options import ThumbnailOptions
1415

@@ -94,21 +95,28 @@ def filer_has_permission(context, item, action):
9495

9596

9697
def file_icon_context(file, detail, width, height):
97-
# Only check on FileSystemStorage if file exists for performance reasons
98-
if isinstance(default_storage, FileSystemStorage) and not file.file.exists():
99-
return {
100-
'icon_url': staticfiles_storage.url('filer/icons/file-missing.svg'),
101-
'alt_text': _("File is missing"),
102-
'width': width,
103-
'height': height,
104-
}
10598
mime_maintype, mime_subtype = file.mime_maintype, file.mime_subtype
10699
context = {
107100
'mime_maintype': mime_maintype,
108101
'mime_type': file.mime_type,
109102
}
103+
# Get download_url and aspect ratio right for detail view
110104
if detail:
111105
context['download_url'] = file.url
106+
if file.width:
107+
width, height = 210, ceil(210 / file.width * file.height)
108+
context['sidebar_image_ratio'] = file.width / 210
109+
# returned context if icon is not available
110+
not_available_context = {
111+
'icon_url': staticfiles_storage.url('filer/icons/file-missing.svg'),
112+
'alt_text': _("File is missing"),
113+
'width': width,
114+
'height': width, # The icon is a square
115+
}
116+
# Check if file exists for performance reasons (only on FileSystemStorage)
117+
if isinstance(default_storage, FileSystemStorage) and file.file and not file.file.exists():
118+
return not_available_context
119+
112120
if isinstance(file, BaseImage):
113121
thumbnailer = get_thumbnailer(file)
114122

@@ -118,14 +126,12 @@ def file_icon_context(file, detail, width, height):
118126
icon_url = staticfiles_storage.url('filer/icons/file-unknown.svg')
119127
else:
120128
if detail:
121-
width, height = 210, ceil(210 / file.width * file.height)
122-
context['sidebar_image_ratio'] = file.width / 210
123129
opts = {'size': (width, height), 'upscale': True}
124130
else:
125131
opts = {'size': (width, height), 'crop': True}
126132
thumbnail_options = ThumbnailOptions(opts)
127133
# Optimize directory listing:
128-
if not detail and width == height and width in DEFERRED_THUMBNAIL_SIZES and hasattr(file, "thumbnail_name"):
134+
if width == height and width in DEFERRED_THUMBNAIL_SIZES and hasattr(file, "thumbnail_name"):
129135
# Get name of thumbnail from easy-thumbnail
130136
configured_name = thumbnailer.get_thumbnail_name(thumbnail_options, transparent=file._transparent)
131137
# If the name was annotated: Thumbnail exists and we can use it
@@ -137,17 +143,27 @@ def file_icon_context(file, detail, width, height):
137143
icon_url = reverse("admin:filer_file_fileicon", args=(file.pk, width))
138144
context['alt_text'] = file.default_alt_text
139145
else:
140-
icon_url = thumbnailer.get_thumbnail(thumbnail_options).url
141-
context['alt_text'] = file.default_alt_text
142-
if mime_subtype != 'svg+xml':
143-
thumbnail_options['size'] = 2 * width, 2 * height
144-
context['highres_url'] = thumbnailer.get_thumbnail(thumbnail_options).url
146+
# Try creating thumbnails / take existing ones
147+
try:
148+
icon_url = thumbnailer.get_thumbnail(thumbnail_options).url
149+
context['alt_text'] = file.default_alt_text
150+
if mime_subtype != 'svg+xml':
151+
thumbnail_options['size'] = 2 * width, 2 * height
152+
context['highres_url'] = thumbnailer.get_thumbnail(thumbnail_options).url
153+
except (InvalidImageFormatError, ):
154+
# This is caught by file.exists() for file storage systems
155+
# For remote storage systems we catch the error to avoid second trip
156+
# to the storage system
157+
return not_available_context
145158
elif mime_maintype in ['audio', 'font', 'video']:
146159
icon_url = staticfiles_storage.url(f'filer/icons/file-{mime_maintype}.svg')
160+
height = width # icon is a square
147161
elif mime_maintype == 'application' and mime_subtype in ['zip', 'pdf']:
148162
icon_url = staticfiles_storage.url(f'filer/icons/file-{mime_subtype}.svg')
163+
height = width # icon is a square
149164
else:
150165
icon_url = staticfiles_storage.url('filer/icons/file-unknown.svg')
166+
height = width # icon is a square
151167
context.update(width=width, height=height, icon_url=icon_url)
152168
return context
153169

tests/test_admin.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ def tearDown(self):
266266
os.remove(self.filename)
267267

268268
def test_icon_view_sizes(self):
269-
"""Tests if redirects are issued for accepted thumbnail sizes and 404 otherwise"""
269+
"""Redirects are issued for accepted thumbnail sizes and 404 otherwise"""
270270
test_set = tuple((size, 302) for size in DEFERRED_THUMBNAIL_SIZES)
271271
test_set += (50, 404), (90, 404), (320, 404)
272272
for size, expected_status in test_set:
@@ -283,6 +283,7 @@ def test_icon_view_sizes(self):
283283
self.assertNotIn("/static/", response["Location"])
284284

285285
def test_missing_file(self):
286+
"""Directory shows static icon for missing files"""
286287
image = Image.objects.create(
287288
owner=self.superuser,
288289
original_filename="some-image.jpg",
@@ -291,13 +292,48 @@ def test_missing_file(self):
291292
'file_id': image.pk,
292293
'size': 80,
293294
})
294-
# Make file unaccessible
295295

296296
response = self.client.get(url)
297297

298298
self.assertEqual(response.status_code, 302)
299299
self.assertIn("icons/file-missing.svg", response["Location"])
300300

301+
def test_icon_view_non_image(self):
302+
"""Getting an icon for a non-image results in a 404"""
303+
file = File.objects.create(
304+
owner=self.superuser,
305+
original_filename="some-file.xyz",
306+
)
307+
url = reverse('admin:filer_file_fileicon', kwargs={
308+
'file_id': file.pk,
309+
'size': 80,
310+
})
311+
312+
response = self.client.get(url)
313+
314+
self.assertEqual(response.status_code, 404)
315+
316+
def test_detail_view_missing_file(self):
317+
"""Detail view shows static icon for missing file"""
318+
image = Image.objects.create(
319+
owner=self.superuser,
320+
original_filename="some-image.jpg",
321+
)
322+
image._width = 50
323+
image._height = 200
324+
image.save()
325+
326+
url = reverse('admin:filer_image_change', kwargs={
327+
'object_id': image.pk,
328+
})
329+
330+
response = self.client.get(url)
331+
332+
self.assertContains(response, "icons/file-missing.svg")
333+
self.assertContains(response, 'width="210"')
334+
self.assertContains(response, 'height="210"')
335+
self.assertContains(response, 'alt="File is missing"')
336+
301337

302338
class FilerClipboardAdminUrlsTests(TestCase):
303339
def setUp(self):

0 commit comments

Comments
 (0)