From f99bbb5c6b0795147148feb629069329d9c1bf26 Mon Sep 17 00:00:00 2001 From: erjemin Date: Sun, 7 Jun 2026 20:57:46 +0300 Subject: [PATCH] =?UTF-8?q?mod:=20django-filer=20=D0=BD=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=20(04)=20=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B2=20webp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lpon_site/frontend/apps.py | 179 ++++++++------------------------ lpon_site/lpon_site/settings.py | 27 ++++- 2 files changed, 69 insertions(+), 137 deletions(-) diff --git a/lpon_site/frontend/apps.py b/lpon_site/frontend/apps.py index 0261dce..1c678f5 100644 --- a/lpon_site/frontend/apps.py +++ b/lpon_site/frontend/apps.py @@ -2,47 +2,12 @@ import os import hashlib from io import BytesIO from django.apps import AppConfig -from django.core.files.storage import FileSystemStorage from django.core.files.base import ContentFile from PIL import Image as PILImage -from lpon_site.settings import DEBUG, MEDIA_ROOT +from lpon_site.settings import DEBUG, THUMBNAIL_WEBP_QUALITY -# 0. Кастомная функция генерирования имен файлов специально для WebP -def generate_filename_webp(instance, filename): - """ - Генерирует имя файла для сохранения. Если исходный файл был конвертирован в WebP, - гарантирует что расширение файла будет .webp. - """ - _, ext = os.path.splitext(filename) - if ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]: - base_path = filename.rsplit(ext, 1)[0] - return f"{base_path}.webp" - return filename - - -# 0.1. Функция для генерирования путей с префиксом 'flr/' для основных файлов -def generate_upload_path_flr(instance, filename): - """ - Генерирует путь для сохранения файла с префиксом 'flr/'. - """ - from filer.utils.generate_filename import randomized - base_path = randomized(instance, filename) - return f'flr/{base_path}' - - -# 0.2. Функция для генерирования путей с префиксом 'flrm/' для миниатюр -def generate_upload_path_flrm(instance, filename): - """ - Генерирует путь для сохранения миниатюр с префиксом 'flrm/'. - """ - from filer.utils.generate_filename import randomized - base_path = randomized(instance, filename) - return f'flrm/{base_path}' - - -# 1. class FrontendConfig(AppConfig): name = 'frontend' verbose_name = 'Сайт lpon.ru' @@ -55,50 +20,43 @@ class FrontendConfig(AppConfig): admin.site.index_title = 'Добро пожаловать в LPON' -# 2. -class FilerWebPStorage(FileSystemStorage): - """ - Кастомное хранилище для Filer. Основная логика конвертации перенесена - в патч для MultiStorageFieldFile.save, чтобы обновлять метаданные до сохранения. - """ - def _save(self, name: str, content): - return super()._save(name, content) - - -# Добавляем кастомный конфиг для filer class CustomFilerConfig(AppConfig): name = 'filer' verbose_name = 'Медиафайлы' - # Конфигурация Django-Filer - FILER_ENABLE_PERMISSIONS = DEBUG - FILER_WHITELIST_FOR_PATH_ACCESS = ( - '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', - '.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx', '.csv', - ) - FILER_MAX_UPLOAD_SIZE = 100 * 1024 * 1024 - MIME_TYPE_WHITELIST = ( - 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', - 'application/pdf', 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'text/plain', 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'text/csv', - ) - THUMBNAIL_ALIASES = { - '': { - 'admin_thumbnail': {'size': (64, 64), 'crop': True}, - 'small': {'size': (256, 256), 'crop': True}, - 'medium': {'size': (512, 512), 'crop': True}, - 'large': {'size': (1024, 1024), 'crop': 'smart'}, - }, - } - THUMBNAIL_PRESERVE_FORMAT = False - THUMBNAIL_FORCE_FORMAT = 'WEBP' - THUMBNAIL_WEBP_QUALITY = 80 - THUMBNAIL_ENGINE = 'easy_thumbnails.engines.pil_engine.PilEngine' - THUMBNAIL_DEBUG = DEBUG - FILE_VALIDATORS = {} + @staticmethod + def generate_upload_path_flr(instance, filename): + from filer.utils.generate_filename import randomized + base_path = randomized(instance, filename) + return f'flr/{base_path}' + + @staticmethod + def generate_upload_path_flrm(instance, filename): + from filer.utils.generate_filename import randomized + base_path = randomized(instance, filename) + return f'flrm/{base_path}' + + class WebPConverter: + def convert_to_webp_if_needed(self, name: str, content): + _, original_ext = os.path.splitext(name) + if original_ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]: + try: + content.seek(0) + img = PILImage.open(BytesIO(content.read())) + if img.mode == 'CMYK': + img = img.convert('RGB') + buffer = BytesIO() + img.save(buffer, format="WEBP", quality=THUMBNAIL_WEBP_QUALITY) + buffer.seek(0) + new_name = name.rsplit(original_ext, 1)[0] + ".webp" + return ContentFile(buffer.read()), new_name, True + except Exception as e: + import sys + print(f"[WebPConverter] ERROR converting {name}: {e}", file=sys.stderr) + content.seek(0) + return content, name, False + content.seek(0) + return content, name, False def ready(self): import sys @@ -106,70 +64,25 @@ class CustomFilerConfig(AppConfig): print("[CustomFilerConfig.ready] Patching MultiStorageFieldFile.save()...", file=sys.stderr) original_save = MultiStorageFieldFile.save - webp_converter = WebPConverter() + webp_converter = self.WebPConverter() - def patched_save(self, name, content, save=True): - """ - Переопределенный save(), который преобразует изображение в WebP, обновляет - метаданные модели (MIME-тип, размер, SHA1) и затем сохраняет. - """ + def patched_save(self_instance, name, content, save=True): new_content, new_name, converted = webp_converter.convert_to_webp_if_needed(name, content) - if converted: - # Обновляем метаданные прямо в инстансе модели - self.instance.mime_type = "image/webp" - - if hasattr(self.instance, 'original_filename') and self.instance.original_filename: - original_filename, _ = os.path.splitext(self.instance.original_filename) - self.instance.original_filename = f"{original_filename}.webp" - - # Вручную пересчитываем размер и SHA1-хэш для нового файла .webp + self_instance.instance.mime_type = "image/webp" + if hasattr(self_instance.instance, 'original_filename') and self_instance.instance.original_filename: + original_filename, _ = os.path.splitext(self_instance.instance.original_filename) + self_instance.instance.original_filename = f"{original_filename}.webp" new_content.seek(0) file_bytes = new_content.read() new_content.seek(0) - - self.instance._file_size = len(file_bytes) - self.instance.sha1 = hashlib.sha1(file_bytes).hexdigest() - - # Вызываем оригинальный метод save() с (возможно) новым контентом и именем - return original_save(self, new_name, new_content, save) + self_instance.instance._file_size = len(file_bytes) + self_instance.instance.sha1 = hashlib.sha1(file_bytes).hexdigest() + return original_save(self_instance, new_name, new_content, save) MultiStorageFieldFile.save = patched_save print("[CustomFilerConfig.ready] MultiStorageFieldFile.save() patched successfully", file=sys.stderr) - -class WebPConverter: - """ - Класс, инкапсулирующий логику преобразования изображений в WebP. - """ - def convert_to_webp_if_needed(self, name: str, content): - """ - Проверяет формат файла и, если это необходимо, преобразует его в WebP. - Возвращает кортеж (новое_содержимое, новое_имя, флаг_конвертации). - """ - _, original_ext = os.path.splitext(name) - - if original_ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]: - try: - content.seek(0) - img = PILImage.open(BytesIO(content.read())) - - if img.mode == 'CMYK': - img = img.convert('RGB') - - buffer = BytesIO() - img.save(buffer, format="WEBP", quality=80) - buffer.seek(0) - - new_name = name.rsplit(original_ext, 1)[0] + ".webp" - - return ContentFile(buffer.read()), new_name, True - - except Exception as e: - import sys - print(f"[WebPConverter] ERROR converting {name}: {e}", file=sys.stderr) - content.seek(0) - return content, name, False - - content.seek(0) - return content, name, False +# Создаем псевдонимы на уровне модуля для функций, чтобы их мог найти Django +generate_upload_path_flr = CustomFilerConfig.generate_upload_path_flr +generate_upload_path_flrm = CustomFilerConfig.generate_upload_path_flrm diff --git a/lpon_site/lpon_site/settings.py b/lpon_site/lpon_site/settings.py index 51c5564..b3c58d5 100644 --- a/lpon_site/lpon_site/settings.py +++ b/lpon_site/lpon_site/settings.py @@ -178,10 +178,9 @@ STATIC_ROOT = PUBLIC_DIR.joinpath('staticfiles') FILER_STORAGES = { 'public': { 'main': { - # Используем кастомное хранилище FilerWebPStorage для преобразования в WebP - 'ENGINE': 'frontend.apps.FilerWebPStorage', + # Используем стандартное хранилище Django. Логика конвертации в apps.py + 'ENGINE': 'django.core.files.storage.FileSystemStorage', 'OPTIONS': { - # location должна быть MEDIA_ROOT для корректной генерации URL! 'location': str(MEDIA_ROOT), }, # UPLOAD_TO функция добавляет 'flr/' префикс для более компактных путей в шаблонах @@ -209,6 +208,26 @@ FILER_STORAGES = { THUMBNAIL_PRESERVE_FORMAT = False # Не сохранять оригинальный формат для миниатюр THUMBNAIL_FORMAT = 'WEBP' # Конвертировать все миниатюры в WebP THUMBNAIL_QUALITY = 80 # Качество WebP (достаточно 75-85 для миниатюр) +# Кастомная настройка для встроенного конвертора загружаемых файлов в WebP (см. apps.py) +THUMBNAIL_WEBP_QUALITY = 80 # Качество для WebP + +FILER_ENABLE_PERMISSIONS = DEBUG +FILER_WHITELIST_FOR_PATH_ACCESS = ( + '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', + '.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx', '.csv', +) +FILER_MAX_UPLOAD_SIZE = 100 * 1024 * 1024 +MIME_TYPE_WHITELIST = ( + 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', + 'application/pdf', 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'text/plain', 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv', +) +THUMBNAIL_ENGINE = 'easy_thumbnails.engines.pil_engine.PilEngine' +THUMBNAIL_DEBUG = DEBUG +FILE_VALIDATORS = {} # Размеры миниатюр для разных использований THUMBNAIL_ALIASES = { @@ -220,4 +239,4 @@ THUMBNAIL_ALIASES = { 'medium': {'size': (512, 512), 'crop': True}, 'large': {'size': (1024, 1024), 'crop': 'smart'}, }, -} +} \ No newline at end of file