Изменение структуры хранения изображений и исправление ошибки дублирования ID
This commit is contained in:
@@ -909,7 +909,7 @@ class ProductPhoto(models.Model):
|
|||||||
"""
|
"""
|
||||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='photos',
|
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='photos',
|
||||||
verbose_name="Товар")
|
verbose_name="Товар")
|
||||||
image = models.ImageField(upload_to='products/originals/', verbose_name="Оригинальное фото")
|
image = models.ImageField(upload_to='products/temp/', verbose_name="Оригинальное фото")
|
||||||
order = models.PositiveIntegerField(default=0, verbose_name="Порядок")
|
order = models.PositiveIntegerField(default=0, verbose_name="Порядок")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||||
|
|
||||||
@@ -928,31 +928,45 @@ class ProductPhoto(models.Model):
|
|||||||
from .utils.image_processor import ImageProcessor
|
from .utils.image_processor import ImageProcessor
|
||||||
|
|
||||||
is_new = not self.pk
|
is_new = not self.pk
|
||||||
old_image_path = None
|
|
||||||
|
|
||||||
# Если это обновление существующего объекта, сохраняем старый путь для удаления
|
# Если это новый объект с изображением, нужно сначала сохранить без изображения, чтобы получить ID
|
||||||
if not is_new:
|
if is_new and self.image:
|
||||||
try:
|
# Сохраняем объект без изображения, чтобы получить ID
|
||||||
old_obj = ProductPhoto.objects.get(pk=self.pk)
|
temp_image = self.image
|
||||||
if old_obj.image and old_obj.image != self.image:
|
self.image = None
|
||||||
old_image_path = old_obj.image.name
|
super().save(*args, **kwargs)
|
||||||
except ProductPhoto.DoesNotExist:
|
|
||||||
pass
|
# Теперь обрабатываем изображение с известными ID
|
||||||
|
processed_paths = ImageProcessor.process_image(temp_image, 'products', entity_id=self.product.id, photo_id=self.id)
|
||||||
# Если было загружено новое изображение
|
|
||||||
if self.image and (is_new or old_image_path):
|
|
||||||
# Обрабатываем изображение с использованием slug товара как идентификатора
|
|
||||||
# slug гарантирует уникальность и читаемость имени файла
|
|
||||||
identifier = self.product.slug
|
|
||||||
processed_paths = ImageProcessor.process_image(self.image, 'products', identifier=identifier)
|
|
||||||
# Сохраняем только путь к оригиналу в поле image
|
|
||||||
self.image = processed_paths['original']
|
self.image = processed_paths['original']
|
||||||
|
|
||||||
|
# Обновляем только поле image, чтобы избежать рекурсии и дублирования ID
|
||||||
|
super().save(update_fields=['image'])
|
||||||
|
else:
|
||||||
|
# Проверяем старый путь для удаления, если это обновление
|
||||||
|
old_image_path = None
|
||||||
|
if self.pk:
|
||||||
|
try:
|
||||||
|
old_obj = ProductPhoto.objects.get(pk=self.pk)
|
||||||
|
if old_obj.image and old_obj.image != self.image:
|
||||||
|
old_image_path = old_obj.image.name
|
||||||
|
except ProductPhoto.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Проверяем, нужно ли обрабатывать изображение
|
||||||
|
if self.image and old_image_path:
|
||||||
|
# Обновление существующего изображения
|
||||||
|
processed_paths = ImageProcessor.process_image(self.image, 'products', entity_id=self.product.id, photo_id=self.id)
|
||||||
|
self.image = processed_paths['original']
|
||||||
|
|
||||||
# Удаляем старые версии если это обновление
|
# Удаляем старые версии
|
||||||
if old_image_path:
|
ImageProcessor.delete_all_versions('products', old_image_path, entity_id=self.product.id, photo_id=self.id)
|
||||||
ImageProcessor.delete_all_versions('products', old_image_path)
|
|
||||||
|
# Обновляем только поле image, чтобы избежать рекурсии
|
||||||
super().save(*args, **kwargs)
|
super().save(update_fields=['image'])
|
||||||
|
else:
|
||||||
|
# Просто сохраняем без обработки изображения
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""Удаляет все версии изображения при удалении фото"""
|
"""Удаляет все версии изображения при удалении фото"""
|
||||||
@@ -964,7 +978,7 @@ class ProductPhoto(models.Model):
|
|||||||
if self.image:
|
if self.image:
|
||||||
try:
|
try:
|
||||||
logger.info(f"[ProductPhoto.delete] Удаляем изображение: {self.image.name}")
|
logger.info(f"[ProductPhoto.delete] Удаляем изображение: {self.image.name}")
|
||||||
ImageProcessor.delete_all_versions('products', self.image.name)
|
ImageProcessor.delete_all_versions('products', self.image.name, entity_id=self.product.id, photo_id=self.id)
|
||||||
logger.info(f"[ProductPhoto.delete] ✓ Все версии изображения удалены")
|
logger.info(f"[ProductPhoto.delete] ✓ Все версии изображения удалены")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[ProductPhoto.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True)
|
logger.error(f"[ProductPhoto.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True)
|
||||||
@@ -999,7 +1013,7 @@ class ProductKitPhoto(models.Model):
|
|||||||
"""
|
"""
|
||||||
kit = models.ForeignKey(ProductKit, on_delete=models.CASCADE, related_name='photos',
|
kit = models.ForeignKey(ProductKit, on_delete=models.CASCADE, related_name='photos',
|
||||||
verbose_name="Комплект")
|
verbose_name="Комплект")
|
||||||
image = models.ImageField(upload_to='kits/originals/', verbose_name="Оригинальное фото")
|
image = models.ImageField(upload_to='kits/temp/', verbose_name="Оригинальное фото")
|
||||||
order = models.PositiveIntegerField(default=0, verbose_name="Порядок")
|
order = models.PositiveIntegerField(default=0, verbose_name="Порядок")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||||
|
|
||||||
@@ -1018,31 +1032,45 @@ class ProductKitPhoto(models.Model):
|
|||||||
from .utils.image_processor import ImageProcessor
|
from .utils.image_processor import ImageProcessor
|
||||||
|
|
||||||
is_new = not self.pk
|
is_new = not self.pk
|
||||||
old_image_path = None
|
|
||||||
|
|
||||||
# Если это обновление существующего объекта, сохраняем старый путь для удаления
|
# Если это новый объект с изображением, нужно сначала сохранить без изображения, чтобы получить ID
|
||||||
if not is_new:
|
if is_new and self.image:
|
||||||
try:
|
# Сохраняем объект без изображения, чтобы получить ID
|
||||||
old_obj = ProductKitPhoto.objects.get(pk=self.pk)
|
temp_image = self.image
|
||||||
if old_obj.image and old_obj.image != self.image:
|
self.image = None
|
||||||
old_image_path = old_obj.image.name
|
super().save(*args, **kwargs)
|
||||||
except ProductKitPhoto.DoesNotExist:
|
|
||||||
pass
|
# Теперь обрабатываем изображение с известными ID
|
||||||
|
processed_paths = ImageProcessor.process_image(temp_image, 'kits', entity_id=self.kit.id, photo_id=self.id)
|
||||||
# Если было загружено новое изображение
|
|
||||||
if self.image and (is_new or old_image_path):
|
|
||||||
# Обрабатываем изображение с использованием slug комплекта как идентификатора
|
|
||||||
# slug гарантирует уникальность и читаемость имени файла
|
|
||||||
identifier = self.kit.slug
|
|
||||||
processed_paths = ImageProcessor.process_image(self.image, 'kits', identifier=identifier)
|
|
||||||
# Сохраняем только путь к оригиналу в поле image
|
|
||||||
self.image = processed_paths['original']
|
self.image = processed_paths['original']
|
||||||
|
|
||||||
|
# Обновляем только поле image, чтобы избежать рекурсии и дублирования ID
|
||||||
|
super().save(update_fields=['image'])
|
||||||
|
else:
|
||||||
|
# Проверяем старый путь для удаления, если это обновление
|
||||||
|
old_image_path = None
|
||||||
|
if self.pk:
|
||||||
|
try:
|
||||||
|
old_obj = ProductKitPhoto.objects.get(pk=self.pk)
|
||||||
|
if old_obj.image and old_obj.image != self.image:
|
||||||
|
old_image_path = old_obj.image.name
|
||||||
|
except ProductKitPhoto.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Проверяем, нужно ли обрабатывать изображение
|
||||||
|
if self.image and old_image_path:
|
||||||
|
# Обновление существующего изображения
|
||||||
|
processed_paths = ImageProcessor.process_image(self.image, 'kits', entity_id=self.kit.id, photo_id=self.id)
|
||||||
|
self.image = processed_paths['original']
|
||||||
|
|
||||||
# Удаляем старые версии если это обновление
|
# Удаляем старые версии
|
||||||
if old_image_path:
|
ImageProcessor.delete_all_versions('kits', old_image_path, entity_id=self.kit.id, photo_id=self.id)
|
||||||
ImageProcessor.delete_all_versions('kits', old_image_path)
|
|
||||||
|
# Обновляем только поле image, чтобы избежать рекурсии
|
||||||
super().save(*args, **kwargs)
|
super().save(update_fields=['image'])
|
||||||
|
else:
|
||||||
|
# Просто сохраняем без обработки изображения
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""Удаляет все версии изображения при удалении фото"""
|
"""Удаляет все версии изображения при удалении фото"""
|
||||||
@@ -1054,7 +1082,7 @@ class ProductKitPhoto(models.Model):
|
|||||||
if self.image:
|
if self.image:
|
||||||
try:
|
try:
|
||||||
logger.info(f"[ProductKitPhoto.delete] Удаляем изображение: {self.image.name}")
|
logger.info(f"[ProductKitPhoto.delete] Удаляем изображение: {self.image.name}")
|
||||||
ImageProcessor.delete_all_versions('kits', self.image.name)
|
ImageProcessor.delete_all_versions('kits', self.image.name, entity_id=self.kit.id, photo_id=self.id)
|
||||||
logger.info(f"[ProductKitPhoto.delete] ✓ Все версии изображения удалены")
|
logger.info(f"[ProductKitPhoto.delete] ✓ Все версии изображения удалены")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[ProductKitPhoto.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True)
|
logger.error(f"[ProductKitPhoto.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True)
|
||||||
@@ -1089,7 +1117,7 @@ class ProductCategoryPhoto(models.Model):
|
|||||||
"""
|
"""
|
||||||
category = models.ForeignKey(ProductCategory, on_delete=models.CASCADE, related_name='photos',
|
category = models.ForeignKey(ProductCategory, on_delete=models.CASCADE, related_name='photos',
|
||||||
verbose_name="Категория")
|
verbose_name="Категория")
|
||||||
image = models.ImageField(upload_to='categories/originals/', verbose_name="Оригинальное фото")
|
image = models.ImageField(upload_to='categories/temp/', verbose_name="Оригинальное фото")
|
||||||
order = models.PositiveIntegerField(default=0, verbose_name="Порядок")
|
order = models.PositiveIntegerField(default=0, verbose_name="Порядок")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||||
|
|
||||||
@@ -1108,31 +1136,45 @@ class ProductCategoryPhoto(models.Model):
|
|||||||
from .utils.image_processor import ImageProcessor
|
from .utils.image_processor import ImageProcessor
|
||||||
|
|
||||||
is_new = not self.pk
|
is_new = not self.pk
|
||||||
old_image_path = None
|
|
||||||
|
|
||||||
# Если это обновление существующего объекта, сохраняем старый путь для удаления
|
# Если это новый объект с изображением, нужно сначала сохранить без изображения, чтобы получить ID
|
||||||
if not is_new:
|
if is_new and self.image:
|
||||||
try:
|
# Сохраняем объект без изображения, чтобы получить ID
|
||||||
old_obj = ProductCategoryPhoto.objects.get(pk=self.pk)
|
temp_image = self.image
|
||||||
if old_obj.image and old_obj.image != self.image:
|
self.image = None
|
||||||
old_image_path = old_obj.image.name
|
super().save(*args, **kwargs)
|
||||||
except ProductCategoryPhoto.DoesNotExist:
|
|
||||||
pass
|
# Теперь обрабатываем изображение с известными ID
|
||||||
|
processed_paths = ImageProcessor.process_image(temp_image, 'categories', entity_id=self.category.id, photo_id=self.id)
|
||||||
# Если было загружено новое изображение
|
|
||||||
if self.image and (is_new or old_image_path):
|
|
||||||
# Обрабатываем изображение с использованием slug категории как идентификатора
|
|
||||||
# slug гарантирует уникальность и читаемость имени файла
|
|
||||||
identifier = self.category.slug
|
|
||||||
processed_paths = ImageProcessor.process_image(self.image, 'categories', identifier=identifier)
|
|
||||||
# Сохраняем только путь к оригиналу в поле image
|
|
||||||
self.image = processed_paths['original']
|
self.image = processed_paths['original']
|
||||||
|
|
||||||
|
# Обновляем только поле image, чтобы избежать рекурсии и дублирования ID
|
||||||
|
super().save(update_fields=['image'])
|
||||||
|
else:
|
||||||
|
# Проверяем старый путь для удаления, если это обновление
|
||||||
|
old_image_path = None
|
||||||
|
if self.pk:
|
||||||
|
try:
|
||||||
|
old_obj = ProductCategoryPhoto.objects.get(pk=self.pk)
|
||||||
|
if old_obj.image and old_obj.image != self.image:
|
||||||
|
old_image_path = old_obj.image.name
|
||||||
|
except ProductCategoryPhoto.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Проверяем, нужно ли обрабатывать изображение
|
||||||
|
if self.image and old_image_path:
|
||||||
|
# Обновление существующего изображения
|
||||||
|
processed_paths = ImageProcessor.process_image(self.image, 'categories', entity_id=self.category.id, photo_id=self.id)
|
||||||
|
self.image = processed_paths['original']
|
||||||
|
|
||||||
# Удаляем старые версии если это обновление
|
# Удаляем старые версии
|
||||||
if old_image_path:
|
ImageProcessor.delete_all_versions('categories', old_image_path, entity_id=self.category.id, photo_id=self.id)
|
||||||
ImageProcessor.delete_all_versions('categories', old_image_path)
|
|
||||||
|
# Обновляем только поле image, чтобы избежать рекурсии
|
||||||
super().save(*args, **kwargs)
|
super().save(update_fields=['image'])
|
||||||
|
else:
|
||||||
|
# Просто сохраняем без обработки изображения
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""Удаляет все версии изображения при удалении фото"""
|
"""Удаляет все версии изображения при удалении фото"""
|
||||||
@@ -1144,7 +1186,7 @@ class ProductCategoryPhoto(models.Model):
|
|||||||
if self.image:
|
if self.image:
|
||||||
try:
|
try:
|
||||||
logger.info(f"[ProductCategoryPhoto.delete] Удаляем изображение: {self.image.name}")
|
logger.info(f"[ProductCategoryPhoto.delete] Удаляем изображение: {self.image.name}")
|
||||||
ImageProcessor.delete_all_versions('categories', self.image.name)
|
ImageProcessor.delete_all_versions('categories', self.image.name, entity_id=self.category.id, photo_id=self.id)
|
||||||
logger.info(f"[ProductCategoryPhoto.delete] ✓ Все версии изображения удалены")
|
logger.info(f"[ProductCategoryPhoto.delete] ✓ Все версии изображения удалены")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[ProductCategoryPhoto.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True)
|
logger.error(f"[ProductCategoryPhoto.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True)
|
||||||
|
|||||||
@@ -54,24 +54,23 @@ class ImageProcessor:
|
|||||||
return folders.get(size_key, size_key)
|
return folders.get(size_key, size_key)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def process_image(image_file, base_path, identifier=None):
|
def process_image(image_file, base_path, entity_id=None, photo_id=None):
|
||||||
"""
|
"""
|
||||||
Обрабатывает загруженное изображение и создает несколько версий.
|
Обрабатывает загруженное изображение и создает несколько версий.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
image_file: Загруженный файл изображения (InMemoryUploadedFile)
|
image_file: Загруженный файл изображения (InMemoryUploadedFile)
|
||||||
base_path: Базовый путь для сохранения (например, 'products', 'kits', 'categories')
|
base_path: Базовый путь для сохранения (например, 'products', 'kits', 'categories')
|
||||||
identifier: (Optional) Идентификатор товара/категории (slug, SKU, имя)
|
entity_id: ID сущности (product_id, category_id, kit_id)
|
||||||
для более понятного имени файла.
|
photo_id: ID фотографии
|
||||||
Пример: 'robot-50cm', 'bouquet-red', 'category-flowers'
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Словарь с путями сохраненных файлов
|
dict: Словарь с путями сохраненных файлов
|
||||||
{
|
{
|
||||||
'original': 'products/originals/robot-50cm_1729611234567_original.jpg',
|
'original': 'products/<entity_id>/<photo_id>/original.jpg',
|
||||||
'large': 'products/large/robot-50cm_1729611234567_large.webp',
|
'large': 'products/<entity_id>/<photo_id>/large.webp',
|
||||||
'medium': 'products/medium/robot-50cm_1729611234567_medium.webp',
|
'medium': 'products/<entity_id>/<photo_id>/medium.webp',
|
||||||
'thumbnail': 'products/thumbnails/robot-50cm_1729611234567_thumbnail.webp',
|
'thumbnail': 'products/<entity_id>/<photo_id>/thumb.webp',
|
||||||
}
|
}
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@@ -92,20 +91,11 @@ class ImageProcessor:
|
|||||||
elif img.mode != 'RGB':
|
elif img.mode != 'RGB':
|
||||||
img = img.convert('RGB')
|
img = img.convert('RGB')
|
||||||
|
|
||||||
# Генерируем уникальное имя файла
|
|
||||||
if identifier:
|
|
||||||
# Используем переданный идентификатор (slug) + timestamp для уникальности
|
|
||||||
base_filename = f"{identifier}_{ImageProcessor._generate_unique_id()}"
|
|
||||||
else:
|
|
||||||
# Если идентификатор не передан, используем исходное имя файла
|
|
||||||
original_name = image_file.name.split('.')[0]
|
|
||||||
base_filename = f"{original_name}_{ImageProcessor._generate_unique_id()}"
|
|
||||||
|
|
||||||
saved_paths = {}
|
saved_paths = {}
|
||||||
|
|
||||||
# Сохраняем оригинал (масштабируем если больше max_width/max_height)
|
# Сохраняем оригинал (масштабируем если больше max_width/max_height)
|
||||||
original_path = ImageProcessor._save_image_version(
|
original_path = ImageProcessor._save_image_version(
|
||||||
img, base_path, base_filename, 'original'
|
img, base_path, entity_id, photo_id, 'original'
|
||||||
)
|
)
|
||||||
saved_paths['original'] = original_path
|
saved_paths['original'] = original_path
|
||||||
|
|
||||||
@@ -113,8 +103,12 @@ class ImageProcessor:
|
|||||||
for size_key in ['large', 'medium', 'thumbnail']:
|
for size_key in ['large', 'medium', 'thumbnail']:
|
||||||
size_dims = ImageProcessor._get_size_dimensions(size_key)
|
size_dims = ImageProcessor._get_size_dimensions(size_key)
|
||||||
resized_img = ImageProcessor._resize_image(img, size_dims)
|
resized_img = ImageProcessor._resize_image(img, size_dims)
|
||||||
|
|
||||||
|
# Переименовываем thumbnail в thumb для конечного пользователя
|
||||||
|
final_size_key = 'thumb' if size_key == 'thumbnail' else size_key
|
||||||
|
|
||||||
size_path = ImageProcessor._save_image_version(
|
size_path = ImageProcessor._save_image_version(
|
||||||
resized_img, base_path, base_filename, size_key
|
resized_img, base_path, entity_id, photo_id, size_key, final_size_key
|
||||||
)
|
)
|
||||||
saved_paths[size_key] = size_path
|
saved_paths[size_key] = size_path
|
||||||
|
|
||||||
@@ -169,17 +163,18 @@ class ImageProcessor:
|
|||||||
return new_img
|
return new_img
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _save_image_version(img, base_path, base_filename, size_key):
|
def _save_image_version(img, base_path, entity_id, photo_id, size_key, final_size_name=None):
|
||||||
"""
|
"""
|
||||||
Сохраняет версию изображения с информацией о размере в имени файла.
|
Сохраняет версию изображения в новой структуре с фиксированными именами.
|
||||||
Использует формат и качество из конфигурации для каждого размера.
|
Использует формат и качество из конфигурации для каждого размера.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
img: PIL Image object
|
img: PIL Image object
|
||||||
base_path: Базовый путь (например, 'products')
|
base_path: Базовый путь (например, 'products')
|
||||||
base_filename: Базовое имя файла без расширения и размера
|
entity_id: ID сущности (product_id, category_id, kit_id)
|
||||||
(например, 'robot-50cm_1729611234567')
|
photo_id: ID фотографии
|
||||||
size_key: Ключ размера ('original', 'large', 'medium', 'thumbnail')
|
size_key: Ключ размера ('original', 'large', 'medium', 'thumbnail')
|
||||||
|
final_size_name: Имя размера в файле ('original', 'large', 'medium', 'thumb')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Путь сохраненного файла относительно MEDIA_ROOT
|
str: Путь сохраненного файла относительно MEDIA_ROOT
|
||||||
@@ -197,12 +192,15 @@ class ImageProcessor:
|
|||||||
}
|
}
|
||||||
extension = ext_map.get(image_format, 'jpg')
|
extension = ext_map.get(image_format, 'jpg')
|
||||||
|
|
||||||
# Создаем имя файла с указанием размера и расширением
|
# Определяем имя размера для файла ('thumb' вместо 'thumbnail')
|
||||||
filename = f"{base_filename}_{size_key}.{extension}"
|
if final_size_name is None:
|
||||||
|
final_size_name = 'thumb' if size_key == 'thumbnail' else size_key
|
||||||
|
|
||||||
# Создаем путь в правильной папке
|
# Создаем имя файла с фиксированным именем и расширением
|
||||||
folder = ImageProcessor._get_folder(size_key)
|
filename = f"{final_size_name}.{extension}"
|
||||||
file_path = f"{base_path}/{folder}/{filename}"
|
|
||||||
|
# Создаем путь в новой структуре: base_path/entity_id/photo_id/filename
|
||||||
|
file_path = f"{base_path}/{entity_id}/{photo_id}/{filename}"
|
||||||
|
|
||||||
# Сохраняем в памяти
|
# Сохраняем в памяти
|
||||||
img_io = BytesIO()
|
img_io = BytesIO()
|
||||||
@@ -248,64 +246,94 @@ class ImageProcessor:
|
|||||||
return saved_path
|
return saved_path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_all_versions(base_path, original_image_path):
|
def delete_all_versions(base_path, original_image_path, entity_id=None, photo_id=None):
|
||||||
"""
|
"""
|
||||||
Удаляет все версии изображения (original, large, medium, thumbnail).
|
Удаляет все версии изображения (original, large, medium, thumb) из новой структуры.
|
||||||
|
|
||||||
Работает с форматом имен файлов:
|
|
||||||
- robot-50cm_1729611234567_original.jpg
|
|
||||||
- robot-50cm_1729611234567_large.webp
|
|
||||||
- robot-50cm_1729611234567_medium.webp
|
|
||||||
- robot-50cm_1729611234567_thumbnail.webp
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
base_path: Базовый путь (например, 'products')
|
base_path: Базовый путь (например, 'products')
|
||||||
original_image_path: Путь к оригинальному файлу (из БД)
|
original_image_path: Путь к оригинальному файлу (из БД)
|
||||||
|
entity_id: ID сущности (product_id, category_id, kit_id)
|
||||||
|
photo_id: ID фотографии
|
||||||
"""
|
"""
|
||||||
if not original_image_path:
|
if not original_image_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Извлекаем имя файла из пути
|
# Если переданы entity_id и photo_id, используем новую структуру
|
||||||
filename = os.path.basename(str(original_image_path))
|
if entity_id is not None and photo_id is not None:
|
||||||
|
# Удаляем файлы в новой структуре
|
||||||
|
for size_key in ['original', 'large', 'medium', 'thumbnail']:
|
||||||
|
format_config = ImageProcessor._get_format_config(size_key)
|
||||||
|
image_format = format_config.get('format', 'JPEG')
|
||||||
|
|
||||||
# Удаляем расширение и последний размер для получения base_filename
|
# Определяем расширение
|
||||||
# Из 'robot-50cm_1729611234567_original.jpg' получаем 'robot-50cm_1729611234567'
|
ext_map = {
|
||||||
# Важно: используем максимум 2 разделителя, т.к. в base_filename может быть _
|
'JPEG': 'jpg',
|
||||||
parts = filename.rsplit('_', 1)
|
'WEBP': 'webp',
|
||||||
if len(parts) == 2:
|
'PNG': 'png',
|
||||||
base_filename = parts[0]
|
}
|
||||||
|
extension = ext_map.get(image_format, 'jpg')
|
||||||
|
|
||||||
|
# Определяем имя размера для файла ('thumb' вместо 'thumbnail')
|
||||||
|
final_size_name = 'thumb' if size_key == 'thumbnail' else size_key
|
||||||
|
|
||||||
|
# Создаем имя файла для этого размера
|
||||||
|
size_filename = f"{final_size_name}.{extension}"
|
||||||
|
|
||||||
|
# Создаем путь в новой структуре
|
||||||
|
file_path = f"{base_path}/{entity_id}/{photo_id}/{size_filename}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if default_storage.exists(file_path):
|
||||||
|
default_storage.delete(file_path)
|
||||||
|
logger.info(f"Deleted file: {file_path}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"File not found: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting {file_path}: {str(e)}", exc_info=True)
|
||||||
else:
|
else:
|
||||||
# Если формат не совпадает, используем полное имя без расширения
|
# Для совместимости с предыдущей структурой
|
||||||
base_filename = os.path.splitext(filename)[0]
|
# Извлекаем имя файла из пути
|
||||||
|
filename = os.path.basename(str(original_image_path))
|
||||||
|
|
||||||
config = ImageProcessor._get_config()
|
# Удаляем расширение и последний размер для получения base_filename
|
||||||
|
# Из 'robot-50cm_1729611234567_original.jpg' получаем 'robot-50cm_1729611234567'
|
||||||
|
# Важно: используем максимум 2 разделителя, т.к. в base_filename может быть _
|
||||||
|
parts = filename.rsplit('_', 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
base_filename = parts[0]
|
||||||
|
else:
|
||||||
|
# Если формат не совпадает, используем полное имя без расширения
|
||||||
|
base_filename = os.path.splitext(filename)[0]
|
||||||
|
|
||||||
# Удаляем все версии
|
config = ImageProcessor._get_config()
|
||||||
for size_key in ['original', 'large', 'medium', 'thumbnail']:
|
|
||||||
format_config = ImageProcessor._get_format_config(size_key)
|
|
||||||
image_format = format_config.get('format', 'JPEG')
|
|
||||||
|
|
||||||
# Определяем расширение
|
# Удаляем все версии в старой структуре
|
||||||
ext_map = {
|
for size_key in ['original', 'large', 'medium', 'thumbnail']:
|
||||||
'JPEG': 'jpg',
|
format_config = ImageProcessor._get_format_config(size_key)
|
||||||
'WEBP': 'webp',
|
image_format = format_config.get('format', 'JPEG')
|
||||||
'PNG': 'png',
|
|
||||||
}
|
|
||||||
extension = ext_map.get(image_format, 'jpg')
|
|
||||||
|
|
||||||
# Создаем имя файла для этого размера
|
# Определяем расширение
|
||||||
size_filename = f"{base_filename}_{size_key}.{extension}"
|
ext_map = {
|
||||||
folder = ImageProcessor._get_folder(size_key)
|
'JPEG': 'jpg',
|
||||||
file_path = f"{base_path}/{folder}/{size_filename}"
|
'WEBP': 'webp',
|
||||||
|
'PNG': 'png',
|
||||||
|
}
|
||||||
|
extension = ext_map.get(image_format, 'jpg')
|
||||||
|
|
||||||
try:
|
# Создаем имя файла для этого размера
|
||||||
if default_storage.exists(file_path):
|
size_filename = f"{base_filename}_{size_key}.{extension}"
|
||||||
default_storage.delete(file_path)
|
folder = ImageProcessor._get_folder(size_key)
|
||||||
logger.info(f"Deleted file: {file_path}")
|
file_path = f"{base_path}/{folder}/{size_filename}"
|
||||||
else:
|
|
||||||
logger.warning(f"File not found: {file_path}")
|
try:
|
||||||
except Exception as e:
|
if default_storage.exists(file_path):
|
||||||
logger.error(f"Error deleting {file_path}: {str(e)}", exc_info=True)
|
default_storage.delete(file_path)
|
||||||
|
logger.info(f"Deleted file: {file_path}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"File not found: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting {file_path}: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _generate_unique_id():
|
def _generate_unique_id():
|
||||||
|
|||||||
@@ -54,16 +54,16 @@ class ImageService:
|
|||||||
"""
|
"""
|
||||||
Получает URL изображения нужного размера.
|
Получает URL изображения нужного размера.
|
||||||
|
|
||||||
Работает с новым форматом имён файлов с поддержкой разных расширений:
|
Работает с новой структурой:
|
||||||
- robot-50cm_1729611234567_original.jpg (JPEG, оригинал)
|
- products/<entity_id>/<photo_id>/original.jpg
|
||||||
- robot-50cm_1729611234567_large.webp (WebP)
|
- products/<entity_id>/<photo_id>/large.webp
|
||||||
- robot-50cm_1729611234567_medium.webp (WebP)
|
- products/<entity_id>/<photo_id>/medium.webp
|
||||||
- robot-50cm_1729611234567_thumbnail.webp (WebP)
|
- products/<entity_id>/<photo_id>/thumb.webp
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
original_image_path: Путь к оригинальному файлу (из models.image)
|
original_image_path: Путь к оригинальному файлу (из models.image)
|
||||||
Обычно это путь к файлу 'original'
|
Обычно это путь к файлу 'original'
|
||||||
Пример: products/originals/robot-50cm_1729611234567_original.jpg
|
Пример: products/123/456/original.jpg
|
||||||
size: Размер ('original', 'large', 'medium', 'thumbnail')
|
size: Размер ('original', 'large', 'medium', 'thumbnail')
|
||||||
По умолчанию 'medium'
|
По умолчанию 'medium'
|
||||||
|
|
||||||
@@ -74,18 +74,70 @@ class ImageService:
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Извлекаем имя файла и базовый путь
|
# Работаем с новой структурой: products/<entity_id>/<photo_id>/original.jpg
|
||||||
path_str = str(original_image_path)
|
path_str = str(original_image_path)
|
||||||
|
parts = path_str.split('/')
|
||||||
|
|
||||||
|
if len(parts) >= 3:
|
||||||
|
# Извлекаем base_path, entity_id, photo_id из пути
|
||||||
|
base_path = parts[0] # products, kits, categories
|
||||||
|
entity_id = parts[1] # ID сущности
|
||||||
|
photo_id = parts[2] # ID фото
|
||||||
|
|
||||||
|
# Определяем размер в имени файла
|
||||||
|
filename = parts[-1] if parts else os.path.basename(path_str)
|
||||||
|
|
||||||
|
# Проверяем, является ли это новой структурой
|
||||||
|
if filename in ['original.jpg', 'large.webp', 'medium.webp', 'thumb.webp']:
|
||||||
|
# Это новая структура, заменяем только размер
|
||||||
|
ext_map = {
|
||||||
|
'original': 'jpg',
|
||||||
|
'large': 'webp',
|
||||||
|
'medium': 'webp',
|
||||||
|
'thumbnail': 'webp',
|
||||||
|
}
|
||||||
|
target_ext = ext_map.get(size, 'jpg')
|
||||||
|
|
||||||
|
# Переименовываем thumbnail в thumb
|
||||||
|
final_size_name = 'thumb' if size == 'thumbnail' else size
|
||||||
|
|
||||||
|
# Создаем путь в новой структуре
|
||||||
|
new_path = f"{base_path}/{entity_id}/{photo_id}/{final_size_name}.{target_ext}"
|
||||||
|
|
||||||
|
# Проверяем существование файла
|
||||||
|
if default_storage.exists(new_path):
|
||||||
|
return f"{settings.MEDIA_URL}{new_path}"
|
||||||
|
|
||||||
|
# Если файл не найден, пробуем с другим расширением
|
||||||
|
# Определяем расширение из конфигурации
|
||||||
|
format_config = ImageService._get_format_config(size)
|
||||||
|
image_format = format_config.get('format', 'JPEG')
|
||||||
|
|
||||||
|
ext_map_config = {
|
||||||
|
'JPEG': 'jpg',
|
||||||
|
'WEBP': 'webp',
|
||||||
|
'PNG': 'png',
|
||||||
|
}
|
||||||
|
target_ext = ext_map_config.get(image_format, 'jpg')
|
||||||
|
|
||||||
|
final_size_name = 'thumb' if size == 'thumbnail' else size
|
||||||
|
fallback_path = f"{base_path}/{entity_id}/{photo_id}/{final_size_name}.{target_ext}"
|
||||||
|
|
||||||
|
if default_storage.exists(fallback_path):
|
||||||
|
return f"{settings.MEDIA_URL}{fallback_path}"
|
||||||
|
|
||||||
|
return f"{settings.MEDIA_URL}{path_str}"
|
||||||
|
|
||||||
|
# Старая структура для совместимости
|
||||||
filename = os.path.basename(path_str)
|
filename = os.path.basename(path_str)
|
||||||
|
|
||||||
# Определяем базовый путь (products, kits, categories)
|
# Определяем базовый путь (products, kits, categories)
|
||||||
parts = path_str.split('/')
|
|
||||||
if len(parts) > 0:
|
if len(parts) > 0:
|
||||||
base_path = parts[0]
|
base_path = parts[0]
|
||||||
else:
|
else:
|
||||||
base_path = 'products'
|
base_path = 'products'
|
||||||
|
|
||||||
# Проверяем новый формат имени файла с расширением
|
# Проверяем старый формат имени файла с расширением
|
||||||
# Поддерживаем jpg, webp, png расширения
|
# Поддерживаем jpg, webp, png расширения
|
||||||
if filename.endswith(('.jpg', '.webp', '.png')):
|
if filename.endswith(('.jpg', '.webp', '.png')):
|
||||||
# Определяем расширение файла
|
# Определяем расширение файла
|
||||||
@@ -97,7 +149,7 @@ class ImageService:
|
|||||||
|
|
||||||
if len(parts_of_name) == 2:
|
if len(parts_of_name) == 2:
|
||||||
base_filename, file_size_key = parts_of_name
|
base_filename, file_size_key = parts_of_name
|
||||||
# Это новый формат с явным указанием размера в имени
|
# Это старый формат с явным указанием размера в имени
|
||||||
|
|
||||||
# Получаем расширение для целевого размера
|
# Получаем расширение для целевого размера
|
||||||
target_ext = ImageService._get_file_extension(size)
|
target_ext = ImageService._get_file_extension(size)
|
||||||
@@ -123,7 +175,6 @@ class ImageService:
|
|||||||
|
|
||||||
# Если ничего не найдено, возвращаем путь с новым расширением (браузер покажет ошибку)
|
# Если ничего не найдено, возвращаем путь с новым расширением (браузер покажет ошибку)
|
||||||
return f"{settings.MEDIA_URL}{new_path_primary}"
|
return f"{settings.MEDIA_URL}{new_path_primary}"
|
||||||
# Иначе оставляем как есть
|
|
||||||
|
|
||||||
# Строим новый путь (для старых файлов без новой структуры)
|
# Строим новый путь (для старых файлов без новой структуры)
|
||||||
size_folders = ImageService._get_size_folders()
|
size_folders = ImageService._get_size_folders()
|
||||||
|
|||||||
Reference in New Issue
Block a user