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}",
|
"user": "${DB_USER}",
|
||||||
"password": "${DB_PASSWORD}",
|
"password": "${DB_PASSWORD}",
|
||||||
"aeskey": "${AES_KEY}",
|
"aeskey": "${AES_KEY}",
|
||||||
|
"storage": "${STORAGE}",
|
||||||
"charset": "utf8mb4",
|
"charset": "utf8mb4",
|
||||||
"pool": {
|
"pool": {
|
||||||
"pre_ping": true,
|
"pre_ping": true,
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,54 @@ from packages.v1.administrativo.schemas.ato_principal_schema import (
|
||||||
AtoPrincipalSaveSchema,
|
AtoPrincipalSaveSchema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# NOVAS IMPORTAÇÕES para lidar com Base64 e Arquivos
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
# Configuração da Chave AES
|
# Configuração da Chave AES
|
||||||
DB_SETTINGS = get_database_settings()
|
DB_SETTINGS = get_database_settings()
|
||||||
AES_KEY = getattr(DB_SETTINGS, "aeskey", None)
|
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:
|
class SaveMultipleRepository:
|
||||||
"""
|
"""
|
||||||
|
|
@ -41,6 +84,7 @@ class SaveMultipleRepository:
|
||||||
exclude_unset=True,
|
exclude_unset=True,
|
||||||
exclude={"ato_partes", "ato_documentos", "atos_vinculados"},
|
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
|
# Define o ID de origem se for um ato vinculado
|
||||||
if parent_ato_principal_id is not None:
|
if parent_ato_principal_id is not None:
|
||||||
|
|
@ -89,8 +133,6 @@ class SaveMultipleRepository:
|
||||||
|
|
||||||
for campo in parte_campos_criptografar:
|
for campo in parte_campos_criptografar:
|
||||||
valor = parte_data.get(campo)
|
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():
|
if isinstance(valor, str) and valor.strip():
|
||||||
parte_data[campo] = func.aes_encrypt(valor, AES_KEY)
|
parte_data[campo] = func.aes_encrypt(valor, AES_KEY)
|
||||||
else:
|
else:
|
||||||
|
|
@ -99,10 +141,31 @@ class SaveMultipleRepository:
|
||||||
new_parte = AtoParte(**parte_data, ato_principal_id=new_ato_id)
|
new_parte = AtoParte(**parte_data, ato_principal_id=new_ato_id)
|
||||||
db.add(new_parte)
|
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:
|
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"]
|
doc_campos_criptografar = ["url", "nome_documento", "tipo_documento"]
|
||||||
|
|
||||||
for campo in doc_campos_criptografar:
|
for campo in doc_campos_criptografar:
|
||||||
|
|
@ -110,8 +173,11 @@ class SaveMultipleRepository:
|
||||||
if isinstance(valor, str) and valor.strip():
|
if isinstance(valor, str) and valor.strip():
|
||||||
doc_data[campo] = func.aes_encrypt(valor, AES_KEY)
|
doc_data[campo] = func.aes_encrypt(valor, AES_KEY)
|
||||||
else:
|
else:
|
||||||
|
# Garante que campos vazios ou None sejam inseridos como NULL
|
||||||
doc_data[campo] = None
|
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)
|
new_documento = AtoDocumento(**doc_data, ato_principal_id=new_ato_id)
|
||||||
db.add(new_documento)
|
db.add(new_documento)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@ from fastapi import HTTPException, status
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime
|
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.)
|
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
|
||||||
# É importante que esta função seja mantida/implementada no seu ambiente
|
# É importante que esta função seja mantida/implementada no seu ambiente
|
||||||
# from actions.validations.text import Text # Descomentar se for usar
|
# 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):
|
class AtoDocumentoSchema(BaseModel):
|
||||||
ato_documento_id: Optional[int] = None
|
ato_documento_id: Optional[int] = None
|
||||||
ato_principal_id: Optional[int] = None # bigint NOT NULL
|
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
|
nome_documento: Optional[str] = None # varchar(255) NOT NULL
|
||||||
tipo_documento: Optional[str] = None # varchar(50) NOT NULL
|
tipo_documento: Optional[str] = None # varchar(50) NOT NULL
|
||||||
created_at: Optional[datetime] = None
|
created_at: Optional[datetime] = None
|
||||||
|
|
@ -53,27 +54,11 @@ class AtoDocumentoIdSchema(BaseModel):
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
class AtoDocumentoSaveSchema(BaseModel):
|
class AtoDocumentoSaveSchema(BaseModel):
|
||||||
# Campos obrigatórios
|
# Campos obrigatórios
|
||||||
ato_principal_id: Optional[int] = None # <<< tornar opcional
|
ato_principal_id: Optional[int] = None
|
||||||
url: str
|
arquivo_base64: Optional[str] = None
|
||||||
nome_documento: constr(max_length=255)
|
nome_documento: constr(max_length=255)
|
||||||
tipo_documento: constr(max_length=50)
|
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)
|
# Validação e Sanitização de Tipo Documento (chk_tipo_documento_not_empty)
|
||||||
@field_validator("tipo_documento")
|
@field_validator("tipo_documento")
|
||||||
def validate_tipo_documento(cls, v: str):
|
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
|
# Validação e Sanitização de Nome Documento
|
||||||
@field_validator("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):
|
class AtoDocumentoUpdateSchema(BaseModel):
|
||||||
# Todos os campos são opcionais no UPDATE
|
# Todos os campos são opcionais no UPDATE
|
||||||
ato_principal_id: Optional[int] = None
|
ato_principal_id: Optional[int] = None
|
||||||
url: Optional[str] = None
|
arquivo_base64: Optional[str] = None
|
||||||
nome_documento: Optional[constr(max_length=255)] = None
|
nome_documento: Optional[constr(max_length=255)] = None
|
||||||
tipo_documento: Optional[constr(max_length=50)] = 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
|
# Validação de Tipo Documento
|
||||||
@field_validator("tipo_documento")
|
@field_validator("tipo_documento")
|
||||||
def validate_tipo_documento(cls, v: Optional[str]):
|
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
|
from datetime import datetime
|
||||||
import re
|
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.)
|
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
|
||||||
# É importante que esta função seja mantida/implementada no seu ambiente
|
# É importante que esta função seja mantida/implementada no seu ambiente
|
||||||
# from actions.validations.text import Text # Descomentar se for usar
|
# 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
|
# Sanitiza o campo
|
||||||
return v
|
return Text.sanitize_input(v)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
@ -102,12 +105,12 @@ class AtoParteSaveSchema(BaseModel):
|
||||||
# Validação de Nome
|
# Validação de Nome
|
||||||
@field_validator("nome")
|
@field_validator("nome")
|
||||||
def validate_nome(cls, v: str):
|
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
|
# Validação de CPF/CNPJ
|
||||||
@field_validator("cpf_cnpj")
|
@field_validator("cpf_cnpj")
|
||||||
def validate_cpf_cnpj_field(cls, v: str):
|
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]):
|
def validate_nome_update(cls, v: Optional[str]):
|
||||||
if v is None:
|
if v is None:
|
||||||
return v
|
return v
|
||||||
return validate_nome_not_empty(cls, v)
|
return validate_nome_not_empty(cls, Text.sanitize_input(v))
|
||||||
|
|
||||||
# Validação de CPF/CNPJ
|
# Validação de CPF/CNPJ
|
||||||
@field_validator("cpf_cnpj")
|
@field_validator("cpf_cnpj")
|
||||||
def validate_cpf_cnpj_update(cls, v: Optional[str]):
|
def validate_cpf_cnpj_update(cls, v: Optional[str]):
|
||||||
if v is None:
|
if v is None:
|
||||||
return v
|
return Text.sanitize_input(v)
|
||||||
return validate_cpf_cnpj(cls, 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)
|
# Nota: Telefone não precisa de validação complexa além do constr(max_length)
|
||||||
# se o objetivo for apenas armazenar a string fornecida.
|
# se o objetivo for apenas armazenar a string fornecida.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ from typing import Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal # Importar Decimal para campos monetários
|
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.)
|
# Funções para sanitização de entradas (evitar XSS, SQLi etc.)
|
||||||
# É importante que esta função seja mantida/implementada no seu ambiente
|
# É importante que esta função seja mantida/implementada no seu ambiente
|
||||||
# from actions.validations.text import Text # Descomentar se for usar
|
# 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",
|
"codigo_ato",
|
||||||
"nome_civil_ato",
|
"nome_civil_ato",
|
||||||
"nome_serventuario_praticou_ato",
|
"nome_serventuario_praticou_ato",
|
||||||
|
"inteiro_teor",
|
||||||
)
|
)
|
||||||
def validate_required_strings(cls, v: str):
|
def validate_required_strings(cls, v: str):
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
|
|
@ -183,7 +187,7 @@ class AtoPrincipalSaveSchema(BaseModel):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
# Adicionar aqui a sanitização de texto (ex: Text.sanitize(v)) se disponível
|
# 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)
|
# Validação dos campos monetários (baseado na CHECK CONSTRAINT da DDL)
|
||||||
@field_validator(
|
@field_validator(
|
||||||
|
|
@ -206,7 +210,7 @@ class AtoPrincipalSaveSchema(BaseModel):
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return v
|
return Text.sanitize_input(v)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
@ -257,7 +261,7 @@ class AtoPrincipalUpdateSchema(BaseModel):
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return v
|
return Text.sanitize_input(v)
|
||||||
|
|
||||||
# Reutiliza a validação de valores positivos
|
# Reutiliza a validação de valores positivos
|
||||||
@field_validator(
|
@field_validator(
|
||||||
|
|
@ -274,7 +278,7 @@ class AtoPrincipalUpdateSchema(BaseModel):
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return v
|
return Text.sanitize_input(v)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue