From 76c5e4f74bf8e4f36be1910baec1c99416b11b2f Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Tue, 28 Jul 2026 12:59:45 +0000 Subject: [PATCH] [Fixes #14373] Duplicate thumbnail creation fix --- geonode/base/models.py | 27 ++++++++------------------- geonode/base/tests.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/geonode/base/models.py b/geonode/base/models.py index 90850fe7f16..ac874721f9f 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -23,7 +23,9 @@ import math import uuid import logging +import tempfile import traceback +from io import BytesIO from typing import List, Optional, Union, Tuple from sequences.models import Sequence from sequences import get_next_value @@ -37,7 +39,7 @@ from django.db.models import Q, signals from django.db.utils import IntegrityError, OperationalError from django.contrib.auth.models import Group -from django.core.files.base import ContentFile +from django.core.files import File from django.contrib.auth import get_user_model from django.db.models.query import QuerySet from django.db.models.fields.json import JSONField @@ -1593,28 +1595,15 @@ def save_thumbnail(self, filename, image, thumbnail_algorithm=ThumbnailAlgorithm image = None if image: - actual_name = storage_manager.save(upload_path, ContentFile(image)) - actual_file_name = os.path.basename(actual_name) - - if filename != actual_file_name: - upload_path = upload_path.replace(filename, actual_file_name) url = storage_manager.url(upload_path) try: - # Optimize the Thumbnail size and resolution - im = Image.open(storage_manager.open(actual_name)) + im = Image.open(BytesIO(image)) im = thumbnail_algorithm(im, **kwargs) - # Saving the thumb into a temporary directory on file system - tmp_location = os.path.abspath(f"{settings.MEDIA_ROOT}/{upload_path}") - im.save(tmp_location, quality="high") - - with open(tmp_location, "rb+") as img: - # Saving the img via storage manager - storage_manager.save(upload_path, img) - - # If we use a remote storage, the local img is deleted - if tmp_location != storage_manager.path(upload_path): - os.remove(tmp_location) + with tempfile.NamedTemporaryFile(suffix=".jpg") as optimized_tmp: + im.save(optimized_tmp, format="JPEG", quality="high") + optimized_tmp.seek(0) + storage_manager.save(upload_path, File(optimized_tmp)) except Exception as e: logger.exception(e) diff --git a/geonode/base/tests.py b/geonode/base/tests.py index 8ead751b872..5c47d06677d 100644 --- a/geonode/base/tests.py +++ b/geonode/base/tests.py @@ -147,6 +147,35 @@ def test_thumb_utils_methods(self, image): thumb_utils.remove_thumbs(filename) self.assertFalse(thumb_utils.thumb_exists(filename)) + def test_save_thumbnail_creates_a_single_file(self): + """ + A single save_thumbnail call must create exactly one thumbnail file + """ + image = Image.new("RGB", (100, 100)) + for x in range(100): + image.putpixel((x, x), (x + 1, (x * 2) % 256, (x * 3) % 256)) + with BytesIO() as output: + image.save(output, format="PNG") + content = output.getvalue() + + name_prefix = f"dataset-{self.rb.uuid}-thumb" + thumbs_before = set(thumb_utils.get_thumbs()) + + self.rb.save_thumbnail(f"{name_prefix}-{uuid4()}.png", image=content) + + new_thumbs = set(thumb_utils.get_thumbs()) - thumbs_before + try: + # exactly one file must have been created (no suffixed duplicate) ... + self.assertEqual( + len(new_thumbs), + 1, + msg=f"Expected a single thumbnail file, found: {sorted(new_thumbs)}", + ) + # ... and it must be the one referenced by the resource + self.assertIn(os.path.basename(self.rb.thumbnail_path), new_thumbs) + finally: + thumb_utils.remove_thumbs(name_prefix) + class TestThumbnailUrl(GeoNodeBaseTestSupport): def setUp(self):