fix(): Ajuste de envio de multoplos registros, aplicando o recebimento de arquivo base64 e salvando em disco
This commit is contained in:
parent
a5b0fda716
commit
b4c810901d
5 changed files with 106 additions and 57 deletions
|
|
@ -5,6 +5,7 @@
|
|||
"user": "${DB_USER}",
|
||||
"password": "${DB_PASSWORD}",
|
||||
"aeskey": "${AES_KEY}",
|
||||
"storage": "${STORAGE}",
|
||||
"charset": "utf8mb4",
|
||||
"pool": {
|
||||
"pre_ping": true,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue