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 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)

View File

@@ -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',