From 6961cbc6398e9e36de42d21023855f4645007990 Mon Sep 17 00:00:00 2001
From: earthcomfy <hanagetbel93@gmail.com>
Date: Thu, 13 Mar 2025 21:09:06 +0300
Subject: [PATCH 1/4] fix: enhance thumbnail handling for public and private
 files

---
 filer/admin/folderadmin.py           | 26 +++++++++++-
 filer/utils/filer_easy_thumbnails.py | 13 +++++-
 tests/test_models.py                 |  7 ++--
 tests/test_thumbnails.py             | 62 ++++++++++++++++++++++++++++
 4 files changed, 102 insertions(+), 6 deletions(-)
 create mode 100644 tests/test_thumbnails.py

diff --git a/filer/admin/folderadmin.py b/filer/admin/folderadmin.py
index 8b3190f8e..4de19fea6 100644
--- a/filer/admin/folderadmin.py
+++ b/filer/admin/folderadmin.py
@@ -375,8 +375,30 @@ def directory_listing(self, request, folder_id=None, viewtype=None):
             .order_by("-modified")
         )
         file_qs = file_qs.annotate(
-            thumbnail_name=Subquery(thumbnail_qs.filter(name__contains=f"__{size}_").values_list("name")[:1]),
-            thumbnailx2_name=Subquery(thumbnail_qs.filter(name__contains=f"__{size_x2}_").values_list("name")[:1])
+            # For private files (Filer pattern)
+            thumbnail_name_filer=Subquery(
+                thumbnail_qs.filter(name__contains=f"__{size}_").values_list("name")[:1]
+            ),
+            thumbnailx2_name_filer=Subquery(
+                thumbnail_qs.filter(name__contains=f"__{size_x2}_").values_list("name")[
+                    :1
+                ]
+            ),
+            # For public files (easy_thumbnails pattern)
+            thumbnail_name_easy=Subquery(
+                thumbnail_qs.filter(name__contains=f".{size}_q85_crop").values_list(
+                    "name"
+                )[:1]
+            ),
+            thumbnailx2_name_easy=Subquery(
+                thumbnail_qs.filter(name__contains=f".{size_x2}_q85_crop").values_list(
+                    "name"
+                )[:1]
+            ),
+            thumbnail_name=Coalesce("thumbnail_name_filer", "thumbnail_name_easy"),
+            thumbnailx2_name=Coalesce(
+                "thumbnailx2_name_filer", "thumbnailx2_name_easy"
+            ),
         ).select_related("owner")
 
         try:
diff --git a/filer/utils/filer_easy_thumbnails.py b/filer/utils/filer_easy_thumbnails.py
index 00b2f4b43..9b678b755 100644
--- a/filer/utils/filer_easy_thumbnails.py
+++ b/filer/utils/filer_easy_thumbnails.py
@@ -27,8 +27,19 @@ def get_thumbnail_name(self, thumbnail_options, transparent=False):
         """
         A version of ``Thumbnailer.get_thumbnail_name`` that produces a
         reproducible thumbnail name that can be converted back to the original
-        filename.
+        filename. For public files, it uses easy_thumbnails default naming.
         """
+        is_public = False
+        if hasattr(self, "thumbnail_storage"):
+            is_public = "PrivateFileSystemStorage" not in str(
+                self.thumbnail_storage.__class__
+            )
+
+        if is_public:
+            return super(ThumbnailerNameMixin, self).get_thumbnail_name(
+                thumbnail_options, transparent
+            )
+        
         path, source_filename = os.path.split(self.name)
         source_extension = os.path.splitext(source_filename)[1][1:].lower()
         preserve_extensions = self.thumbnail_preserve_extensions
diff --git a/tests/test_models.py b/tests/test_models.py
index 3d52e2502..6ee7ac204 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -35,13 +35,14 @@ def tearDown(self):
         for f in File.objects.all():
             f.delete()
 
-    def create_filer_image(self, owner=None):
+    def create_filer_image(self, owner=None, is_public=True):
         if owner is None:
             owner = self.superuser
         file_obj = DjangoFile(open(self.filename, 'rb'), name=self.image_name)
         image = Image.objects.create(owner=owner,
                                      original_filename=self.image_name,
-                                     file=file_obj)
+                                     file=file_obj,
+                                     is_public=is_public)
         return image
 
     def test_create_folder_structure(self):
@@ -80,7 +81,7 @@ def test_create_clipboard_item(self):
         self.assertEqual(Clipboard.objects.count(), 1)
 
     def test_create_icons(self):
-        image = self.create_filer_image()
+        image = self.create_filer_image(is_public=False)
         image.save()
         icons = image.icons
         file_basename = os.path.basename(image.file.path)
diff --git a/tests/test_thumbnails.py b/tests/test_thumbnails.py
new file mode 100644
index 000000000..4fbc1ce2a
--- /dev/null
+++ b/tests/test_thumbnails.py
@@ -0,0 +1,62 @@
+import os
+
+from django.conf import settings
+from django.core.files import File as DjangoFile
+from django.test import TestCase, override_settings
+
+from filer.models.filemodels import File
+from filer.settings import FILER_IMAGE_MODEL
+from filer.utils.loader import load_model
+from tests.helpers import create_image, create_superuser
+
+Image = load_model(FILER_IMAGE_MODEL)
+
+
+def custom_namer(thumbnailer, **kwargs):
+    path, filename = os.path.split(thumbnailer.name)
+    return os.path.join(path, f"custom_prefix_{filename}")
+
+
+class ThumbnailNameTests(TestCase):
+    def setUp(self):
+        self.superuser = create_superuser()
+        self.img = create_image()
+        self.image_name = "test_file.jpg"
+        self.filename = os.path.join(settings.FILE_UPLOAD_TEMP_DIR, self.image_name)
+        self.img.save(self.filename, "JPEG")
+
+    def tearDown(self):
+        os.remove(self.filename)
+        for f in File.objects.all():
+            f.delete()
+
+    def create_filer_image(self, is_public=True):
+        with open(self.filename, "rb") as f:
+            file_obj = DjangoFile(f)
+            image = Image.objects.create(
+                owner=self.superuser,
+                original_filename=self.image_name,
+                file=file_obj,
+                is_public=is_public,
+            )
+        return image
+
+    def test_thumbnailer_class_for_public_files(self):
+        image = self.create_filer_image(is_public=True)
+        thumbnailer = image.easy_thumbnails_thumbnailer
+        name = thumbnailer.get_thumbnail_name({"size": (100, 100)})
+        self.assertNotIn("__", name)
+
+    def test_thumbnailer_class_for_private_files(self):
+        image = self.create_filer_image(is_public=False)
+        thumbnailer = image.easy_thumbnails_thumbnailer
+        name = thumbnailer.get_thumbnail_name({"size": (100, 100)})
+        self.assertIn("__", name)
+
+    @override_settings(THUMBNAIL_NAMER="tests.test_thumbnails.custom_namer")
+    def test_thumbnail_custom_namer(self):
+        image = self.create_filer_image(is_public=True)
+        thumbnailer = image.easy_thumbnails_thumbnailer
+        name = thumbnailer.get_thumbnail_name({"size": (100, 100)})
+        filename = os.path.basename(name)
+        self.assertTrue(filename.startswith("custom_prefix_"))

From e939b4660e77f5a7aceebdc291b65349f34689f9 Mon Sep 17 00:00:00 2001
From: earthcomfy <hanagetbel93@gmail.com>
Date: Wed, 19 Mar 2025 22:33:42 +0300
Subject: [PATCH 2/4] fix: similar implementation for private and public files

---
 filer/admin/folderadmin.py           | 26 +--------
 filer/utils/filer_easy_thumbnails.py | 82 +++++++++-------------------
 tests/test_models.py                 |  9 ++-
 tests/test_thumbnails.py             | 13 ++++-
 4 files changed, 44 insertions(+), 86 deletions(-)

diff --git a/filer/admin/folderadmin.py b/filer/admin/folderadmin.py
index 38a738a8c..75e33975f 100644
--- a/filer/admin/folderadmin.py
+++ b/filer/admin/folderadmin.py
@@ -376,30 +376,8 @@ def directory_listing(self, request, folder_id=None, viewtype=None):
             .order_by("-modified")
         )
         file_qs = file_qs.annotate(
-            # For private files (Filer pattern)
-            thumbnail_name_filer=Subquery(
-                thumbnail_qs.filter(name__contains=f"__{size}_").values_list("name")[:1]
-            ),
-            thumbnailx2_name_filer=Subquery(
-                thumbnail_qs.filter(name__contains=f"__{size_x2}_").values_list("name")[
-                    :1
-                ]
-            ),
-            # For public files (easy_thumbnails pattern)
-            thumbnail_name_easy=Subquery(
-                thumbnail_qs.filter(name__contains=f".{size}_q85_crop").values_list(
-                    "name"
-                )[:1]
-            ),
-            thumbnailx2_name_easy=Subquery(
-                thumbnail_qs.filter(name__contains=f".{size_x2}_q85_crop").values_list(
-                    "name"
-                )[:1]
-            ),
-            thumbnail_name=Coalesce("thumbnail_name_filer", "thumbnail_name_easy"),
-            thumbnailx2_name=Coalesce(
-                "thumbnailx2_name_filer", "thumbnailx2_name_easy"
-            ),
+            thumbnail_name=Subquery(thumbnail_qs.filter(name__contains=f".{size}_").values_list("name")[:1]),
+            thumbnailx2_name=Subquery(thumbnail_qs.filter(name__contains=f".{size_x2}_").values_list("name")[:1])
         ).select_related("owner")
 
         try:
diff --git a/filer/utils/filer_easy_thumbnails.py b/filer/utils/filer_easy_thumbnails.py
index 9b678b755..c4d4a3aa6 100644
--- a/filer/utils/filer_easy_thumbnails.py
+++ b/filer/utils/filer_easy_thumbnails.py
@@ -3,12 +3,11 @@
 
 from easy_thumbnails.files import Thumbnailer
 
-
-# match the source filename using `__` as the seperator. ``opts_and_ext`` is non
-# greedy so it should match the last occurence of `__`.
-# in ``ThumbnailerNameMixin.get_thumbnail_name`` we ensure that there is no `__`
-# in the opts part.
-RE_ORIGINAL_FILENAME = re.compile(r"^(?P<source_filename>.*)__(?P<opts_and_ext>.*?)$")
+# easy-thumbnails default pattern
+# e.g: source.jpg.100x100_q80_crop_upscale.jpg
+RE_ORIGINAL_FILENAME = re.compile(
+    r"^(?P<source_filename>.*?)\.(?P<opts_and_ext>[^.]+\.[^.]+)$"
+)
 
 
 def thumbnail_to_original_filename(thumbnail_name):
@@ -19,15 +18,15 @@ def thumbnail_to_original_filename(thumbnail_name):
 
 
 class ThumbnailerNameMixin:
-    thumbnail_basedir = ''
-    thumbnail_subdir = ''
-    thumbnail_prefix = ''
+    thumbnail_basedir = ""
+    thumbnail_subdir = ""
+    thumbnail_prefix = ""
 
     def get_thumbnail_name(self, thumbnail_options, transparent=False):
         """
-        A version of ``Thumbnailer.get_thumbnail_name`` that produces a
-        reproducible thumbnail name that can be converted back to the original
-        filename. For public files, it uses easy_thumbnails default naming.
+        Get thumbnail name using easy-thumbnails pattern.
+        For public files: Uses configurable naming via THUMBNAIL_NAMER
+        For private files: Uses easy-thumbnails default naming pattern regardless of THUMBNAIL_NAMER
         """
         is_public = False
         if hasattr(self, "thumbnail_storage"):
@@ -35,53 +34,26 @@ def get_thumbnail_name(self, thumbnail_options, transparent=False):
                 self.thumbnail_storage.__class__
             )
 
-        if is_public:
-            return super(ThumbnailerNameMixin, self).get_thumbnail_name(
-                thumbnail_options, transparent
-            )
-        
         path, source_filename = os.path.split(self.name)
-        source_extension = os.path.splitext(source_filename)[1][1:].lower()
-        preserve_extensions = self.thumbnail_preserve_extensions
-        if preserve_extensions is True or source_extension == 'svg' or \
-                isinstance(preserve_extensions, (list, tuple)) and source_extension in preserve_extensions:
-            extension = source_extension
-        elif transparent:
-            extension = self.thumbnail_transparency_extension
-        else:
-            extension = self.thumbnail_extension
-        extension = extension or 'jpg'
-
-        thumbnail_options = thumbnail_options.copy()
-        size = tuple(thumbnail_options.pop('size'))
-        initial_opts = ['{}x{}'.format(*size)]
-        quality = thumbnail_options.pop('quality', self.thumbnail_quality)
-        if extension == 'jpg':
-            initial_opts.append(f'q{quality}')
-        elif extension == 'svg':
-            thumbnail_options.pop('subsampling', None)
-            thumbnail_options.pop('upscale', None)
-
-        opts = list(thumbnail_options.items())
-        opts.sort()   # Sort the options so the file name is consistent.
-        opts = ['{}'.format(v is not True and f'{k}-{v}' or k)
-                for k, v in opts if v]
-        all_opts = '_'.join(initial_opts + opts)
-
-        basedir = self.thumbnail_basedir
-        subdir = self.thumbnail_subdir
-
-        # make sure our magic delimiter is not used in all_opts
-        all_opts = all_opts.replace('__', '_')
-        filename = f'{source_filename}__{all_opts}.{extension}'
+        thumbnail_name = super(ThumbnailerNameMixin, self).get_thumbnail_name(
+            thumbnail_options, transparent
+        )
+        if is_public:
+            return thumbnail_name
 
-        return os.path.join(basedir, path, subdir, filename)
+        base_thumb_name = os.path.basename(thumbnail_name)
+        return os.path.join(
+            self.thumbnail_basedir,
+            path,
+            self.thumbnail_subdir,
+            f"{source_filename}.{base_thumb_name}",
+        )
 
 
 class ActionThumbnailerMixin:
-    thumbnail_basedir = ''
-    thumbnail_subdir = ''
-    thumbnail_prefix = ''
+    thumbnail_basedir = ""
+    thumbnail_subdir = ""
+    thumbnail_prefix = ""
 
     def get_thumbnail_name(self, thumbnail_options, transparent=False):
         """
@@ -101,7 +73,7 @@ def thumbnail_exists(self, thumbnail_name):
 
 class FilerThumbnailer(ThumbnailerNameMixin, Thumbnailer):
     def __init__(self, *args, **kwargs):
-        self.thumbnail_basedir = kwargs.pop('thumbnail_basedir', '')
+        self.thumbnail_basedir = kwargs.pop("thumbnail_basedir", "")
         super().__init__(*args, **kwargs)
 
 
diff --git a/tests/test_models.py b/tests/test_models.py
index 6ee7ac204..f9466a5d5 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -35,14 +35,13 @@ def tearDown(self):
         for f in File.objects.all():
             f.delete()
 
-    def create_filer_image(self, owner=None, is_public=True):
+    def create_filer_image(self, owner=None):
         if owner is None:
             owner = self.superuser
         file_obj = DjangoFile(open(self.filename, 'rb'), name=self.image_name)
         image = Image.objects.create(owner=owner,
                                      original_filename=self.image_name,
-                                     file=file_obj,
-                                     is_public=is_public)
+                                     file=file_obj)
         return image
 
     def test_create_folder_structure(self):
@@ -81,14 +80,14 @@ def test_create_clipboard_item(self):
         self.assertEqual(Clipboard.objects.count(), 1)
 
     def test_create_icons(self):
-        image = self.create_filer_image(is_public=False)
+        image = self.create_filer_image()
         image.save()
         icons = image.icons
         file_basename = os.path.basename(image.file.path)
         self.assertEqual(len(icons), len(filer_settings.FILER_ADMIN_ICON_SIZES))
         for size in filer_settings.FILER_ADMIN_ICON_SIZES:
             self.assertEqual(os.path.basename(icons[size]),
-                             file_basename + '__{}x{}_q85_crop_subsampling-2_upscale.jpg'.format(size, size))
+                             file_basename + '.{}x{}_q85_crop_upscale.jpg'.format(size, size))
 
     def test_access_icons_property(self):
         """Test IconsMixin that calls static on a non-existent file"""
diff --git a/tests/test_thumbnails.py b/tests/test_thumbnails.py
index 4fbc1ce2a..b758d4ff7 100644
--- a/tests/test_thumbnails.py
+++ b/tests/test_thumbnails.py
@@ -45,13 +45,13 @@ def test_thumbnailer_class_for_public_files(self):
         image = self.create_filer_image(is_public=True)
         thumbnailer = image.easy_thumbnails_thumbnailer
         name = thumbnailer.get_thumbnail_name({"size": (100, 100)})
-        self.assertNotIn("__", name)
+        self.assertRegex(name, r"^.*\..*\.[^.]+$")
 
     def test_thumbnailer_class_for_private_files(self):
         image = self.create_filer_image(is_public=False)
         thumbnailer = image.easy_thumbnails_thumbnailer
         name = thumbnailer.get_thumbnail_name({"size": (100, 100)})
-        self.assertIn("__", name)
+        self.assertRegex(name, r"^.*\..*\.[^.]+$")
 
     @override_settings(THUMBNAIL_NAMER="tests.test_thumbnails.custom_namer")
     def test_thumbnail_custom_namer(self):
@@ -60,3 +60,12 @@ def test_thumbnail_custom_namer(self):
         name = thumbnailer.get_thumbnail_name({"size": (100, 100)})
         filename = os.path.basename(name)
         self.assertTrue(filename.startswith("custom_prefix_"))
+
+    @override_settings(THUMBNAIL_NAMER="tests.test_thumbnails.custom_namer")
+    def test_private_thumbnail_ignores_custom_namer(self):
+        image = self.create_filer_image(is_public=False)
+        thumbnailer = image.easy_thumbnails_thumbnailer
+        name = thumbnailer.get_thumbnail_name({"size": (100, 100)})
+        filename = os.path.basename(name)
+        self.assertFalse(filename.startswith("custom_prefix_"))
+        self.assertRegex(name, r"^.*\..*\.[^.]+$")

From 1908ab553c236f609d5e42d1b5a4c1ef84fe8491 Mon Sep 17 00:00:00 2001
From: Fabian Braun <fsbraun@gmx.de>
Date: Thu, 20 Mar 2025 12:01:01 +0100
Subject: [PATCH 3/4] Update filer/utils/filer_easy_thumbnails.py

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
---
 filer/utils/filer_easy_thumbnails.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/filer/utils/filer_easy_thumbnails.py b/filer/utils/filer_easy_thumbnails.py
index c4d4a3aa6..b60fec008 100644
--- a/filer/utils/filer_easy_thumbnails.py
+++ b/filer/utils/filer_easy_thumbnails.py
@@ -30,9 +30,8 @@ def get_thumbnail_name(self, thumbnail_options, transparent=False):
         """
         is_public = False
         if hasattr(self, "thumbnail_storage"):
-            is_public = "PrivateFileSystemStorage" not in str(
-                self.thumbnail_storage.__class__
-            )
+            from filer.storage import PrivateFileSystemStorage
+            is_public = not isinstance(self.thumbnail_storage, PrivateFileSystemStorage)
 
         path, source_filename = os.path.split(self.name)
         thumbnail_name = super(ThumbnailerNameMixin, self).get_thumbnail_name(

From 1934f899ad9610f9c27b6eab72d19954ababc7e1 Mon Sep 17 00:00:00 2001
From: earthcomfy <hanagetbel93@gmail.com>
Date: Mon, 24 Mar 2025 23:08:12 +0300
Subject: [PATCH 4/4] fix: use context manager to use default namer for private
 files

---
 filer/utils/filer_easy_thumbnails.py | 41 ++++++++++++++++++----------
 1 file changed, 26 insertions(+), 15 deletions(-)

diff --git a/filer/utils/filer_easy_thumbnails.py b/filer/utils/filer_easy_thumbnails.py
index b60fec008..ac3bb93d8 100644
--- a/filer/utils/filer_easy_thumbnails.py
+++ b/filer/utils/filer_easy_thumbnails.py
@@ -1,7 +1,9 @@
 import os
 import re
+from contextlib import contextmanager
 
 from easy_thumbnails.files import Thumbnailer
+from easy_thumbnails.namers import default
 
 # easy-thumbnails default pattern
 # e.g: source.jpg.100x100_q80_crop_upscale.jpg
@@ -17,6 +19,19 @@ def thumbnail_to_original_filename(thumbnail_name):
     return None
 
 
+@contextmanager
+def use_default_namer(thumbnailer):
+    """
+    Context manager to use the default easy-thumbnails namer for private files.
+    """
+    original_namer = thumbnailer.thumbnail_namer
+    thumbnailer.thumbnail_namer = default
+    try:
+        yield
+    finally:
+        thumbnailer.thumbnail_namer = original_namer
+
+
 class ThumbnailerNameMixin:
     thumbnail_basedir = ""
     thumbnail_subdir = ""
@@ -30,23 +45,19 @@ def get_thumbnail_name(self, thumbnail_options, transparent=False):
         """
         is_public = False
         if hasattr(self, "thumbnail_storage"):
-            from filer.storage import PrivateFileSystemStorage
-            is_public = not isinstance(self.thumbnail_storage, PrivateFileSystemStorage)
+            is_public = "PrivateFileSystemStorage" not in str(
+                self.thumbnail_storage.__class__
+            )
 
-        path, source_filename = os.path.split(self.name)
-        thumbnail_name = super(ThumbnailerNameMixin, self).get_thumbnail_name(
-            thumbnail_options, transparent
-        )
         if is_public:
-            return thumbnail_name
-
-        base_thumb_name = os.path.basename(thumbnail_name)
-        return os.path.join(
-            self.thumbnail_basedir,
-            path,
-            self.thumbnail_subdir,
-            f"{source_filename}.{base_thumb_name}",
-        )
+            return super(ThumbnailerNameMixin, self).get_thumbnail_name(
+                thumbnail_options, transparent
+            )
+
+        with use_default_namer(self):
+            return super(ThumbnailerNameMixin, self).get_thumbnail_name(
+                thumbnail_options, transparent
+            )
 
 
 class ActionThumbnailerMixin: