docling-processor / utils /validators.py
Gabriel Ramos
refactor: Melhorias de robustez
6c2e797
raw
history blame
8.13 kB
"""
Validadores para arquivos de entrada.
Este módulo contém funções para validar arquivos antes do processamento,
incluindo verificação de tamanho, contagem, MIME type e sanitização de nomes.
"""
import os
import re
from pathlib import Path
from typing import BinaryIO
import config
# Tenta importar python-magic, mas oferece fallback se não disponível
try:
import magic
HAS_MAGIC = True
except ImportError:
HAS_MAGIC = False
class ValidationError(Exception):
"""Exceção levantada quando uma validação falha."""
def __init__(self, message: str, error_code: str = "VALIDATION_ERROR"):
self.message = message
self.error_code = error_code
super().__init__(self.message)
def validate_file_count(files: list) -> bool:
"""
Valida se o número de arquivos está dentro do limite permitido.
Args:
files: Lista de arquivos para validar.
Returns:
True se a contagem está válida.
Raises:
ValidationError: Se houver arquivos demais ou nenhum arquivo.
"""
if not files:
raise ValidationError(
"Nenhum arquivo enviado. Por favor, selecione ao menos um arquivo.",
error_code="NO_FILES"
)
if len(files) > config.MAX_FILES_PER_SESSION:
raise ValidationError(
f"Muitos arquivos! Máximo permitido: {config.MAX_FILES_PER_SESSION}. "
f"Você enviou: {len(files)}.",
error_code="TOO_MANY_FILES"
)
return True
def validate_file_size(file_path: str | Path) -> bool:
"""
Valida se o tamanho do arquivo está dentro do limite permitido.
Args:
file_path: Caminho para o arquivo a ser validado.
Returns:
True se o tamanho está válido.
Raises:
ValidationError: Se o arquivo for muito grande.
"""
file_path = Path(file_path)
if not file_path.exists():
raise ValidationError(
f"Arquivo não encontrado: {file_path.name}",
error_code="FILE_NOT_FOUND"
)
file_size = file_path.stat().st_size
if file_size > config.MAX_FILE_SIZE_BYTES:
size_mb = file_size / (1024 * 1024)
raise ValidationError(
f"Arquivo muito grande: {file_path.name} ({size_mb:.1f}MB). "
f"Máximo permitido: {config.MAX_FILE_SIZE_MB}MB.",
error_code="FILE_TOO_LARGE"
)
if file_size == 0:
raise ValidationError(
f"Arquivo vazio: {file_path.name}",
error_code="EMPTY_FILE"
)
return True
def _get_mime_type_magic(file_path: str | Path) -> str:
"""
Obtém o MIME type usando python-magic.
Args:
file_path: Caminho para o arquivo.
Returns:
String com o MIME type detectado.
"""
mime = magic.Magic(mime=True)
return mime.from_file(str(file_path))
def _get_mime_type_fallback(file_path: str | Path) -> str:
"""
Fallback para detecção de MIME type sem python-magic.
Usa assinaturas de arquivo (magic bytes).
Args:
file_path: Caminho para o arquivo.
Returns:
String com o MIME type detectado ou extensão-based guess.
"""
file_path = Path(file_path)
# Magic bytes para tipos comuns
signatures = {
b"%PDF": "application/pdf",
b"PK\x03\x04": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
b"\xd0\xcf\x11\xe0": "application/msword", # OLE Compound Document
}
try:
with open(file_path, "rb") as f:
header = f.read(8)
for sig, mime_type in signatures.items():
if header.startswith(sig):
return mime_type
except Exception:
pass
# Fallback para extensão
ext = file_path.suffix.lower()
ext_to_mime = {
".pdf": "application/pdf",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
}
return ext_to_mime.get(ext, "application/octet-stream")
def get_mime_type(file_path: str | Path) -> str:
"""
Obtém o MIME type de um arquivo.
Usa python-magic se disponível, caso contrário usa fallback
baseado em assinaturas de arquivo.
Args:
file_path: Caminho para o arquivo.
Returns:
String com o MIME type detectado.
"""
if HAS_MAGIC:
try:
return _get_mime_type_magic(file_path)
except Exception:
# Fallback se libmagic falhar em runtime (OSError, etc)
pass
return _get_mime_type_fallback(file_path)
def validate_mime_type(file_path: str | Path) -> bool:
"""
Valida se o MIME type do arquivo é suportado.
Args:
file_path: Caminho para o arquivo a ser validado.
Returns:
True se o MIME type é válido.
Raises:
ValidationError: Se o tipo de arquivo não for suportado.
"""
file_path = Path(file_path)
extension = file_path.suffix.lower()
# Verifica se a extensão é suportada
if extension not in config.SUPPORTED_EXTENSIONS:
raise ValidationError(
f"Extensão não suportada: {extension}. "
f"Tipos aceitos: {', '.join(config.SUPPORTED_EXTENSIONS)}",
error_code="UNSUPPORTED_EXTENSION"
)
# Obtém o MIME type real do arquivo
detected_mime = get_mime_type(file_path)
# Verifica se o MIME type corresponde à extensão
expected_mimes = config.SUPPORTED_MIME_TYPES.get(extension, [])
if detected_mime not in expected_mimes:
# DOCX pode ser detectado como ZIP em alguns casos
if extension == ".docx" and detected_mime == "application/zip":
return True
raise ValidationError(
f"Tipo de arquivo inválido: {file_path.name}. "
f"O conteúdo não corresponde à extensão {extension}. "
f"Detectado: {detected_mime}",
error_code="MIME_MISMATCH"
)
return True
def sanitize_filename(filename: str) -> str:
"""
Remove caracteres especiais/perigosos do nome de arquivo.
Args:
filename: Nome original do arquivo.
Returns:
Nome de arquivo sanitizado.
"""
if not filename:
return "arquivo_sem_nome"
# Remove caracteres proibidos
for char in config.FORBIDDEN_FILENAME_CHARS:
filename = filename.replace(char, "_")
# Remove caracteres de controle
filename = re.sub(r"[\x00-\x1f\x7f]", "", filename)
# Substitui espaços múltiplos por um único underscore
filename = re.sub(r"\s+", "_", filename)
# Remove underscores múltiplos
filename = re.sub(r"_+", "_", filename)
# Remove underscores no início e fim
filename = filename.strip("_")
# Limita o comprimento
if len(filename) > config.FILENAME_MAX_LENGTH:
# Preserva a extensão
name, ext = os.path.splitext(filename)
max_name_len = config.FILENAME_MAX_LENGTH - len(ext)
filename = name[:max_name_len] + ext
# Se ficou vazio após sanitização
if not filename or filename == "." or filename == "..":
return "arquivo_sanitizado"
return filename
def validate_files(files: list) -> list[tuple[Path, str]]:
"""
Valida uma lista de arquivos completamente.
Args:
files: Lista de arquivos (podem ser paths ou objetos de arquivo).
Returns:
Lista de tuplas (path, nome_sanitizado) para arquivos válidos.
Raises:
ValidationError: Se qualquer validação falhar.
"""
validate_file_count(files)
validated = []
for file_obj in files:
# Gradio retorna objetos com atributo 'name'
if hasattr(file_obj, "name"):
file_path = Path(file_obj.name)
else:
file_path = Path(file_obj)
# Valida tamanho
validate_file_size(file_path)
# Valida MIME type
validate_mime_type(file_path)
# Sanitiza nome
sanitized_name = sanitize_filename(file_path.name)
validated.append((file_path, sanitized_name))
return validated