Close files in create thumbnail (#252)

* Close the orig file in `create_thumbnail` method

* Skip python-magic tests on windows

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Johannes Wilm <mail@johanneswilm.org>
This commit is contained in:
satya-waylit 2026-01-07 15:03:47 -06:00 committed by GitHub
parent 7cb55334c1
commit 80a7c95583
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 37 additions and 29 deletions

View file

@ -1,6 +1,7 @@
Changelog Changelog
========= =========
* 9.0.0 (in progress) * 9.0.0 (in progress)
* Fix files not closed in `create_thumbnail`
* Add Django 5.2 and 6.0 support * Add Django 5.2 and 6.0 support
* Add Python 3.13, 3.14 support * Add Python 3.13, 3.14 support
* Drop Python 3.8, 3.9 support * Drop Python 3.8, 3.9 support

View file

@ -1,6 +1,7 @@
import binascii import binascii
import hashlib import hashlib
import os import os
from contextlib import closing
from io import BytesIO from io import BytesIO
from django.core.files import File from django.core.files import File
@ -142,38 +143,38 @@ class Avatar(models.Model):
orig = self.avatar.storage.open(self.avatar.name, "rb") orig = self.avatar.storage.open(self.avatar.name, "rb")
except IOError: except IOError:
return # What should we do here? Render a "sorry, didn't work" img? return # What should we do here? Render a "sorry, didn't work" img?
try:
image = Image.open(orig) with closing(orig):
image = self.transpose_image(image) try:
quality = quality or settings.AVATAR_THUMB_QUALITY image = Image.open(orig)
w, h = image.size except IOError:
if w != width or h != height:
ratioReal = 1.0 * w / h
ratioWant = 1.0 * width / height
if ratioReal > ratioWant:
diff = int((w - (h * ratioWant)) / 2)
image = image.crop((diff, 0, w - diff, h))
elif ratioReal < ratioWant:
diff = int((h - (w / ratioWant)) / 2)
image = image.crop((0, diff, w, h - diff))
if settings.AVATAR_THUMB_FORMAT == "JPEG" and image.mode == "RGBA":
image = image.convert("RGB")
elif image.mode not in (settings.AVATAR_THUMB_MODES):
image = image.convert(settings.AVATAR_THUMB_MODES[0])
image = image.resize((width, height), settings.AVATAR_RESIZE_METHOD)
thumb = BytesIO()
image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality)
thumb_file = ContentFile(thumb.getvalue())
else:
thumb_file = File(orig) thumb_file = File(orig)
else:
image = self.transpose_image(image)
quality = quality or settings.AVATAR_THUMB_QUALITY
w, h = image.size
if w != width or h != height:
ratioReal = 1.0 * w / h
ratioWant = 1.0 * width / height
if ratioReal > ratioWant:
diff = int((w - (h * ratioWant)) / 2)
image = image.crop((diff, 0, w - diff, h))
elif ratioReal < ratioWant:
diff = int((h - (w / ratioWant)) / 2)
image = image.crop((0, diff, w, h - diff))
if settings.AVATAR_THUMB_FORMAT == "JPEG" and image.mode == "RGBA":
image = image.convert("RGB")
elif image.mode not in (settings.AVATAR_THUMB_MODES):
image = image.convert(settings.AVATAR_THUMB_MODES[0])
image = image.resize((width, height), settings.AVATAR_RESIZE_METHOD)
thumb = BytesIO()
image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality)
thumb_file = ContentFile(thumb.getvalue())
else:
thumb_file = File(orig)
thumb_name = self.avatar_name(width, height) thumb_name = self.avatar_name(width, height)
thumb = self.avatar.storage.save(thumb_name, thumb_file) thumb = self.avatar.storage.save(thumb_name, thumb_file)
except IOError: invalidate_cache(self.user, width, height)
thumb_file = File(orig)
thumb = self.avatar.storage.save(
self.avatar_name(width, height), thumb_file
)
invalidate_cache(self.user, width, height)
def avatar_url(self, width, height=None): def avatar_url(self, width, height=None):
return self.avatar.storage.url(self.avatar_name(width, height)) return self.avatar.storage.url(self.avatar_name(width, height))

View file

@ -1,7 +1,9 @@
import math import math
import os.path import os.path
import sys
from pathlib import Path from pathlib import Path
from shutil import rmtree from shutil import rmtree
from unittest import skipIf
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.core import management from django.core import management
@ -118,6 +120,7 @@ class AvatarTests(TestCase):
self.assertTrue(avatar.primary) self.assertTrue(avatar.primary)
# We allow the .tiff file extension but not the mime type # We allow the .tiff file extension but not the mime type
@skipIf(sys.platform == "win32", "Skipping test on Windows platform")
@override_settings(AVATAR_ALLOWED_FILE_EXTS=(".png", ".gif", ".jpg", ".tiff")) @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".png", ".gif", ".jpg", ".tiff"))
@override_settings( @override_settings(
AVATAR_ALLOWED_MIMETYPES=("image/png", "image/gif", "image/jpeg") AVATAR_ALLOWED_MIMETYPES=("image/png", "image/gif", "image/jpeg")
@ -130,6 +133,7 @@ class AvatarTests(TestCase):
self.assertNotEqual(response.context["upload_avatar_form"].errors, {}) self.assertNotEqual(response.context["upload_avatar_form"].errors, {})
# We allow the .tiff file extension and the mime type # We allow the .tiff file extension and the mime type
@skipIf(sys.platform == "win32", "Skipping test on Windows platform")
@override_settings(AVATAR_ALLOWED_FILE_EXTS=(".png", ".gif", ".jpg", ".tiff")) @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".png", ".gif", ".jpg", ".tiff"))
@override_settings( @override_settings(
AVATAR_ALLOWED_MIMETYPES=("image/png", "image/gif", "image/jpeg", "image/tiff") AVATAR_ALLOWED_MIMETYPES=("image/png", "image/gif", "image/jpeg", "image/tiff")
@ -141,6 +145,7 @@ class AvatarTests(TestCase):
self.assertEqual(len(response.redirect_chain), 1) # Redirect only if it worked self.assertEqual(len(response.redirect_chain), 1) # Redirect only if it worked
self.assertEqual(response.context["upload_avatar_form"].errors, {}) self.assertEqual(response.context["upload_avatar_form"].errors, {})
@skipIf(sys.platform == "win32", "Skipping test on Windows platform")
@override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png")) @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png"))
def test_image_without_wrong_extension(self): def test_image_without_wrong_extension(self):
response = upload_helper(self, "imagefilewithoutext") response = upload_helper(self, "imagefilewithoutext")
@ -148,6 +153,7 @@ class AvatarTests(TestCase):
self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.assertNotEqual(response.context["upload_avatar_form"].errors, {}) self.assertNotEqual(response.context["upload_avatar_form"].errors, {})
@skipIf(sys.platform == "win32", "Skipping test on Windows platform")
@override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png")) @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png"))
def test_image_with_wrong_extension(self): def test_image_with_wrong_extension(self):
response = upload_helper(self, "imagefilewithwrongext.ogg") response = upload_helper(self, "imagefilewithwrongext.ogg")