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 v2s3, 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 oUrlConnectionHttpClient(não onetty-niodefault) pra não colidir com o Netty do Micronaut.- Captura na ingestão (
ProdutoService) — após gravar oIngestaoSnapshot, fazput("ingestao/<id>", bytes, tipo)e gravaarquivo_ref/arquivo_tipona linha (migração V7). Vale nas duas portas (/api/csv/processarasync e/admin/uploadsíncrono). Falha doputnão derruba a ingestão (o processamento é o efeito principal). - Download
GET /admin/{id}/arquivo(AdminViewController, sob aViewSecurityRule) — stream proxied do Garage comContent-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): oCleanupJobapaga o objeto e zeraarquivo_refaos 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-micronautnão tinha recipe de storage — §6). (Superado em 2026-08-11: a casa extraiuxadm-comum-storage, adotada — ver Atualização abaixo.) - Envs reais são
GARAGE_*, nãoS3_*. A docinfraestrutura/garagedizS3_*, mas o broker injetaGARAGE_ENDPOINT/GARAGE_BUCKET/GARAGE_REGION/GARAGE_ACCESS_KEY_ID/GARAGE_SECRET_ACCESS_KEY(§6). Mapeados paragarage.*noapplication.ymlvia placeholder${GARAGE_*}— evita a ambiguidade env→property do Micronaut (nome com múltiplos_). Aregionvem do env, então o app usa o valor do cluster — que precisa sergarage. O broker chegou a injetargarega(typo) e o valor se propagou aapp.json/Coolify; não é inofensivo: o Garage valida a region no escopo SigV4 e exigegarage— comgaregatoda chamada S3 volta400 AuthorizationHeaderMalformed. Corrigido na fonte client-grade (probe LIST/PUT/GET/DELETE 200/204 comgarage). - Teste com
adobe s3mock, não MinIO. A casa migrou de MinIO pro Garage — o duplo de teste é ocom.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-storageadotada — 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 asGARAGE_*no Coolify (ver implantação §5.2). Oapp.jsonguarda o client-grade (features.garage), nunca o segredo.
Alternativas descartadas¶
byteano 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:
ArquivoStoreagora injeta oS3Clientbean da lib. AS3ClientFactory(@Factory) o constrói a partir doS3Config(@ConfigurationProperties("s3")): endpoint + path-style + creds estáticas. O wrapper local ficou só comput/get/delete(a interfaceArquivoXlsStorageda lib é datada/XLS, não casa com o.zip/.csv+contentTypedaqui). O bean da lib é lazy → o gate continua no consumidor (@RequiresnoArquivoStore).- Config permanece
garage.*(o storage É o Garage — nome semântico). A lib binda o prefixo fixos3.*; oGarageS3ConfigFactorymonta oS3Configda lib a partir degarage.*e@Replaceso bindings3.*default. Envs de deployGARAGE_*e@Requires garage.secret-access-keyinalterados. netty-nio-client(transitivo daxadm-comum-storageviaawssdk:s3) excluído — mantém ourl-connection-clientcomo 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 peloGarageTestResourcedaxadm-comum-teste(que sobe um Garage realdxflrs/garage— mais pesado); o s3mock foi escolha deliberada e continua servindo. Idem Postgres: mantido o Micronaut Test Resources (framework, idiomático), não oPostgresTestResourceda 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@RequiresdoArquivoStoree os testes de integração leemgarage.secret-access-key/garage.access-key-id, que axadm-comum-storage0.3.0 liga direto das envs, sem ponte noapplication.yml. O s3mock segue como fixture de S3.