fix(): Ajuste de envio de multoplos registros, aplicando o recebimento de arquivo base64 e salvando em disco

This commit is contained in:
Kenio 2025-11-05 11:28:58 -03:00
parent a5b0fda716
commit b4c810901d
5 changed files with 106 additions and 57 deletions

View file

@ -5,6 +5,7 @@
"user": "${DB_USER}",
"password": "${DB_PASSWORD}",
"aeskey": "${AES_KEY}",
"storage": "${STORAGE}",
"charset": "utf8mb4",
"pool": {
"pre_ping": true,

View file

@ -11,11 +11,54 @@ from packages.v1.administrativo.schemas.ato_principal_schema import (
AtoPrincipalSaveSchema,
)
# NOVAS IMPORTAÇÕES para lidar com Base64 e Arquivos
import base64
import os
from pathlib import Path
# Configuração da Chave AES
DB_SETTINGS = get_database_settings()
AES_KEY = getattr(DB_SETTINGS, "aeskey", None)
# --- CONFIGURAÇÃO DE DIRETÓRIO DE UPLOAD ---
# **IMPORTANTE:** Configure este caminho conforme sua arquitetura!
# Exemplo: pasta 'storage' no diretório raiz do projeto
# Use 'os.environ.get' para facilitar a configuração em diferentes ambientes.
UPLOAD_DIR = Path(os.environ.get("STORAGE", "./storage"))
def _save_file_from_base64(base64_content: str, file_name: str) -> str:
"""
Decodifica o Base64, salva o arquivo no disco e retorna o caminho/URL do arquivo salvo.
"""
# Garante que o diretório de uploads existe
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# Define o caminho completo do arquivo
file_path = UPLOAD_DIR / file_name
try:
# Decodificação Base64
file_bytes = base64.b64decode(base64_content)
# Escrita do arquivo em disco (modo binário 'wb')
with open(file_path, "wb") as f:
f.write(file_bytes)
# Retorna o caminho onde o arquivo foi salvo (Este será o valor salvo no campo 'url' do banco)
return str(file_path)
except base64.binascii.Error:
# Erro específico para conteúdo Base64 inválido
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"O conteúdo do arquivo '{file_name}' não é um Base64 válido.",
)
except Exception as e:
# Outros erros de I/O ou sistema
raise Exception(f"Erro inesperado ao salvar arquivo '{file_name}': {e}")
class SaveMultipleRepository:
"""
@ -41,6 +84,7 @@ class SaveMultipleRepository:
exclude_unset=True,
exclude={"ato_partes", "ato_documentos", "atos_vinculados"},
)
# ... (restante do bloco de pré-processamento/criptografia do Ato Principal é mantido)
# Define o ID de origem se for um ato vinculado
if parent_ato_principal_id is not None:
@ -89,8 +133,6 @@ class SaveMultipleRepository:
for campo in parte_campos_criptografar:
valor = parte_data.get(campo)
# A validação/conversão para string já foi feita pelo Pydantic,
# mas mantemos a checagem de tipo e strip para segurança antes da criptografia
if isinstance(valor, str) and valor.strip():
parte_data[campo] = func.aes_encrypt(valor, AES_KEY)
else:
@ -99,10 +141,31 @@ class SaveMultipleRepository:
new_parte = AtoParte(**parte_data, ato_principal_id=new_ato_id)
db.add(new_parte)
# 4. Salva os Filhos Diretos: Ato Documentos
# 4. Salva os Filhos Diretos: Ato Documentos (BLOCO MODIFICADO)
for doc in ato_schema.ato_documentos:
doc_data = doc.model_dump(exclude_unset=True)
# Pydantic v2 model_dump usa o argumento 'exclude' para excluir campos por nome
# Usamos 'exclude_unset' para evitar campos opcionais que não foram enviados
doc_data = doc.model_dump(
exclude_unset=True,
exclude={"arquivo_base64"}, # <<< Exclui o Base64 ANTES de ir pro banco
)
# --- NOVO FLUXO DE ARQUIVOS (Base64 -> Disco) ---
# O campo 'arquivo_base64' foi excluído do doc_data acima.
# Agora verificamos se ele existe no objeto original (doc)
base64_content = getattr(doc, "arquivo_base64", None)
file_url_path = None
if base64_content and doc.nome_documento:
# 1. Decodifica e salva o Base64 em disco
file_name = doc.nome_documento
file_url_path = _save_file_from_base64(base64_content, file_name)
# 2. Atribui o caminho do arquivo salvo (ou None, se não foi enviado) ao campo 'url' do banco
doc_data["url"] = file_url_path
# 3. Criptografia dos Metadados do Documento (url, nome, tipo)
# O campo 'url' agora contém o caminho do disco ou None.
doc_campos_criptografar = ["url", "nome_documento", "tipo_documento"]
for campo in doc_campos_criptografar:
@ -110,8 +173,11 @@ class SaveMultipleRepository:
if isinstance(valor, str) and valor.strip():
doc_data[campo] = func.aes_encrypt(valor, AES_KEY)
else:
# Garante que campos vazios ou None sejam inseridos como NULL
doc_data[campo] = None
# 4. Criação e Persistência no Banco
# Note: doc_data NÃO tem mais 'arquivo_base64'. Ele tem 'url' (path do disco).
new_documento = AtoDocumento(**doc_data, ato_principal_id=new_ato_id)
db.add(new_documento)

View file

@ -3,11 +3,12 @@ from fastapi import HTTPException, status
from typing import Optional
from datetime import datetime
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
from actions.validations.text import Text
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
# É importante que esta função seja mantida/implementada no seu ambiente
# from actions.validations.text import Text # Descomentar se for usar
# Funções para validar URL (ajustar importação conforme seu projeto)
# from actions.validations.url import URL # Descomentar se for usar
# ----------------------------------------------------
@ -17,7 +18,7 @@ from datetime import datetime
class AtoDocumentoSchema(BaseModel):
ato_documento_id: Optional[int] = None
ato_principal_id: Optional[int] = None # bigint NOT NULL
url: Optional[str] = None # text NOT NULL
arquivo_base64: Optional[str] = None
nome_documento: Optional[str] = None # varchar(255) NOT NULL
tipo_documento: Optional[str] = None # varchar(50) NOT NULL
created_at: Optional[datetime] = None
@ -53,27 +54,11 @@ class AtoDocumentoIdSchema(BaseModel):
# ----------------------------------------------------
class AtoDocumentoSaveSchema(BaseModel):
# Campos obrigatórios
ato_principal_id: Optional[int] = None # <<< tornar opcional
url: str
ato_principal_id: Optional[int] = None
arquivo_base64: Optional[str] = None
nome_documento: constr(max_length=255)
tipo_documento: constr(max_length=50)
# Nota: created_at e updated_at são tratados pelo Model/Banco
# Validação e Sanitização de URL (chk_url_https)
@field_validator("url")
def validate_url(cls, v: str):
v = v.strip()
if not v.startswith("https://"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[
{"input": "url", "message": "A URL do documento deve ser HTTPS."}
],
)
# Adicionar aqui a validação de URL (ex: URL.is_valid_url(v)) se disponível
return v
# Validação e Sanitização de Tipo Documento (chk_tipo_documento_not_empty)
@field_validator("tipo_documento")
def validate_tipo_documento(cls, v: str):
@ -88,8 +73,9 @@ class AtoDocumentoSaveSchema(BaseModel):
}
],
)
# Adicionar aqui a sanitização de texto (ex: Text.sanitize(v)) se disponível
return v
# Sanitiza o campo
return Text.sanitize_input(v)
# Validação e Sanitização de Nome Documento
@field_validator("nome_documento")
@ -105,7 +91,9 @@ class AtoDocumentoSaveSchema(BaseModel):
}
],
)
return v
# Sanitiza o campo
return Text.sanitize_input(v)
# ----------------------------------------------------
@ -114,25 +102,10 @@ class AtoDocumentoSaveSchema(BaseModel):
class AtoDocumentoUpdateSchema(BaseModel):
# Todos os campos são opcionais no UPDATE
ato_principal_id: Optional[int] = None
url: Optional[str] = None
arquivo_base64: Optional[str] = None
nome_documento: Optional[constr(max_length=255)] = None
tipo_documento: Optional[constr(max_length=50)] = None
# Validação de URL
@field_validator("url")
def validate_url(cls, v: Optional[str]):
if v is None:
return v
v = v.strip()
if v and not v.startswith("https://"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[
{"input": "url", "message": "A URL do documento deve ser HTTPS."}
],
)
return v
# Validação de Tipo Documento
@field_validator("tipo_documento")
def validate_tipo_documento(cls, v: Optional[str]):
@ -149,4 +122,6 @@ class AtoDocumentoUpdateSchema(BaseModel):
}
],
)
return v
# Sanitiza o campo
return Text.sanitize_input(v)

View file

@ -4,6 +4,9 @@ from typing import Optional
from datetime import datetime
import re
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
from actions.validations.text import Text
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
# É importante que esta função seja mantida/implementada no seu ambiente
# from actions.validations.text import Text # Descomentar se for usar
@ -81,8 +84,8 @@ def validate_nome_not_empty(cls, v: str):
}
],
)
# Adicionar aqui a sanitização de texto (ex: Text.sanitize(v)) se disponível
return v
# Sanitiza o campo
return Text.sanitize_input(v)
# ----------------------------------------------------
@ -102,12 +105,12 @@ class AtoParteSaveSchema(BaseModel):
# Validação de Nome
@field_validator("nome")
def validate_nome(cls, v: str):
return validate_nome_not_empty(cls, v)
return validate_nome_not_empty(cls, Text.sanitize_input(v))
# Validação de CPF/CNPJ
@field_validator("cpf_cnpj")
def validate_cpf_cnpj_field(cls, v: str):
return validate_cpf_cnpj(cls, v)
return validate_cpf_cnpj(cls, Text.sanitize_input(v))
# ----------------------------------------------------
@ -125,14 +128,14 @@ class AtoParteUpdateSchema(BaseModel):
def validate_nome_update(cls, v: Optional[str]):
if v is None:
return v
return validate_nome_not_empty(cls, v)
return validate_nome_not_empty(cls, Text.sanitize_input(v))
# Validação de CPF/CNPJ
@field_validator("cpf_cnpj")
def validate_cpf_cnpj_update(cls, v: Optional[str]):
if v is None:
return v
return validate_cpf_cnpj(cls, v)
return Text.sanitize_input(v)
return validate_cpf_cnpj(cls, Text.sanitize_input(v))
# Nota: Telefone não precisa de validação complexa além do constr(max_length)
# se o objetivo for apenas armazenar a string fornecida.

View file

@ -5,6 +5,9 @@ from typing import Optional
from datetime import datetime
from decimal import Decimal # Importar Decimal para campos monetários
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
from actions.validations.text import Text
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
# É importante que esta função seja mantida/implementada no seu ambiente
# from actions.validations.text import Text # Descomentar se for usar
@ -124,7 +127,7 @@ class AtoPrincipalCodigoAtoSchema(BaseModel):
],
)
return v
return Text.sanitize_input(v)
# ----------------------------------------------------
@ -165,6 +168,7 @@ class AtoPrincipalSaveSchema(BaseModel):
"codigo_ato",
"nome_civil_ato",
"nome_serventuario_praticou_ato",
"inteiro_teor",
)
def validate_required_strings(cls, v: str):
v = v.strip()
@ -183,7 +187,7 @@ class AtoPrincipalSaveSchema(BaseModel):
],
)
# Adicionar aqui a sanitização de texto (ex: Text.sanitize(v)) se disponível
return v
return Text.sanitize_input(v)
# Validação dos campos monetários (baseado na CHECK CONSTRAINT da DDL)
@field_validator(
@ -206,7 +210,7 @@ class AtoPrincipalSaveSchema(BaseModel):
}
],
)
return v
return Text.sanitize_input(v)
class Config:
from_attributes = True
@ -257,7 +261,7 @@ class AtoPrincipalUpdateSchema(BaseModel):
}
],
)
return v
return Text.sanitize_input(v)
# Reutiliza a validação de valores positivos
@field_validator(
@ -274,7 +278,7 @@ class AtoPrincipalUpdateSchema(BaseModel):
}
],
)
return v
return Text.sanitize_input(v)
class Config:
from_attributes = True