From 2153806e89597b2c879d4be9c1d240d6c56d8e67 Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:42:56 +0100 Subject: [PATCH 01/13] feat: add filename length safety check with random suffix --- filer/utils/files.py | 34 +++++++++++++++++++-- tests/test_files.py | 71 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 tests/test_files.py diff --git a/filer/utils/files.py b/filer/utils/files.py index c9732843d..58f18ed6c 100644 --- a/filer/utils/files.py +++ b/filer/utils/files.py @@ -1,5 +1,6 @@ import mimetypes import os +import uuid from django.http.multipartparser import ChunkIter, SkipFile, StopFutureHandlers, StopUpload, exhaust from django.template.defaultfilters import slugify as slugify_django @@ -121,6 +122,32 @@ def slugify(string): return slugify_django(force_str(string)) +def _ensure_safe_length(filename, max_length=255, random_suffix_length=16): + """ + Ensures that the filename does not exceed the maximum allowed length. + If it does, the function truncates the filename and appends a random hexadecimal + suffix of length `random_suffix_length` to ensure uniqueness and compliance with + database constraints. + + Parameters: + filename (str): The filename to check. + max_length (int): The maximum allowed length for the filename. + random_suffix_length (int): The length of the random suffix to append. + + Returns: + str: The safe filename. + + + Reference issue: https://github.com/django-cms/django-filer/issues/1270 + """ + if len(filename) <= max_length: + return filename + + keep_length = max_length - random_suffix_length + random_suffix = uuid.uuid4().hex[:random_suffix_length] + return filename[:keep_length] + random_suffix + + def get_valid_filename(s): """ like the regular get_valid_filename, but also slugifies away @@ -131,6 +158,9 @@ def get_valid_filename(s): filename = slugify(filename) ext = slugify(ext) if ext: - return "{}.{}".format(filename, ext) + valid_filename = "{}.{}".format(filename, ext) else: - return "{}".format(filename) + valid_filename = "{}".format(filename) + + # Ensure the filename meets the maximum length requirements. + return _ensure_safe_length(valid_filename) diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 000000000..25bd80b39 --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,71 @@ +import string + +from django.test import TestCase + +from filer.utils.files import get_valid_filename + + +class GetValidFilenameTest(TestCase): + def test_short_filename_remains_unchanged(self): + """ + Test that a filename under the maximum length remains unchanged. + """ + original = "example.jpg" + result = get_valid_filename(original) + self.assertEqual(result, "example.jpg") + + def test_long_filename_is_truncated_and_suffix_appended(self): + """ + Test that a filename longer than the maximum allowed length is truncated and a random + hexadecimal suffix is appended. The final filename must not exceed 255 characters. + """ + # Create a filename that is much longer than 255 characters. + base = "a" * 300 # 300 characters + original = f"{base}.jpg" + result = get_valid_filename(original) + # Assert that the result is within the maximum allowed length. + self.assertTrue(len(result) <= 255, "Filename exceeds 255 characters.") + + # When truncated, the filename should end with a random hexadecimal suffix of length 16. + # We check that the suffix contains only hexadecimal digits. + random_suffix = result[-16:] + valid_hex_chars = set(string.hexdigits) + self.assertTrue(all(c in valid_hex_chars for c in random_suffix), + "The suffix is not a valid hexadecimal string.") + + def test_filename_with_extension_preserved(self): + """ + Test that the file extension is preserved (and slugified) after processing. + """ + original = "This is a test IMAGE.JPG" + result = get_valid_filename(original) + # Since slugification converts characters to lowercase, we expect ".jpg" + self.assertTrue(result.endswith(".jpg"), + "File extension was not preserved correctly.") + + def test_unicode_characters(self): + """ + Test that filenames with Unicode characters are handled correctly and result in a valid filename. + """ + original = "fiłęñâmé_üñîçødé.jpeg" + result = get_valid_filename(original) + # Verify that the result ends with the expected extension and contains only allowed characters. + self.assertTrue(result.endswith(".jpeg"), "File extension is not preserved for unicode filename.") + # Optionally, check that no unexpected characters remain (depends on your slugify behavior). + for char in result: + # Allow only alphanumeric characters, underscores, dashes, and the dot. + self.assertIn(char, string.ascii_lowercase + string.digits + "._-", + f"Unexpected character '{char}' found in filename.") + + def test_edge_case_exact_length(self): + """ + Test an edge case where the filename is exactly the maximum allowed length. + The function should leave such a filename unchanged. + """ + # Create a filename that is exactly 255 characters long. + base = "b" * 250 # 250 characters for base + original = f"{base}.png" # This may reach exactly or slightly above 255 depending on slugification + result = get_valid_filename(original) + # We check that the final result does not exceed 255 characters. + self.assertTrue(len(result) <= 255, + "Edge case filename exceeds the maximum allowed length.") From bc65f7c5a340506a93597966087678dc2ca513dc Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Fri, 28 Feb 2025 19:13:03 +0100 Subject: [PATCH 02/13] fix: correct base filename length for validation test --- tests/test_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_files.py b/tests/test_files.py index 25bd80b39..383ced79c 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -63,7 +63,7 @@ def test_edge_case_exact_length(self): The function should leave such a filename unchanged. """ # Create a filename that is exactly 255 characters long. - base = "b" * 250 # 250 characters for base + base = "b" * 251 # 250 characters for base original = f"{base}.png" # This may reach exactly or slightly above 255 depending on slugification result = get_valid_filename(original) # We check that the final result does not exceed 255 characters. From 14a27451e4a77eeb14869bb134d0e6ef14247474 Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Fri, 28 Feb 2025 19:14:37 +0100 Subject: [PATCH 03/13] fix: update base filename length in validation test --- tests/test_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_files.py b/tests/test_files.py index 383ced79c..7c27e4e3e 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -63,7 +63,7 @@ def test_edge_case_exact_length(self): The function should leave such a filename unchanged. """ # Create a filename that is exactly 255 characters long. - base = "b" * 251 # 250 characters for base + base = "b" * 251 # 251 characters for base original = f"{base}.png" # This may reach exactly or slightly above 255 depending on slugification result = get_valid_filename(original) # We check that the final result does not exceed 255 characters. From eb6b4a38ac98e53094bbf42ba079ccc65441d26b Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Fri, 28 Feb 2025 19:31:33 +0100 Subject: [PATCH 04/13] test: enhance filename length validation and edge case handling Integration of the suggestion from the community and sourcery-ai --- tests/test_files.py | 76 ++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/tests/test_files.py b/tests/test_files.py index 7c27e4e3e..051fdb8e9 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -1,7 +1,5 @@ import string - from django.test import TestCase - from filer.utils.files import get_valid_filename @@ -17,17 +15,13 @@ def test_short_filename_remains_unchanged(self): def test_long_filename_is_truncated_and_suffix_appended(self): """ Test that a filename longer than the maximum allowed length is truncated and a random - hexadecimal suffix is appended. The final filename must not exceed 255 characters. + hexadecimal suffix of length 16 is appended, resulting in exactly 255 characters. """ - # Create a filename that is much longer than 255 characters. - base = "a" * 300 # 300 characters + base = "a" * 300 # 300 characters base original = f"{base}.jpg" result = get_valid_filename(original) - # Assert that the result is within the maximum allowed length. - self.assertTrue(len(result) <= 255, "Filename exceeds 255 characters.") - - # When truncated, the filename should end with a random hexadecimal suffix of length 16. - # We check that the suffix contains only hexadecimal digits. + self.assertEqual(len(result), 255, "Filename length should be exactly 255 characters.") + # Verify that the last 16 characters form a valid hexadecimal string. random_suffix = result[-16:] valid_hex_chars = set(string.hexdigits) self.assertTrue(all(c in valid_hex_chars for c in random_suffix), @@ -39,33 +33,65 @@ def test_filename_with_extension_preserved(self): """ original = "This is a test IMAGE.JPG" result = get_valid_filename(original) - # Since slugification converts characters to lowercase, we expect ".jpg" self.assertTrue(result.endswith(".jpg"), "File extension was not preserved correctly.") def test_unicode_characters(self): """ - Test that filenames with Unicode characters are handled correctly and result in a valid filename. + Test that filenames with Unicode characters are handled correctly. """ original = "fiłęñâmé_üñîçødé.jpeg" result = get_valid_filename(original) - # Verify that the result ends with the expected extension and contains only allowed characters. - self.assertTrue(result.endswith(".jpeg"), "File extension is not preserved for unicode filename.") - # Optionally, check that no unexpected characters remain (depends on your slugify behavior). + self.assertTrue(result.endswith(".jpeg"), + "File extension is not preserved for unicode filename.") + # Verify that the resulting filename contains only allowed characters. + allowed_chars = set(string.ascii_lowercase + string.digits + "._-") for char in result: - # Allow only alphanumeric characters, underscores, dashes, and the dot. - self.assertIn(char, string.ascii_lowercase + string.digits + "._-", + self.assertIn(char, allowed_chars, f"Unexpected character '{char}' found in filename.") def test_edge_case_exact_length(self): """ - Test an edge case where the filename is exactly the maximum allowed length. - The function should leave such a filename unchanged. + Test that a filename exactly at the maximum allowed length remains unchanged. + """ + extension = ".png" + base_length = 255 - len(extension) + base = "b" * base_length + original = f"{base}{extension}" + result = get_valid_filename(original) + self.assertEqual(len(result), 255, + "Filename with length exactly 255 should remain unchanged.") + self.assertEqual(result, original, + "Filename with length exactly 255 should not be modified.") + + def test_edge_case_filenames(self): + """ + Test filenames at various boundary conditions to ensure correct behavior. """ - # Create a filename that is exactly 255 characters long. - base = "b" * 251 # 251 characters for base - original = f"{base}.png" # This may reach exactly or slightly above 255 depending on slugification + max_length = 255 + random_suffix_length = 16 + extension = ".jpg" + + # Test case 1: Filename with length exactly max_length - 1. + base_length = max_length - 1 - len(extension) + base = "c" * base_length + original = f"{base}{extension}" + result = get_valid_filename(original) + self.assertEqual(result, original, + "Filename with length max_length-1 should remain unchanged.") + + # Test case 2: Filename with length exactly equal to max_length - random_suffix_length. + base_length = max_length - random_suffix_length - len(extension) + base = "d" * base_length + original = f"{base}{extension}" + result = get_valid_filename(original) + self.assertEqual(result, original, + "Filename with length equal to max_length - random_suffix_length should remain unchanged.") + + # Test case 3: Filename with length exactly equal to max_length - random_suffix_length - 1. + base_length = max_length - random_suffix_length - 1 - len(extension) + base = "e" * base_length + original = f"{base}{extension}" result = get_valid_filename(original) - # We check that the final result does not exceed 255 characters. - self.assertTrue(len(result) <= 255, - "Edge case filename exceeds the maximum allowed length.") + self.assertEqual(result, original, + "Filename with length equal to max_length - random_suffix_length - 1 should remain unchanged.") From fe3f08bc5cafc4d458d864e4199a582eda328400 Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Sat, 1 Mar 2025 17:13:40 +0100 Subject: [PATCH 05/13] fix: update _ensure_safe_length to use settings for max length and random suffix --- filer/utils/files.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/filer/utils/files.py b/filer/utils/files.py index 58f18ed6c..42da2a813 100644 --- a/filer/utils/files.py +++ b/filer/utils/files.py @@ -2,6 +2,7 @@ import os import uuid +from django.conf import settings from django.http.multipartparser import ChunkIter, SkipFile, StopFutureHandlers, StopUpload, exhaust from django.template.defaultfilters import slugify as slugify_django from django.utils.encoding import force_str @@ -122,7 +123,7 @@ def slugify(string): return slugify_django(force_str(string)) -def _ensure_safe_length(filename, max_length=255, random_suffix_length=16): +def _ensure_safe_length(filename, max_length=None, random_suffix_length=None): """ Ensures that the filename does not exceed the maximum allowed length. If it does, the function truncates the filename and appends a random hexadecimal @@ -140,6 +141,10 @@ def _ensure_safe_length(filename, max_length=255, random_suffix_length=16): Reference issue: https://github.com/django-cms/django-filer/issues/1270 """ + + max_length = max_length or getattr(settings, "FILER_MAX_FILENAME_LENGTH", 255) + random_suffix_length = random_suffix_length or getattr(settings, "FILER_RANDOM_SUFFIX_LENGTH", 16) + if len(filename) <= max_length: return filename From bb08f291191a546f2ce07336d32b35f2685122c5 Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Sat, 1 Mar 2025 17:21:34 +0100 Subject: [PATCH 06/13] test: read max filename length and random suffix from settings --- tests/test_files.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/tests/test_files.py b/tests/test_files.py index 051fdb8e9..e6fda6647 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -1,9 +1,19 @@ import string + +from django.conf import settings from django.test import TestCase from filer.utils.files import get_valid_filename class GetValidFilenameTest(TestCase): + + def setUp(self): + """ + Set up the test case by reading the configuration settings for the maximum filename length. + """ + self.max_length = getattr(settings, "FILER_MAX_FILENAME_LENGTH", 255) + self.random_suffix_length = getattr(settings, "FILER_RANDOM_SUFFIX_LENGTH", 16) + def test_short_filename_remains_unchanged(self): """ Test that a filename under the maximum length remains unchanged. @@ -20,7 +30,11 @@ def test_long_filename_is_truncated_and_suffix_appended(self): base = "a" * 300 # 300 characters base original = f"{base}.jpg" result = get_valid_filename(original) - self.assertEqual(len(result), 255, "Filename length should be exactly 255 characters.") + self.assertEqual( + len(result), + self.max_length, + "Filename length should be exactly 255 characters." + ) # Verify that the last 16 characters form a valid hexadecimal string. random_suffix = result[-16:] valid_hex_chars = set(string.hexdigits) @@ -59,8 +73,11 @@ def test_edge_case_exact_length(self): base = "b" * base_length original = f"{base}{extension}" result = get_valid_filename(original) - self.assertEqual(len(result), 255, - "Filename with length exactly 255 should remain unchanged.") + self.assertEqual( + len(result), + self.max_length, + "Filename with length exactly 255 should remain unchanged." + ) self.assertEqual(result, original, "Filename with length exactly 255 should not be modified.") @@ -68,8 +85,8 @@ def test_edge_case_filenames(self): """ Test filenames at various boundary conditions to ensure correct behavior. """ - max_length = 255 - random_suffix_length = 16 + max_length = self.max_length + random_suffix_length = self.random_suffix_length extension = ".jpg" # Test case 1: Filename with length exactly max_length - 1. From be47ca92964d43478f9730d9cbc37755a4487132 Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:54:57 +0100 Subject: [PATCH 07/13] fix: set default values for max filename length and random suffix in _ensure_safe_length --- filer/utils/files.py | 5 +---- tests/test_files.py | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/filer/utils/files.py b/filer/utils/files.py index 42da2a813..0e9707c37 100644 --- a/filer/utils/files.py +++ b/filer/utils/files.py @@ -123,7 +123,7 @@ def slugify(string): return slugify_django(force_str(string)) -def _ensure_safe_length(filename, max_length=None, random_suffix_length=None): +def _ensure_safe_length(filename, max_length=155, random_suffix_length=16): """ Ensures that the filename does not exceed the maximum allowed length. If it does, the function truncates the filename and appends a random hexadecimal @@ -142,9 +142,6 @@ def _ensure_safe_length(filename, max_length=None, random_suffix_length=None): Reference issue: https://github.com/django-cms/django-filer/issues/1270 """ - max_length = max_length or getattr(settings, "FILER_MAX_FILENAME_LENGTH", 255) - random_suffix_length = random_suffix_length or getattr(settings, "FILER_RANDOM_SUFFIX_LENGTH", 16) - if len(filename) <= max_length: return filename diff --git a/tests/test_files.py b/tests/test_files.py index e6fda6647..999dea79e 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -11,8 +11,8 @@ def setUp(self): """ Set up the test case by reading the configuration settings for the maximum filename length. """ - self.max_length = getattr(settings, "FILER_MAX_FILENAME_LENGTH", 255) - self.random_suffix_length = getattr(settings, "FILER_RANDOM_SUFFIX_LENGTH", 16) + self.max_length = 155 + self.random_suffix_length = 16 def test_short_filename_remains_unchanged(self): """ @@ -69,7 +69,7 @@ def test_edge_case_exact_length(self): Test that a filename exactly at the maximum allowed length remains unchanged. """ extension = ".png" - base_length = 255 - len(extension) + base_length = 155 - len(extension) base = "b" * base_length original = f"{base}{extension}" result = get_valid_filename(original) From 709483019a4b3dcf02454b68c55de58349ee2826 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sun, 2 Mar 2025 18:00:06 +0100 Subject: [PATCH 08/13] Update filer/utils/files.py --- filer/utils/files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filer/utils/files.py b/filer/utils/files.py index 0e9707c37..278f73575 100644 --- a/filer/utils/files.py +++ b/filer/utils/files.py @@ -128,7 +128,7 @@ def _ensure_safe_length(filename, max_length=155, random_suffix_length=16): Ensures that the filename does not exceed the maximum allowed length. If it does, the function truncates the filename and appends a random hexadecimal suffix of length `random_suffix_length` to ensure uniqueness and compliance with - database constraints. + database constraints - even after markers for a thumbnail are added. Parameters: filename (str): The filename to check. From a4b7f9040a023fa366717d254f49da979d548d46 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sun, 2 Mar 2025 18:02:08 +0100 Subject: [PATCH 09/13] Apply suggestions from code review --- filer/utils/files.py | 1 - tests/test_files.py | 1 - 2 files changed, 2 deletions(-) diff --git a/filer/utils/files.py b/filer/utils/files.py index 278f73575..b9b3f1354 100644 --- a/filer/utils/files.py +++ b/filer/utils/files.py @@ -2,7 +2,6 @@ import os import uuid -from django.conf import settings from django.http.multipartparser import ChunkIter, SkipFile, StopFutureHandlers, StopUpload, exhaust from django.template.defaultfilters import slugify as slugify_django from django.utils.encoding import force_str diff --git a/tests/test_files.py b/tests/test_files.py index 999dea79e..d92c9c1de 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -1,6 +1,5 @@ import string -from django.conf import settings from django.test import TestCase from filer.utils.files import get_valid_filename From 12c1dced8d6a79df01a94c9eea3b694e21e72f37 Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Mon, 10 Mar 2025 18:00:12 +0100 Subject: [PATCH 10/13] refactor: improve command help text and streamline orphan file handling Issues: Management command filer_check does not find orphaned files in the private storage #1514 --- filer/management/commands/filer_check.py | 159 ++++++++++++----------- tests/test_filer_check.py | 97 +++++++++----- 2 files changed, 149 insertions(+), 107 deletions(-) diff --git a/filer/management/commands/filer_check.py b/filer/management/commands/filer_check.py index efe39027a..9826e9806 100644 --- a/filer/management/commands/filer_check.py +++ b/filer/management/commands/filer_check.py @@ -1,19 +1,17 @@ import os -from django.core.files.storage import DefaultStorage from django.core.management.base import BaseCommand from django.utils.module_loading import import_string -from PIL import UnidentifiedImageError - from filer import settings as filer_settings +from filer.models.filemodels import File from filer.utils.loader import load_model +from PIL import UnidentifiedImageError + class Command(BaseCommand): - help = "Look for orphaned files in media folders." - storage = DefaultStorage() - prefix = filer_settings.FILER_STORAGES['public']['main']['UPLOAD_TO_PREFIX'] + help = "Check for orphaned files, missing file references, and set image dimensions." def add_arguments(self, parser): parser.add_argument( @@ -21,35 +19,35 @@ def add_arguments(self, parser): action='store_true', dest='orphans', default=False, - help="Walk through the media folders and look for orphaned files.", + help="Scan media folders for orphaned files.", ) parser.add_argument( '--delete-orphans', action='store_true', dest='delete_orphans', default=False, - help="Delete orphaned files from their media folders.", + help="Delete orphaned files from storage.", ) parser.add_argument( '--missing', action='store_true', dest='missing', default=False, - help="Verify media folders and report about missing files.", + help="Check file references and report missing files.", ) parser.add_argument( '--delete-missing', action='store_true', dest='delete_missing', default=False, - help="Delete references in database if files are missing in media folder.", + help="Delete database entries if files are missing in the media folder.", ) parser.add_argument( '--image-dimensions', action='store_true', dest='image_dimensions', default=False, - help="Look for images without dimensions set, set them accordingly.", + help="Set image dimensions if they are not set.", ) parser.add_argument( '--noinput', @@ -57,7 +55,7 @@ def add_arguments(self, parser): action='store_false', dest='interactive', default=True, - help="Do NOT prompt the user for input of any kind." + help="Do not prompt the user for any interactive input.", ) def handle(self, *args, **options): @@ -65,99 +63,114 @@ def handle(self, *args, **options): self.verify_references(options) if options['delete_missing']: if options['interactive']: - msg = "\nThis will delete entries from your database. Are you sure you want to do this?\n\n" \ - "Type 'yes' to continue, or 'no' to cancel: " - if input(msg) != 'yes': - self.stdout.write("Aborted: Delete missing file entries from database.") + if input( + "\nThis will delete missing file references from the database.\n" + "Type 'yes' to continue, or 'no' to cancel: " + ) != 'yes': + self.stdout.write("Aborted: Missing file references were not deleted.") return self.verify_references(options) - if options['orphans']: - self.verify_storages(options) - if options['delete_orphans']: - if options['interactive']: - msg = "\nThis will delete orphaned files from your storage. Are you sure you want to do this?\n\n" \ - "Type 'yes' to continue, or 'no' to cancel: " - if input(msg) != 'yes': - self.stdout.write("Aborted: Delete orphaned files from storage.") + if options['orphans'] or options['delete_orphans']: + if options['delete_orphans'] and options['interactive']: + if input( + "\nThis will delete orphaned files from storage.\n" + "Type 'yes' to continue, or 'no' to cancel: " + ) != 'yes': + self.stdout.write("Aborted: Orphaned files were not deleted.") return self.verify_storages(options) + if options['image_dimensions']: self.image_dimensions(options) def verify_references(self, options): - from filer.models.filemodels import File - + """ + Checks that every file reference in the database exists in the storage. + If a file is missing, either report it or delete the reference based on the provided options. + """ for file in File.objects.all(): if not file.file.storage.exists(file.file.name): if options['delete_missing']: file.delete() - msg = "Delete missing file reference '{}/{}' from database." + verbose_msg = f"Deleted missing file reference '{file.folder}/{file}' from the database." else: - msg = "Referenced file '{}/{}' is missing in media folder." - if options['verbosity'] > 2: - self.stdout.write(msg.format(str(file.folder), str(file))) - elif options['verbosity']: + verbose_msg = f"File reference '{file.folder}/{file}' is missing in the storage." + # For higher verbosity, print the full verbose message. + if options.get('verbosity', 1) > 2: + self.stdout.write(verbose_msg) + # Otherwise, just output the relative file path. + elif options.get('verbosity'): self.stdout.write(os.path.join(str(file.folder), str(file))) def verify_storages(self, options): - from filer.models.filemodels import File + """ + Scans all storages defined in FILER_STORAGES (e.g. public and private) + for orphaned files, then reports or deletes them based on the options. + """ - def walk(prefix): + def walk(storage, prefix, label_prefix): child_dirs, files = storage.listdir(prefix) for filename in files: - relfilename = os.path.join(prefix, filename) - if not File.objects.filter(file=relfilename).exists(): + actual_path = os.path.join(prefix, filename) + relfilename = os.path.join(label_prefix, filename) + if not File.objects.filter(file=actual_path).exists(): if options['delete_orphans']: - storage.delete(relfilename) - msg = "Deleted orphaned file '{}'" + storage.delete(actual_path) + message = f"Deleted orphaned file '{relfilename}'" else: - msg = "Found orphaned file '{}'" - if options['verbosity'] > 2: - self.stdout.write(msg.format(relfilename)) - elif options['verbosity']: + message = f"Found orphaned file '{relfilename}'" + if options.get('verbosity', 1) > 2: + self.stdout.write(message) + elif options.get('verbosity'): self.stdout.write(relfilename) - for child in child_dirs: - walk(os.path.join(prefix, child)) - - filer_public = filer_settings.FILER_STORAGES['public']['main'] - storage = import_string(filer_public['ENGINE'])() - walk(filer_public['UPLOAD_TO_PREFIX']) + walk(storage, os.path.join(prefix, child), os.path.join(label_prefix, child)) + + # Loop through each storage configuration (e.g. public, private, etc.) + for storage_name, storage_config in filer_settings.FILER_STORAGES.items(): + storage_settings = storage_config.get('main') + if not storage_settings: + continue + storage = import_string(storage_settings['ENGINE'])() + if storage_settings.get('OPTIONS', {}).get('location'): + storage.location = storage_settings['OPTIONS']['location'] + # Use a label prefix: for public and private storages, use their names. + if storage_name in ['public', 'private']: + label_prefix = storage_name + else: + label_prefix = storage_settings['UPLOAD_TO_PREFIX'] + walk(storage, storage_settings['UPLOAD_TO_PREFIX'], label_prefix) def image_dimensions(self, options): + """ + For images without set dimensions (_width == 0 or None), try to read their dimensions + and save them, handling SVG files and possible image errors. + """ from django.db.models import Q - import easy_thumbnails from easy_thumbnails.VIL import Image as VILImage - from filer.utils.compatibility import PILImage - no_dimensions = load_model(filer_settings.FILER_IMAGE_MODEL).objects.filter( - Q(_width=0) | Q(_width__isnull=True) - ) - self.stdout.write(f"trying to set dimensions on {no_dimensions.count()} files") - for image in no_dimensions: - if image.file_ptr: - file_holder = image.file_ptr - else: - file_holder = image + ImageModel = load_model(filer_settings.FILER_IMAGE_MODEL) + images_without_dimensions = ImageModel.objects.filter(Q(_width=0) | Q(_width__isnull=True)) + self.stdout.write(f"Setting dimensions for {images_without_dimensions.count()} images") + for image in images_without_dimensions: + file_holder = image.file_ptr if hasattr(image, 'file_ptr') and image.file_ptr else image try: imgfile = file_holder.file imgfile.seek(0) - except (FileNotFoundError): - pass + except FileNotFoundError: + continue + if image.file.name.lower().endswith('.svg'): + # For SVG files, use VILImage (invalid SVGs do not throw errors) + with VILImage.load(imgfile) as vil_image: + image._width, image._height = vil_image.size else: - if image.file.name.lower().endswith('.svg'): - with VILImage.load(imgfile) as vil_image: - # invalid svg doesnt throw errors - image._width, image._height = vil_image.size - else: - try: - with PILImage.open(imgfile) as pil_image: - image._width, image._height = pil_image.size - image._transparent = easy_thumbnails.utils.is_transparent(pil_image) - except UnidentifiedImageError: - continue - image.save() - return + try: + with PILImage.open(imgfile) as pil_image: + image._width, image._height = pil_image.size + image._transparent = easy_thumbnails.utils.is_transparent(pil_image) + except UnidentifiedImageError: + continue + image.save() diff --git a/tests/test_filer_check.py b/tests/test_filer_check.py index 98592df1d..852e4421c 100644 --- a/tests/test_filer_check.py +++ b/tests/test_filer_check.py @@ -13,12 +13,10 @@ from filer.utils.loader import load_model from tests.helpers import create_image - Image = load_model(FILER_IMAGE_MODEL) class FilerCheckTestCase(TestCase): - svg_file_string = """ @@ -29,20 +27,23 @@ class FilerCheckTestCase(TestCase): """ def setUp(self): - # ensure that filer_public directory is empty from previous tests - storage = import_string(filer_settings.FILER_STORAGES['public']['main']['ENGINE'])() - upload_to_prefix = filer_settings.FILER_STORAGES['public']['main']['UPLOAD_TO_PREFIX'] - if storage.exists(upload_to_prefix): - shutil.rmtree(storage.path(upload_to_prefix)) + # Clean up the public folder to avoid interference between tests. + public_settings = filer_settings.FILER_STORAGES['public']['main'] + storage = import_string(public_settings['ENGINE'])() + upload_prefix = public_settings['UPLOAD_TO_PREFIX'] + if storage.exists(upload_prefix): + shutil.rmtree(storage.path(upload_prefix)) original_filename = 'testimage.jpg' file_obj = SimpleUploadedFile( name=original_filename, content=create_image().tobytes(), - content_type='image/jpeg') + content_type='image/jpeg' + ) self.filer_file = File.objects.create( file=file_obj, - original_filename=original_filename) + original_filename=original_filename + ) def tearDown(self): self.filer_file.delete() @@ -63,23 +64,50 @@ def test_delete_missing(self): with self.assertRaises(File.DoesNotExist): File.objects.get(id=file_pk) - def test_delete_orphans(self): + def test_delete_orphans_public(self): out = StringIO() self.assertTrue(os.path.exists(self.filer_file.file.path)) call_command('filer_check', stdout=out, orphans=True) - # folder must be clean, free of orphans + # The public folder should be free of orphaned files. self.assertEqual('', out.getvalue()) - # add an orphan file to our storage - storage = import_string(filer_settings.FILER_STORAGES['public']['main']['ENGINE'])() - filer_public = storage.path(filer_settings.FILER_STORAGES['public']['main']['UPLOAD_TO_PREFIX']) - orphan_file = os.path.join(filer_public, 'hello.txt') + # Add an orphan file to the public storage. + public_settings = filer_settings.FILER_STORAGES['public']['main'] + storage = import_string(public_settings['ENGINE'])() + public_path = storage.path(public_settings['UPLOAD_TO_PREFIX']) + orphan_file = os.path.join(public_path, 'hello.txt') + os.makedirs(public_path, exist_ok=True) + with open(orphan_file, 'w') as fh: + fh.write("I don't belong here!") + call_command('filer_check', stdout=out, orphans=True) + self.assertEqual("public/hello.txt\n", out.getvalue()) + self.assertTrue(os.path.exists(orphan_file)) + + call_command('filer_check', delete_orphans=True, interactive=False, verbosity=0) + self.assertFalse(os.path.exists(orphan_file)) + + def test_delete_orphans_private(self): + # Skip test if private storage is not configured. + if 'private' not in filer_settings.FILER_STORAGES: + self.skipTest("Private storage not configured in FILER_STORAGES.") + + out = StringIO() + private_settings = filer_settings.FILER_STORAGES['private']['main'] + storage = import_string(private_settings['ENGINE'])() + if private_settings.get('OPTIONS', {}).get('location'): + storage.location = private_settings['OPTIONS']['location'] + private_path = storage.path(private_settings['UPLOAD_TO_PREFIX']) + os.makedirs(private_path, exist_ok=True) + + orphan_file = os.path.join(private_path, 'private_orphan.txt') with open(orphan_file, 'w') as fh: fh.write("I don't belong here!") + # Verify that the command detects the orphan file. call_command('filer_check', stdout=out, orphans=True) - self.assertEqual("filer_public/hello.txt\n", out.getvalue()) + self.assertIn("private_orphan.txt", out.getvalue()) self.assertTrue(os.path.exists(orphan_file)) + # Delete the orphan file. call_command('filer_check', delete_orphans=True, interactive=False, verbosity=0) self.assertFalse(os.path.exists(orphan_file)) @@ -87,13 +115,13 @@ def test_image_dimensions_corrupted_file(self): original_filename = 'testimage.jpg' file_obj = SimpleUploadedFile( name=original_filename, - # corrupted! - content=create_image().tobytes(), - content_type='image/jpeg') + content=create_image().tobytes(), # corrupted file + content_type='image/jpeg' + ) self.filer_image = Image.objects.create( file=file_obj, - original_filename=original_filename) - + original_filename=original_filename + ) self.filer_image._width = 0 self.filer_image.save() call_command('filer_check', image_dimensions=True) @@ -101,12 +129,12 @@ def test_image_dimensions_corrupted_file(self): def test_image_dimensions_file_not_found(self): self.filer_image = Image.objects.create( file="123.jpg", - original_filename="123.jpg") + original_filename="123.jpg" + ) call_command('filer_check', image_dimensions=True) self.filer_image.refresh_from_db() def test_image_dimensions(self): - original_filename = 'testimage.jpg' with BytesIO() as jpg: create_image().save(jpg, format='JPEG') @@ -114,11 +142,12 @@ def test_image_dimensions(self): file_obj = SimpleUploadedFile( name=original_filename, content=jpg.read(), - content_type='image/jpeg') + content_type='image/jpeg' + ) self.filer_image = Image.objects.create( file=file_obj, - original_filename=original_filename) - + original_filename=original_filename + ) self.filer_image._width = 0 self.filer_image.save() @@ -127,33 +156,33 @@ def test_image_dimensions(self): self.assertGreater(self.filer_image._width, 0) def test_image_dimensions_invalid_svg(self): - original_filename = 'test.svg' svg_file = bytes("" + self.svg_file_string, "utf-8") file_obj = SimpleUploadedFile( name=original_filename, content=svg_file, - content_type='image/svg+xml') + content_type='image/svg+xml' + ) self.filer_image = Image.objects.create( file=file_obj, - original_filename=original_filename) - + original_filename=original_filename + ) self.filer_image._width = 0 self.filer_image.save() call_command('filer_check', image_dimensions=True) def test_image_dimensions_svg(self): - original_filename = 'test.svg' svg_file = bytes(self.svg_file_string, "utf-8") file_obj = SimpleUploadedFile( name=original_filename, content=svg_file, - content_type='image/svg+xml') + content_type='image/svg+xml' + ) self.filer_image = Image.objects.create( file=file_obj, - original_filename=original_filename) - + original_filename=original_filename + ) self.filer_image._width = 0 self.filer_image.save() From 074c27f038bc375ba0e2a4ba48e134ec9dbf2cc7 Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:49:23 +0100 Subject: [PATCH 11/13] refactor: enhance output messages and improve storage handling in filer_check --- filer/management/commands/filer_check.py | 45 ++++---- tests/test_filer_check.py | 136 +++++++++++++++++++---- 2 files changed, 140 insertions(+), 41 deletions(-) diff --git a/filer/management/commands/filer_check.py b/filer/management/commands/filer_check.py index 9826e9806..bdeeec50b 100644 --- a/filer/management/commands/filer_check.py +++ b/filer/management/commands/filer_check.py @@ -67,7 +67,8 @@ def handle(self, *args, **options): "\nThis will delete missing file references from the database.\n" "Type 'yes' to continue, or 'no' to cancel: " ) != 'yes': - self.stdout.write("Aborted: Missing file references were not deleted.") + self.stdout.write("Aborted: Missing file references were not deleted.\n") + self.stdout.flush() return self.verify_references(options) @@ -77,7 +78,8 @@ def handle(self, *args, **options): "\nThis will delete orphaned files from storage.\n" "Type 'yes' to continue, or 'no' to cancel: " ) != 'yes': - self.stdout.write("Aborted: Orphaned files were not deleted.") + self.stdout.write("Aborted: Orphaned files were not deleted.\n") + self.stdout.flush() return self.verify_storages(options) @@ -86,7 +88,7 @@ def handle(self, *args, **options): def verify_references(self, options): """ - Checks that every file reference in the database exists in the storage. + Checks that every file reference in the database exists in storage. If a file is missing, either report it or delete the reference based on the provided options. """ for file in File.objects.all(): @@ -95,21 +97,24 @@ def verify_references(self, options): file.delete() verbose_msg = f"Deleted missing file reference '{file.folder}/{file}' from the database." else: - verbose_msg = f"File reference '{file.folder}/{file}' is missing in the storage." - # For higher verbosity, print the full verbose message. + verbose_msg = f"File reference '{file.folder}/{file}' is missing in storage." if options.get('verbosity', 1) > 2: - self.stdout.write(verbose_msg) - # Otherwise, just output the relative file path. + self.stdout.write(verbose_msg + "\n") + self.stdout.flush() elif options.get('verbosity'): - self.stdout.write(os.path.join(str(file.folder), str(file))) + self.stdout.write(os.path.join(str(file.folder), str(file)) + "\n") + self.stdout.flush() def verify_storages(self, options): """ - Scans all storages defined in FILER_STORAGES (e.g. public and private) + Scans all storages defined in FILER_STORAGES (e.g., public and private) for orphaned files, then reports or deletes them based on the options. """ def walk(storage, prefix, label_prefix): + # If the directory does not exist, there is nothing to scan + if not storage.exists(prefix): + return child_dirs, files = storage.listdir(prefix) for filename in files: actual_path = os.path.join(prefix, filename) @@ -121,13 +126,15 @@ def walk(storage, prefix, label_prefix): else: message = f"Found orphaned file '{relfilename}'" if options.get('verbosity', 1) > 2: - self.stdout.write(message) + self.stdout.write(message + "\n") + self.stdout.flush() elif options.get('verbosity'): - self.stdout.write(relfilename) + self.stdout.write(relfilename + "\n") + self.stdout.flush() for child in child_dirs: walk(storage, os.path.join(prefix, child), os.path.join(label_prefix, child)) - # Loop through each storage configuration (e.g. public, private, etc.) + # Loop through each storage configuration (e.g., public, private, etc.) for storage_name, storage_config in filer_settings.FILER_STORAGES.items(): storage_settings = storage_config.get('main') if not storage_settings: @@ -135,12 +142,9 @@ def walk(storage, prefix, label_prefix): storage = import_string(storage_settings['ENGINE'])() if storage_settings.get('OPTIONS', {}).get('location'): storage.location = storage_settings['OPTIONS']['location'] - # Use a label prefix: for public and private storages, use their names. - if storage_name in ['public', 'private']: - label_prefix = storage_name - else: - label_prefix = storage_settings['UPLOAD_TO_PREFIX'] - walk(storage, storage_settings['UPLOAD_TO_PREFIX'], label_prefix) + # Set label_prefix: for public and private storages, use their names. + label_prefix = storage_name if storage_name in ['public', 'private'] else storage_settings.get('UPLOAD_TO_PREFIX', '') + walk(storage, storage_settings.get('UPLOAD_TO_PREFIX', ''), label_prefix) def image_dimensions(self, options): """ @@ -154,9 +158,10 @@ def image_dimensions(self, options): ImageModel = load_model(filer_settings.FILER_IMAGE_MODEL) images_without_dimensions = ImageModel.objects.filter(Q(_width=0) | Q(_width__isnull=True)) - self.stdout.write(f"Setting dimensions for {images_without_dimensions.count()} images") + self.stdout.write(f"Setting dimensions for {images_without_dimensions.count()} images" + "\n") + self.stdout.flush() for image in images_without_dimensions: - file_holder = image.file_ptr if hasattr(image, 'file_ptr') and image.file_ptr else image + file_holder = image.file_ptr if getattr(image, 'file_ptr', None) else image try: imgfile = file_holder.file imgfile.seek(0) diff --git a/tests/test_filer_check.py b/tests/test_filer_check.py index 852e4421c..42d38447d 100644 --- a/tests/test_filer_check.py +++ b/tests/test_filer_check.py @@ -1,11 +1,13 @@ import os import shutil +import tempfile from io import BytesIO, StringIO from django.core.files.uploadedfile import SimpleUploadedFile from django.core.management import call_command from django.test import TestCase from django.utils.module_loading import import_string +from django.core.files.base import ContentFile from filer import settings as filer_settings from filer.models.filemodels import File @@ -27,13 +29,18 @@ class FilerCheckTestCase(TestCase): """ def setUp(self): - # Clean up the public folder to avoid interference between tests. - public_settings = filer_settings.FILER_STORAGES['public']['main'] - storage = import_string(public_settings['ENGINE'])() - upload_prefix = public_settings['UPLOAD_TO_PREFIX'] - if storage.exists(upload_prefix): - shutil.rmtree(storage.path(upload_prefix)) + # Clear all configured storages to ensure a clean state for each test. + # This prevents interference from files left in any storage. + for storage_alias, storage_configs in filer_settings.FILER_STORAGES.items(): + config = storage_configs.get('main') + if not config: + continue + storage = import_string(config['ENGINE'])() + upload_prefix = config.get('UPLOAD_TO_PREFIX', '') + if storage.exists(upload_prefix): + shutil.rmtree(storage.path(upload_prefix)) + # Create a sample file for testing in the public storage. original_filename = 'testimage.jpg' file_obj = SimpleUploadedFile( name=original_filename, @@ -55,8 +62,10 @@ def test_delete_missing(self): call_command('filer_check', stdout=out, missing=True) self.assertEqual('', out.getvalue()) + # Remove the file to simulate a missing file. os.remove(self.filer_file.file.path) call_command('filer_check', stdout=out, missing=True) + # When verbosity is low, a simple relative file path is output. self.assertEqual("None/testimage.jpg\n", out.getvalue()) self.assertIsInstance(File.objects.get(id=file_pk), File) @@ -65,26 +74,36 @@ def test_delete_missing(self): File.objects.get(id=file_pk) def test_delete_orphans_public(self): + # First check - should be empty initially out = StringIO() - self.assertTrue(os.path.exists(self.filer_file.file.path)) - call_command('filer_check', stdout=out, orphans=True) - # The public folder should be free of orphaned files. + call_command('filer_check', stdout=out, orphans=True, verbosity=1) self.assertEqual('', out.getvalue()) - # Add an orphan file to the public storage. + # Add an orphan file using the storage API directly public_settings = filer_settings.FILER_STORAGES['public']['main'] storage = import_string(public_settings['ENGINE'])() - public_path = storage.path(public_settings['UPLOAD_TO_PREFIX']) - orphan_file = os.path.join(public_path, 'hello.txt') - os.makedirs(public_path, exist_ok=True) - with open(orphan_file, 'w') as fh: - fh.write("I don't belong here!") - call_command('filer_check', stdout=out, orphans=True) + + # Configure storage location if specified in settings + if public_settings.get('OPTIONS', {}).get('location'): + storage.location = public_settings['OPTIONS']['location'] + + # Get upload prefix and create file path + prefix = public_settings.get('UPLOAD_TO_PREFIX', '') + file_path = 'hello.txt' + rel_path = os.path.join(prefix, file_path) if prefix else file_path + + # Save file through storage API + storage.save(rel_path, ContentFile(b"I don't belong here!")) + self.assertTrue(storage.exists(rel_path)) + + # Check if orphan is detected + out = StringIO() + call_command('filer_check', stdout=out, orphans=True, verbosity=1) self.assertEqual("public/hello.txt\n", out.getvalue()) - self.assertTrue(os.path.exists(orphan_file)) + # Delete orphans call_command('filer_check', delete_orphans=True, interactive=False, verbosity=0) - self.assertFalse(os.path.exists(orphan_file)) + self.assertFalse(storage.exists(rel_path)) def test_delete_orphans_private(self): # Skip test if private storage is not configured. @@ -94,15 +113,16 @@ def test_delete_orphans_private(self): out = StringIO() private_settings = filer_settings.FILER_STORAGES['private']['main'] storage = import_string(private_settings['ENGINE'])() + # Set storage location if defined in OPTIONS. if private_settings.get('OPTIONS', {}).get('location'): storage.location = private_settings['OPTIONS']['location'] - private_path = storage.path(private_settings['UPLOAD_TO_PREFIX']) + private_path = storage.path(private_settings.get('UPLOAD_TO_PREFIX', '')) os.makedirs(private_path, exist_ok=True) orphan_file = os.path.join(private_path, 'private_orphan.txt') with open(orphan_file, 'w') as fh: fh.write("I don't belong here!") - # Verify that the command detects the orphan file. + # Run the command and check that it detects the private orphan file. call_command('filer_check', stdout=out, orphans=True) self.assertIn("private_orphan.txt", out.getvalue()) self.assertTrue(os.path.exists(orphan_file)) @@ -111,11 +131,84 @@ def test_delete_orphans_private(self): call_command('filer_check', delete_orphans=True, interactive=False, verbosity=0) self.assertFalse(os.path.exists(orphan_file)) + def test_delete_orphans_multiple_storages(self): + """ + Test that the filer_check command correctly handles orphaned files in multiple storages + without permanently modifying the settings. We use monkey-patching to assign temporary + directories to the storage configurations. + """ + out = StringIO() + + # --- Monkey-patch public storage location --- + public_config = filer_settings.FILER_STORAGES['public']['main'] + temp_public_dir = tempfile.mkdtemp() + if 'OPTIONS' in public_config: + public_config['OPTIONS']['location'] = temp_public_dir + else: + public_config['OPTIONS'] = {'location': temp_public_dir} + # Determine the upload prefix (if any) and ensure the corresponding directory exists. + public_upload_prefix = public_config.get('UPLOAD_TO_PREFIX', '') + if public_upload_prefix: + public_full_dir = os.path.join(temp_public_dir, public_upload_prefix) + else: + public_full_dir = temp_public_dir + os.makedirs(public_full_dir, exist_ok=True) + + # --- Monkey-patch private storage location --- + private_config = filer_settings.FILER_STORAGES.get('private', {}).get('main') + if private_config: + temp_private_dir = tempfile.mkdtemp() + if 'OPTIONS' in private_config: + private_config['OPTIONS']['location'] = temp_private_dir + else: + private_config['OPTIONS'] = {'location': temp_private_dir} + private_upload_prefix = private_config.get('UPLOAD_TO_PREFIX', '') + if private_upload_prefix: + private_full_dir = os.path.join(temp_private_dir, private_upload_prefix) + else: + private_full_dir = temp_private_dir + os.makedirs(private_full_dir, exist_ok=True) + else: + self.skipTest("Private storage not configured in FILER_STORAGES.") + + # --- Initialize storages using the patched locations --- + from django.core.files.storage import FileSystemStorage + storage_public = FileSystemStorage(location=temp_public_dir) + storage_private = FileSystemStorage(location=private_config['OPTIONS']['location']) + + # --- Save dummy orphan files in both storages --- + # For public storage, include the upload prefix in the filename if needed. + if public_upload_prefix: + filename_public = os.path.join(public_upload_prefix, 'orphan_public.txt') + else: + filename_public = 'orphan_public.txt' + if private_config.get('UPLOAD_TO_PREFIX', ''): + filename_private = os.path.join(private_config['UPLOAD_TO_PREFIX'], 'orphan_private.txt') + else: + filename_private = 'orphan_private.txt' + + storage_public.save(filename_public, ContentFile(b"dummy content")) + storage_private.save(filename_private, ContentFile(b"dummy content")) + + # --- Run the filer_check command --- + call_command('filer_check', stdout=out, orphans=True) + output = out.getvalue() + + # Verify that the output contains indicators for both storages. + self.assertIn('public', output) + self.assertIn('private', output) + + # --- Clean up --- + storage_public.delete(filename_public) + storage_private.delete(filename_private) + shutil.rmtree(temp_public_dir) + shutil.rmtree(private_config['OPTIONS']['location']) + def test_image_dimensions_corrupted_file(self): original_filename = 'testimage.jpg' file_obj = SimpleUploadedFile( name=original_filename, - content=create_image().tobytes(), # corrupted file + content=create_image().tobytes(), # Simulate a corrupted file. content_type='image/jpeg' ) self.filer_image = Image.objects.create( @@ -189,3 +282,4 @@ def test_image_dimensions_svg(self): call_command('filer_check', image_dimensions=True) self.filer_image.refresh_from_db() self.assertGreater(self.filer_image._width, 0) + From 135c19f86b29e7324f00fd2f77545745a5ddde49 Mon Sep 17 00:00:00 2001 From: Raffaele Grieco <62102593+Baraff24@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:55:14 +0100 Subject: [PATCH 12/13] fix: add missing newline at end of test_filer_check.py --- tests/test_filer_check.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_filer_check.py b/tests/test_filer_check.py index 42d38447d..6fe5aca31 100644 --- a/tests/test_filer_check.py +++ b/tests/test_filer_check.py @@ -282,4 +282,3 @@ def test_image_dimensions_svg(self): call_command('filer_check', image_dimensions=True) self.filer_image.refresh_from_db() self.assertGreater(self.filer_image._width, 0) - From 8db00cc880a144baae5249641d73900522f268bd Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 13 Mar 2025 12:41:15 +0100 Subject: [PATCH 13/13] Update test.yml --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98c452bd1..afd39da84 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] + python-version: [3.9, '3.10', '3.11', '3.12'] requirements-file: [ django-4.2.txt, django-5.0.txt, @@ -17,7 +17,7 @@ jobs: ] custom-image-model: [false, true] os: [ - ubuntu-20.04, + ubuntu-latest, ] exclude: - requirements-file: django-5.0.txt @@ -28,10 +28,10 @@ jobs: python-version: 3.8 - requirements-file: django-5.1.txt python-version: 3.9 - - requirements-file: django-main.txt - python-version: 3.8 - requirements-file: django-main.txt python-version: 3.9 + - requirements-file: django-main.txt + python-version: 3.10 steps: - uses: actions/checkout@v1