Spaces:
Runtime error
Runtime error
File size: 8,004 Bytes
780413d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
"""
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:
return _get_mime_type_magic(file_path)
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
|