Pular para conteúdo

0010 — Guardar o upload da ingestão no Garage

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15 · Decidido em: 2026-08-10

Contexto

O X-Adm posta o catálogo (ProdutosSite.csv em .zip) no tradutor (POST /api/csv/processar), que extrai, processa e descarta os bytes. Ao investigar um processamento na tela /admin/{id}, o operador não conseguia baixar o arquivo que foi usado — a evidência não existia. Faltava guardar o upload e expor um download.

Decisão

Guardar o upload original (os bytes como vieram — .zip ou .csv) no Garage (object storage S3-compatível da casa) e servir um botão de download:

  • ArquivoStore (pacote …webstormecom.arquivo) — cliente AWS SDK v2 s3, path-style, endpointOverride. @Requires(property="garage.secret-access-key", pattern=".+"): o bean só sobe com a write key presente — sem Garage configurado, a captura é no-op e o botão some (dev/test sobem sem Garage). Usa o UrlConnectionHttpClient (não o netty-nio default) pra não colidir com o Netty do Micronaut.
  • Captura na ingestão (ProdutoService) — após gravar o IngestaoSnapshot, faz put("ingestao/<id>", bytes, tipo) e grava arquivo_ref/arquivo_tipo na linha (migração V7). Vale nas duas portas (/api/csv/processar async e /admin/upload síncrono). Falha do put não derruba a ingestão (o processamento é o efeito principal).
  • Download GET /admin/{id}/arquivo (AdminViewController, sob a ViewSecurityRule) — stream proxied do Garage com Content-Disposition; botão Baixar arquivo no detalhe, só quando há ref.
  • Retenção do arquivo = 30 dias (webstorm.arquivo.retencao-dias), menor que a da linha (90d): o CleanupJob apaga o objeto e zera arquivo_ref aos 30d; a linha de auditoria fica os 90d, só perde o download.

Notas de implementação (armadilhas de stack — não há recipe da casa)

  • Não existia lib de storage na época. Garage é S3 puro; o app falava AWS SDK v2 path-style direto. Era net-new (a página engenharia/java-micronaut não tinha recipe de storage — §6). (Superado em 2026-08-11: a casa extraiu xadm-comum-storage, adotada — ver Atualização abaixo.)
  • Envs reais são GARAGE_*, não S3_*. A doc infraestrutura/garage diz S3_*, mas o broker injeta GARAGE_ENDPOINT/GARAGE_BUCKET/GARAGE_REGION/GARAGE_ACCESS_KEY_ID/GARAGE_SECRET_ACCESS_KEY (§6). Mapeados para garage.* no application.yml via placeholder ${GARAGE_*} — evita a ambiguidade env→property do Micronaut (nome com múltiplos _). A region vem do env, então o app usa o valor do cluster — que precisa ser garage. O broker chegou a injetar garega (typo) e o valor se propagou a app.json/Coolify; não é inofensivo: o Garage valida a region no escopo SigV4 e exige garage — com garega toda chamada S3 volta 400 AuthorizationHeaderMalformed. Corrigido na fonte client-grade (probe LIST/PUT/GET/DELETE 200/204 com garage).
  • Teste com adobe s3mock, não MinIO. A casa migrou de MinIO pro Garage — o duplo de teste é o com.adobe.testing:s3mock-testcontainers (mock S3 feito pra teste), que exercita o protocolo S3 real (path-style, AWS SDK) sem reintroduzir MinIO.

Fica de fora (follow-up se exigido)

  • Presigned URLs — o download é proxied pela app (auth in-app), sem presign.
  • Editar a retenção pela tela / guardar o CSV extraído — guarda-se o upload original; retenção é config.
  • ~~Lib xadm-arquivos — não existe; se a casa extrair uma, migrar então.~~ Feito (2026-08-11): xadm-comum-storage adotada — ver Atualização.

Consequências

  • O upload fica auditável e baixável por 30 dias na tela de detalhe.
  • Provisionamento via /xadm-setup (Garage): cria bucket + write key, injeta as GARAGE_* no Coolify (ver implantação §5.2). O app.json guarda o client-grade (features.garage), nunca o segredo.

Alternativas descartadas

  • bytea no Postgres — entregaria o mesmo botão sem infra (e caía na retenção da linha), mas o usuário optou pelo jeito da casa (object storage). Registrado como a opção "simples" preterida.
  • MinIO como backend — a casa já migrou dele pro Garage; não reintroduzir (nem em teste).
  • Config do bucket em tabela editável — over-engineering; endpoint/bucket/region vêm de env.

Linhagem de trabalho: .ia/012-guardar-arquivo-ingestao-garage-*.

Atualização 2026-08-11 — adota xadm-comum-storage (elimina a construção do client na mão)

Auditoria de aderência às libs da casa: a construção do S3Client que o ArquivoStore fazia na mão era o mesmo código da S3ClientFactory da casa. Adotada a xadm-comum-storage:

  • ArquivoStore agora injeta o S3Client bean da lib. A S3ClientFactory (@Factory) o constrói a partir do S3Config (@ConfigurationProperties("s3")): endpoint + path-style + creds estáticas. O wrapper local ficou só com put/get/delete (a interface ArquivoXlsStorage da lib é datada/XLS, não casa com o .zip/.csv + contentType daqui). O bean da lib é lazy → o gate continua no consumidor (@Requires no ArquivoStore).
  • Config permanece garage.* (o storage É o Garage — nome semântico). A lib binda o prefixo fixo s3.*; o GarageS3ConfigFactory monta o S3Config da lib a partir de garage.* e @Replaces o binding s3.* default. Envs de deploy GARAGE_* e @Requires garage.secret-access-key inalterados.
  • netty-nio-client (transitivo da xadm-comum-storage via awssdk:s3) excluído — mantém o url-connection-client como HTTP client sync e evita o 2º Netty colidindo com o do Micronaut.
  • Teste: segue com adobe s3mock (props s3.*). Decisão explícita: não trocado pelo GarageTestResource da xadm-comum-teste (que sobe um Garage real dxflrs/garage — mais pesado); o s3mock foi escolha deliberada e continua servindo. Idem Postgres: mantido o Micronaut Test Resources (framework, idiomático), não o PostgresTestResource da lib — nenhum dos dois é replicação de código da casa.

Na mesma leva (auditoria de replicação): o SHA-256 na mão do diff/ingestão (ProdutoDiff, ProdutoService) passou a ChecksumSha256.hex do xadm-comum-util (output idêntico — hex minúsculo, sem drift de checksum persistido); e foi adicionado um teste de arquitetura (ArquiteturaTest) com a RegrasArquitetura.semCiclosEntreFatias do xadm-comum-teste (app confirmado livre de ciclos entre fatias).

Atualização 2026-08-19 — bump de manutenção xadm-comum-storage e xadm-comum-teste 0.1.0 → 0.2.0

Acompanhamento do latest do registro. Source-compatível: o S3Config (montado pelo GarageS3ConfigFactory via @Replaces, namespace garage.*) e a RegrasArquitetura do teste seguem com as mesmas assinaturas — zero adaptação, gate da stack verde. As decisões explícitas de manter o adobe s3mock e o Micronaut Test Resources (em vez do GarageTestResource/PostgresTestResource da lib) permanecem.

Atualização 2026-09-11 — a ponte GarageS3ConfigFactory saiu

A premissa dela deixou de valer na xadm-comum-storage 0.2.0: o S3Config da lib passou a bindar garage.* direto (@ConfigurationProperties("garage"), com criar-bucket default false). O @Replaces local só traduzia o nome de duas chaves (access-key-id/secret-access-key → access-key/secret-key) — tradução que agora mora no placeholder do application.yml. As envs de deploy não mudaram (GARAGE_ACCESS_KEY_ID/GARAGE_SECRET_ACCESS_KEY); o @Requires do ArquivoStore e os testes de integração passaram a usar garage.secret-key/garage.access-key. O s3mock segue como fixture de teste (decisão explícita acima, inalterada).

Correção (2026-09-15): o Postgres de teste é o singleton da xadm-comum-teste (IntegracaoComPostgres), a norma da casa para o teste de integração Micronaut, e o Micronaut Test Resources saiu do build; o Testcontainers chega pela lib e pelo BOM do Micronaut, sem declaração no app. A credencial do Garage usa os nomes do broker: o @Requires do ArquivoStore e os testes de integração leem garage.secret-access-key/garage.access-key-id, que a xadm-comum-storage 0.3.0 liga direto das envs, sem ponte no application.yml. O s3mock segue como fixture de S3.