From fa559f2517366ccb4e6e038064e70f74a6ef81a3 Mon Sep 17 00:00:00 2001 From: erjemin Date: Mon, 8 Jun 2026 01:37:26 +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(08)=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lpon_site/frontend/apps.py | 110 ++++++++++++++++++++++++++------ lpon_site/lpon_site/settings.py | 26 ++++++-- 2 files changed, 112 insertions(+), 24 deletions(-) diff --git a/lpon_site/frontend/apps.py b/lpon_site/frontend/apps.py index 214f178..acfec9f 100644 --- a/lpon_site/frontend/apps.py +++ b/lpon_site/frontend/apps.py @@ -4,14 +4,25 @@ import logging from io import BytesIO from django.apps import AppConfig from django.core.files.base import ContentFile +from django.core.files.storage import FileSystemStorage from PIL import Image as PILImage -from lpon_site.settings import DEBUG, THUMBNAIL_WEBP_QUALITY +from lpon_site.settings import ( + THUMBNAIL_WEBP_QUALITY, + FILER_ENABLE_PERMISSIONS, + FILER_UPLOADER_MAX_FILE_SIZE, + FILER_WHITELIST_FOR_PATH_ACCESS, + MIME_TYPE_WHITELIST, + FILE_VALIDATORS, +) # Получаем логгер для текущего модуля logger = logging.getLogger(__name__) +# ============================================================================== +# Конфигурация приложения для фронтенда lpon +# ============================================================================== class FrontendConfig(AppConfig): name = 'frontend' verbose_name = 'Сайт lpon.ru' @@ -23,32 +34,35 @@ class FrontendConfig(AppConfig): admin.site.site_title = 'LPON Administrator' admin.site.index_title = 'Добро пожаловать в LPON' - +# ============================================================================== +# Кастомная конфигурация для django-filer, которая включает в себя: +# - Кастомное хранилище для миниатюр (ThumbnailFileSystemStorage) +# - Патчинг MultiStorageFieldFile.save() для автоматической конвертации в WebP +# +# Все остальные параметры конфигурации (MIME_TYPE_WHITELIST, FILER_WHITELIST_FOR_PATH_ACCESS и т.д.) +# определены в settings.py и импортируются оттуда. +# ============================================================================== class CustomFilerConfig(AppConfig): name = 'filer' verbose_name = 'Медиафайлы' # ======================================================================== - # Конфигурация Django-Filer, которая читается во время выполнения + # Атрибуты конфигурации Django-Filer (импортированы из settings.py) + # Единое место правды - settings.py # ======================================================================== - FILER_ENABLE_PERMISSIONS = DEBUG - FILER_MAX_UPLOAD_SIZE = 100 * 1024 * 1024 - FILER_WHITELIST_FOR_PATH_ACCESS = ( - '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', - '.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx', '.csv', - ) - 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', - ) - FILE_VALIDATORS = {} + FILER_ENABLE_PERMISSIONS = FILER_ENABLE_PERMISSIONS + FILER_UPLOADER_MAX_FILE_SIZE = FILER_UPLOADER_MAX_FILE_SIZE + FILER_WHITELIST_FOR_PATH_ACCESS = FILER_WHITELIST_FOR_PATH_ACCESS + MIME_TYPE_WHITELIST = MIME_TYPE_WHITELIST + FILE_VALIDATORS = FILE_VALIDATORS + @staticmethod def _convert_to_webp_if_needed(name: str, content): + """ + Преобразует загруженное изображение в WebP формат. + Поддерживает JPEG, PNG, BMP и TIFF. + """ _, original_ext = os.path.splitext(name) if original_ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]: try: @@ -70,25 +84,83 @@ class CustomFilerConfig(AppConfig): return content, name, False def ready(self): + """ + Готовимся к работе: патчим MultiStorageFieldFile.save() для WebP конвертации. + """ from filer.fields.multistorage_file import MultiStorageFieldFile logger.info("Patching MultiStorageFieldFile.save() for WebP conversion...") original_save = MultiStorageFieldFile.save def patched_save(self_instance, name, content, save=True): + # Преобразуем загруженный файл в WebP если требуется new_content, new_name, converted = CustomFilerConfig._convert_to_webp_if_needed(name, content) if converted: + # Обновляем свойства файла для 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" + # Сохраняем размер и контрольную сумму WebP файла new_content.seek(0) file_bytes = new_content.read() new_content.seek(0) 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 logger.info("MultiStorageFieldFile.save() patched successfully.") + + +# ============================================================================== +# Кастомное хранилище для миниатюр (часть конфигурации CustomFilerConfig) +# Удаляет префикс filer_public_thumbnails/ при сохранении и генерации URLs +# ============================================================================== +class ThumbnailFileSystemStorage(FileSystemStorage): + """ + Кастомное хранилище для миниатюр. + + Удаляет префикс 'filer_public_thumbnails/' из пути сохранения и из URLs, + чтобы миниатюры хранились и отображались без этого каталога. + + Используется как часть конфигурации CustomFilerConfig. + """ + + def get_available_name(self, name, max_length=None): + """ + Переопределяем получение доступного имени файла. + Удаляем префикс 'filer_public_thumbnails/' если он присутствует. + """ + # Удаляем префикс filer_public_thumbnails/ если он есть + if name.startswith('filer_public_thumbnails/'): + name = name.replace('filer_public_thumbnails/', '', 1) + logger.debug(f"[ThumbnailFileSystemStorage.get_available_name] Удалён префикс filer_public_thumbnails/, новый путь: {name}") + + return super().get_available_name(name, max_length) + + def _save(self, name, content): + """ + Переопределяем сохранение файла. + Удаляем префикс 'filer_public_thumbnails/' перед сохранением на диск. + """ + # Удаляем префикс filer_public_thumbnails/ если он есть + if name.startswith('filer_public_thumbnails/'): + name = name.replace('filer_public_thumbnails/', '', 1) + logger.debug(f"[ThumbnailFileSystemStorage._save] Удалён префикс filer_public_thumbnails/, новый путь: {name}") + + return super()._save(name, content) + + def url(self, name): + """ + Переопределяем генерацию URL. + Удаляем префикс 'filer_public_thumbnails/' если он присутствует в URL. + Это необходимо потому, что easy_thumbnails может пытаться добавить этот префикс. + """ + # Если имя содержит префикс, удаляем его перед генерацией URL + if name.startswith('filer_public_thumbnails/'): + name = name.replace('filer_public_thumbnails/', '', 1) + logger.debug(f"[ThumbnailFileSystemStorage.url] Удалён префикс filer_public_thumbnails/ из URL, новый путь: {name}") + + return super().url(name) \ No newline at end of file diff --git a/lpon_site/lpon_site/settings.py b/lpon_site/lpon_site/settings.py index 14009ec..fa77876 100644 --- a/lpon_site/lpon_site/settings.py +++ b/lpon_site/lpon_site/settings.py @@ -175,15 +175,16 @@ FILER_STORAGES = { 'UPLOAD_TO_PREFIX': '', }, 'thumbnails': { - 'ENGINE': 'filer.storage.PublicFileSystemStorage', + # Используем кастомное хранилище, которое удаляет префикс filer_public_thumbnails/ + # Смотримайте класс ThumbnailFileSystemStorage в frontend/apps.py (часть CustomFilerConfig) + 'ENGINE': 'frontend.apps.ThumbnailFileSystemStorage', 'OPTIONS': { 'location': MEDIA_ROOT / 'flrm', 'base_url': MEDIA_URL + 'flrm/', - # 'location': os.path.join(MEDIA_ROOT, 'flrm'), - # 'base_url': os.path.join(MEDIA_URL, 'flrm/'), }, - # 'UPLOAD_TO': 'filer.utils.generate_filename.randomized', - # 'UPLOAD_TO_PREFIX': '_', + # Используем ту же функцию генерации пути, что и для основных файлов. + 'UPLOAD_TO': 'filer.utils.generate_filename.randomized', + 'UPLOAD_TO_PREFIX': '', }, }, } @@ -206,6 +207,21 @@ FILER_UPLOADER_MAX_FILES = 3 FILER_UPLOADER_MAX_FILE_SIZE = 100 * 1024 * 1024 FILER_MAX_IMAGE_PIXELS = 4096 * 4096 +FILER_ENABLE_PERMISSIONS = DEBUG +FILER_WHITELIST_FOR_PATH_ACCESS = ( + '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', + '.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx', '.csv', +) +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', +) +FILE_VALIDATORS = {} + # Настройки для "умной" обрезки изобращений THUMBNAIL_PROCESSORS = ( 'easy_thumbnails.processors.colorspace',