mod: django-filer настройка (08) размещение файлов
This commit is contained in:
@@ -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,18 +84,24 @@ 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)
|
||||||
@@ -92,3 +112,55 @@ class CustomFilerConfig(AppConfig):
|
|||||||
|
|
||||||
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)
|
||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user