mod: django-filer настройка (08) размещение файлов

This commit is contained in:
2026-06-08 01:37:26 +03:00
parent 7e63fae613
commit fa559f2517
2 changed files with 112 additions and 24 deletions

View File

@@ -4,14 +4,25 @@ import logging
from io import BytesIO from io import BytesIO
from django.apps import AppConfig from django.apps import AppConfig
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage
from PIL import Image as PILImage 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__) logger = logging.getLogger(__name__)
# ==============================================================================
# Конфигурация приложения для фронтенда lpon
# ==============================================================================
class FrontendConfig(AppConfig): class FrontendConfig(AppConfig):
name = 'frontend' name = 'frontend'
verbose_name = 'Сайт lpon.ru' verbose_name = 'Сайт lpon.ru'
@@ -23,32 +34,35 @@ class FrontendConfig(AppConfig):
admin.site.site_title = 'LPON Administrator' admin.site.site_title = 'LPON Administrator'
admin.site.index_title = 'Добро пожаловать в LPON' admin.site.index_title = 'Добро пожаловать в LPON'
# ==============================================================================
# Кастомная конфигурация для django-filer, которая включает в себя:
# - Кастомное хранилище для миниатюр (ThumbnailFileSystemStorage)
# - Патчинг MultiStorageFieldFile.save() для автоматической конвертации в WebP
#
# Все остальные параметры конфигурации (MIME_TYPE_WHITELIST, FILER_WHITELIST_FOR_PATH_ACCESS и т.д.)
# определены в settings.py и импортируются оттуда.
# ==============================================================================
class CustomFilerConfig(AppConfig): class CustomFilerConfig(AppConfig):
name = 'filer' name = 'filer'
verbose_name = 'Медиафайлы' verbose_name = 'Медиафайлы'
# ======================================================================== # ========================================================================
# Конфигурация Django-Filer, которая читается во время выполнения # Атрибуты конфигурации Django-Filer (импортированы из settings.py)
# Единое место правды - settings.py
# ======================================================================== # ========================================================================
FILER_ENABLE_PERMISSIONS = DEBUG FILER_ENABLE_PERMISSIONS = FILER_ENABLE_PERMISSIONS
FILER_MAX_UPLOAD_SIZE = 100 * 1024 * 1024 FILER_UPLOADER_MAX_FILE_SIZE = FILER_UPLOADER_MAX_FILE_SIZE
FILER_WHITELIST_FOR_PATH_ACCESS = ( FILER_WHITELIST_FOR_PATH_ACCESS = FILER_WHITELIST_FOR_PATH_ACCESS
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', MIME_TYPE_WHITELIST = MIME_TYPE_WHITELIST
'.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx', '.csv', FILE_VALIDATORS = FILE_VALIDATORS
)
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 = {}
@staticmethod @staticmethod
def _convert_to_webp_if_needed(name: str, content): def _convert_to_webp_if_needed(name: str, content):
"""
Преобразует загруженное изображение в WebP формат.
Поддерживает JPEG, PNG, BMP и TIFF.
"""
_, original_ext = os.path.splitext(name) _, original_ext = os.path.splitext(name)
if original_ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]: if original_ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]:
try: try:
@@ -70,25 +84,83 @@ class CustomFilerConfig(AppConfig):
return content, name, False return content, name, False
def ready(self): def ready(self):
"""
Готовимся к работе: патчим MultiStorageFieldFile.save() для WebP конвертации.
"""
from filer.fields.multistorage_file import MultiStorageFieldFile from filer.fields.multistorage_file import MultiStorageFieldFile
logger.info("Patching MultiStorageFieldFile.save() for WebP conversion...") logger.info("Patching MultiStorageFieldFile.save() for WebP conversion...")
original_save = MultiStorageFieldFile.save original_save = MultiStorageFieldFile.save
def patched_save(self_instance, name, content, save=True): def patched_save(self_instance, name, content, save=True):
# Преобразуем загруженный файл в WebP если требуется
new_content, new_name, converted = CustomFilerConfig._convert_to_webp_if_needed(name, content) new_content, new_name, converted = CustomFilerConfig._convert_to_webp_if_needed(name, content)
if converted: if converted:
# Обновляем свойства файла для WebP версии
self_instance.instance.mime_type = "image/webp" self_instance.instance.mime_type = "image/webp"
if hasattr(self_instance.instance, 'original_filename') and self_instance.instance.original_filename: if hasattr(self_instance.instance, 'original_filename') and self_instance.instance.original_filename:
original_filename, _ = os.path.splitext(self_instance.instance.original_filename) original_filename, _ = os.path.splitext(self_instance.instance.original_filename)
self_instance.instance.original_filename = f"{original_filename}.webp" self_instance.instance.original_filename = f"{original_filename}.webp"
# Сохраняем размер и контрольную сумму WebP файла
new_content.seek(0) new_content.seek(0)
file_bytes = new_content.read() file_bytes = new_content.read()
new_content.seek(0) new_content.seek(0)
self_instance.instance._file_size = len(file_bytes) self_instance.instance._file_size = len(file_bytes)
self_instance.instance.sha1 = hashlib.sha1(file_bytes).hexdigest() self_instance.instance.sha1 = hashlib.sha1(file_bytes).hexdigest()
return original_save(self_instance, new_name, new_content, save) return original_save(self_instance, new_name, new_content, save)
MultiStorageFieldFile.save = patched_save MultiStorageFieldFile.save = patched_save
logger.info("MultiStorageFieldFile.save() patched successfully.") 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)

View File

@@ -175,15 +175,16 @@ FILER_STORAGES = {
'UPLOAD_TO_PREFIX': '', 'UPLOAD_TO_PREFIX': '',
}, },
'thumbnails': { 'thumbnails': {
'ENGINE': 'filer.storage.PublicFileSystemStorage', # Используем кастомное хранилище, которое удаляет префикс filer_public_thumbnails/
# Смотримайте класс ThumbnailFileSystemStorage в frontend/apps.py (часть CustomFilerConfig)
'ENGINE': 'frontend.apps.ThumbnailFileSystemStorage',
'OPTIONS': { 'OPTIONS': {
'location': MEDIA_ROOT / 'flrm', 'location': MEDIA_ROOT / 'flrm',
'base_url': MEDIA_URL + '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_UPLOADER_MAX_FILE_SIZE = 100 * 1024 * 1024
FILER_MAX_IMAGE_PIXELS = 4096 * 4096 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 = ( THUMBNAIL_PROCESSORS = (
'easy_thumbnails.processors.colorspace', 'easy_thumbnails.processors.colorspace',