Refactor project structure and add new features
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Модуль validators.py содержит классы и функции для валидации входных данных.
|
||||
"""
|
||||
|
||||
import os
|
||||
import magic
|
||||
from typing import Dict, List, BinaryIO, Optional
|
||||
import logging
|
||||
|
||||
# Получаем логгер из централизованной настройки
|
||||
logger = logging.getLogger('app.validators')
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""Исключение для ошибок валидации."""
|
||||
pass
|
||||
|
||||
|
||||
class FileValidator:
|
||||
"""
|
||||
Класс для валидации файлов.
|
||||
|
||||
Проверяет тип файла, размер и другие параметры на основе конфигурации.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
"""
|
||||
Инициализация валидатора файлов.
|
||||
|
||||
Args:
|
||||
config: Словарь с параметрами конфигурации.
|
||||
"""
|
||||
self.validation_config = config.get("file_validation", {})
|
||||
self.max_file_size_mb = self.validation_config.get("max_file_size_mb", 100)
|
||||
self.allowed_extensions = self.validation_config.get("allowed_extensions",
|
||||
[".wav", ".mp3", ".ogg", ".flac", ".m4a"])
|
||||
self.allowed_mime_types = self.validation_config.get("allowed_mime_types",
|
||||
["audio/wav", "audio/mpeg", "audio/ogg",
|
||||
"audio/flac", "audio/mp4"])
|
||||
|
||||
def validate_file(self, file: BinaryIO, filename: str) -> bool:
|
||||
"""
|
||||
Валидирует файл на основе конфигурации.
|
||||
|
||||
Args:
|
||||
file: Файловый объект.
|
||||
filename: Имя файла.
|
||||
|
||||
Returns:
|
||||
True, если файл прошел валидацию.
|
||||
|
||||
Raises:
|
||||
ValidationError: Если файл не прошел валидацию.
|
||||
"""
|
||||
try:
|
||||
# Проверка размера файла
|
||||
self._validate_file_size(file)
|
||||
|
||||
# Проверка расширения файла
|
||||
self._validate_file_extension(filename)
|
||||
|
||||
# Проверка MIME-типа файла
|
||||
self._validate_file_mime_type(file)
|
||||
|
||||
return True
|
||||
except ValidationError as e:
|
||||
# Логирование общей ошибки валидации
|
||||
logger.warning(f"Ошибка валидации файла '{filename}': {str(e)}")
|
||||
raise
|
||||
|
||||
def _validate_file_size(self, file: BinaryIO) -> None:
|
||||
"""
|
||||
Валидирует размер файла.
|
||||
|
||||
Args:
|
||||
file: Файловый объект.
|
||||
|
||||
Raises:
|
||||
ValidationError: Если размер файла превышает максимально допустимый.
|
||||
"""
|
||||
# Сохранение текущей позиции
|
||||
current_position = file.tell()
|
||||
|
||||
# Переход в конец файла для определения размера
|
||||
file.seek(0, os.SEEK_END)
|
||||
file_length = file.tell()
|
||||
|
||||
# Возврат к исходной позиции
|
||||
file.seek(current_position)
|
||||
|
||||
max_size_bytes = self.max_file_size_mb * 1024 * 1024
|
||||
if file_length > max_size_bytes:
|
||||
logger.warning(f"Попытка загрузки файла размером {file_length / (1024*1024):.2f} МБ, "
|
||||
f"что превышает максимально допустимый размер {self.max_file_size_mb} МБ")
|
||||
|
||||
raise ValidationError(f"Размер файла ({file_length / (1024*1024):.2f} МБ) "
|
||||
f"превышает максимально допустимый ({self.max_file_size_mb} МБ)")
|
||||
|
||||
def _validate_file_extension(self, filename: str) -> None:
|
||||
"""
|
||||
Валидирует расширение файла.
|
||||
|
||||
Args:
|
||||
filename: Имя файла.
|
||||
|
||||
Raises:
|
||||
ValidationError: Если расширение файла не входит в список разрешенных.
|
||||
"""
|
||||
if not any(filename.lower().endswith(ext.lower()) for ext in self.allowed_extensions):
|
||||
# Логирование попытки загрузки файла с неразрешенным расширением
|
||||
file_extension = os.path.splitext(filename)[1]
|
||||
logger.warning(f"Попытка загрузки файла с неразрешенным расширением '{file_extension}'. "
|
||||
f"Имя файла: {filename}. Разрешенные расширения: {', '.join(self.allowed_extensions)}")
|
||||
|
||||
raise ValidationError(f"Расширение файла не разрешено. "
|
||||
f"Разрешенные расширения: {', '.join(self.allowed_extensions)}")
|
||||
|
||||
def _validate_file_mime_type(self, file: BinaryIO) -> None:
|
||||
"""
|
||||
Валидирует MIME-тип файла.
|
||||
|
||||
Args:
|
||||
file: Файловый объект.
|
||||
|
||||
Raises:
|
||||
ValidationError: Если MIME-тип файла не входит в список разрешенных.
|
||||
"""
|
||||
# Сохранение текущей позиции
|
||||
current_position = file.tell()
|
||||
|
||||
try:
|
||||
# Чтение первых байтов для определения MIME-типа
|
||||
header = file.read(1024)
|
||||
mime_type = magic.from_buffer(header, mime=True)
|
||||
|
||||
# Возврат к исходной позиции
|
||||
file.seek(current_position)
|
||||
|
||||
if mime_type not in self.allowed_mime_types:
|
||||
# Логирование попытки загрузки файла с неразрешенным MIME-типом
|
||||
logger.warning(f"Попытка загрузки файла с неразрешенным MIME-типом '{mime_type}'. "
|
||||
f"Разрешенные MIME-типы: {', '.join(self.allowed_mime_types)}")
|
||||
|
||||
raise ValidationError(f"MIME-тип файла ({mime_type}) не разрешен. "
|
||||
f"Разрешенные MIME-типы: {', '.join(self.allowed_mime_types)}")
|
||||
except Exception as e:
|
||||
# Возврат к исходной позиции в случае ошибки
|
||||
file.seek(current_position)
|
||||
logger.warning(f"Не удалось определить MIME-тип файла: {e}")
|
||||
# Не прерываем валидацию, если не удалось определить MIME-тип
|
||||
|
||||
@staticmethod
|
||||
def validate_local_file_path(file_path: str, allowed_directories: Optional[List[str]] = None) -> str:
|
||||
"""
|
||||
Валидирует путь к локальному файлу для предотвращения атак обхода пути.
|
||||
|
||||
Args:
|
||||
file_path: Путь к файлу.
|
||||
allowed_directories: Список разрешенных директорий.
|
||||
|
||||
Returns:
|
||||
Нормализованный и проверенный путь к файлу.
|
||||
|
||||
Raises:
|
||||
ValidationError: Если путь к файлу небезопасен.
|
||||
"""
|
||||
# Нормализация пути
|
||||
normalized_path = os.path.normpath(file_path)
|
||||
|
||||
# Если указаны разрешенные директории, проверяем, что путь находится в одной из них
|
||||
if allowed_directories:
|
||||
for allowed_dir in allowed_directories:
|
||||
full_allowed_path = os.path.abspath(allowed_dir)
|
||||
full_file_path = os.path.abspath(os.path.join(full_allowed_path, normalized_path))
|
||||
|
||||
if full_file_path.startswith(full_allowed_path):
|
||||
return full_file_path
|
||||
|
||||
logger.warning(f"Попытка доступа к файлу вне разрешенных директорий: {file_path}")
|
||||
raise ValidationError("Путь к файлу не находится в разрешенных директориях")
|
||||
|
||||
# Если разрешенные директории не указаны, просто возвращаем нормализованный путь
|
||||
return normalized_path
|
||||
Reference in New Issue
Block a user