diff --git a/.gitignore b/.gitignore index d276bf8..a56c37d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.env docs/ *.pyc -tests/.*/ \ No newline at end of file +tests/.*/ +tests\.tmp_arquivo.pdf \ No newline at end of file diff --git a/README.md b/README.md index 6e3132b..1e1433f 100644 --- a/README.md +++ b/README.md @@ -241,11 +241,14 @@ O worker: - atualiza o status do anexo; - envia o arquivo ao Cloudinary; +- organiza o arquivo em `acta-arquivos/{nome-empresa}-{uuid-curto}/ciclo-{id_ciclo}/{id_anexo}`; - salva o `secure_url` retornado; - altera o status para `ATIVO` em caso de sucesso; - altera o status para `ERRO` em caso de falha definitiva; - remove o arquivo temporário após o processamento concluído. +O nome da empresa é normalizado e recebe um UUID curto e estável, mantendo os anexos separados por empresa e ciclo. + Falhas de enfileiramento retornam `503`. Arquivos vazios retornam `400` e arquivos acima de 10 MB retornam `413`. ## 🧪 Testes diff --git a/app/services/jobs.py b/app/services/jobs.py index ab67d07..206dc83 100644 --- a/app/services/jobs.py +++ b/app/services/jobs.py @@ -1,5 +1,8 @@ +import re +import unicodedata from pathlib import Path from tempfile import NamedTemporaryFile +from uuid import NAMESPACE_URL, uuid5 from rq import get_current_job from app.database.postgres import conn_worker from app.models.enums import Status @@ -17,7 +20,14 @@ def processar_upload(job: UploadJob) -> None: caminho_temp = arquivo_temp.name _atualizar_status(conn, job.id_anexo, job.id_usuario, Status.PROCESSANDO) - resultado = enviar_cloudinary(caminho_temp, job.nome_original, job.extensao, public_id=f'anexos/{job.id_anexo}') + pasta = _obter_pasta_cloudinary(conn, job.id_anexo) + resultado = enviar_cloudinary( + caminho_temp, + job.nome_original, + job.extensao, + pasta=pasta, + public_id=str(job.id_anexo) + ) _atualizar_sucesso(conn, job.id_anexo, job.id_usuario, resultado) except Exception: @@ -35,6 +45,29 @@ def _configurar_usuario(conn, id_usuario: int) -> None: with conn.cursor() as cursor: cursor.execute("SELECT set_config('app.current_user_id', %s, true)", (str(id_usuario),),) +def _obter_pasta_cloudinary(conn, id_anexo: int) -> str: + with conn.cursor() as cursor: + cursor.execute( + """ + SELECT e.id, e.nome, a.id_ciclo + FROM pdca.anexo a + JOIN public.empresa e ON e.id = a.id_empresa + WHERE a.id = %s + """, + (id_anexo,) + ) + empresa = cursor.fetchone() + + if empresa is None: + raise ValueError('Empresa do anexo não encontrada') + + id_empresa, nome_empresa, id_ciclo = empresa + nome_normalizado = unicodedata.normalize('NFKD', nome_empresa).encode('ascii', 'ignore').decode().lower() + nome_normalizado = re.sub(r'[^a-z0-9]+', '-', nome_normalizado).strip('-') + uuid_curto = uuid5(NAMESPACE_URL, f'acta:empresa:{id_empresa}').hex[:8] + + return f'acta-arquivos/{nome_normalizado}-{uuid_curto}/ciclo-{id_ciclo}' + def _atualizar_status(conn, id_anexo: int, id_usuario: int, status: Status) -> None: _configurar_usuario(conn, id_usuario) with conn.cursor() as cursor: diff --git a/tests/test_jobs.py b/tests/test_jobs.py index a537810..1700d1e 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -1,11 +1,13 @@ -from pathlib import Path +from unittest.mock import Mock + from app.models.enums import Status from app.schemas import UploadJob from app.services import jobs class Cursor: - def __init__(self): + def __init__(self, retorno=(5, "Instituto J&F", 7)): self.executados = [] + self.retorno = retorno def __enter__(self): return self @@ -16,6 +18,9 @@ def __exit__(self, *args): def execute(self, sql, parametros): self.executados.append((sql, parametros)) + def fetchone(self): + return self.retorno + class Connection: def __init__(self): self.cursor_obj = Cursor() @@ -28,16 +33,19 @@ def commit(self): self.commits += 1 def test_processa_upload_com_sucesso(monkeypatch): - caminho = Path("tests/.tmp_arquivo.pdf") - caminho.write_bytes(b"pdf") conexao = Connection() job = UploadJob(id_anexo=42, id_usuario=3, conteudo=b"pdf", nome_original="arquivo.pdf", extensao="pdf") + enviar_cloudinary = Mock(return_value={"url": "https://arquivo", "public_id": "42", "resource_type": "image"}) monkeypatch.setattr(jobs, "conn_worker", lambda: _contexto(conexao)) - monkeypatch.setattr(jobs, "enviar_cloudinary", lambda *args, **kwargs: {"url": "https://arquivo", "public_id": "42", "resource_type": "image"}) + monkeypatch.setattr(jobs, "enviar_cloudinary", enviar_cloudinary) jobs.processar_upload(job) assert any(Status.ATIVO.value in parametros for _, parametros in conexao.cursor_obj.executados) + assert enviar_cloudinary.call_args.kwargs == { + "pasta": "acta-arquivos/instituto-j-f-f6ce836f/ciclo-7", + "public_id": "42" + } class _contexto: def __init__(self, valor): @@ -47,4 +55,4 @@ def __enter__(self): return self.valor def __exit__(self, *args): - return False \ No newline at end of file + return False