Pular para o conteúdo

ADR-0030 Armazenamento de ficheiros e objetos

Padrão da empresa para ficheiros — a API S3 sobre um store compatível com S3 (Ceph RADOS Gateway por predefinição), buckets privados e URLs pré-assinados para uploads e downloads a partir do browser.

Decisão
API S3 sobre Ceph ou outro store compatível com S3Guardar e proteger
Estado
Aceite
Data
Âmbito
Predefinição da empresa para todas as novas apps

As apps guardam ficheiros: uploads, exportações geradas, anexos. Precisam de um sítio que não seja nem uma linha no Postgres nem o disco do contentor.

  • Os contentores são substituídos em cada deploy e as réplicas não partilham sistema de ficheiros (ADR-0003)
  • A edge limita o corpo dos pedidos a 10 MB (ADR-0017), pelo que os uploads grandes não podem passar pela app
  • Os deployments variam: Ceph na nossa infraestrutura, ou um store compatível com S3 que o cliente já opera
  • Os ficheiros têm regras de acesso: quem pode ler um ficheiro é decidido pela app, pedido a pedido (ADR-0009)

O que está em aberto: a API de armazenamento, o cliente e como os browsers enviam e obtêm ficheiros.

Prós: nada a mais para operar. Contras: preso a uma máquina, não partilhado entre réplicas, e o backup fica por inventar.

Prós: transacional com o resto dos dados; um só backup. Contras: todos os bytes passam pela app e pelo pool de ligações; a base de dados e os seus backups crescem com o volume de ficheiros.

Prós

  • Uma só API, seja qual for o store do deployment
  • Os URLs pré-assinados deixam o browser enviar e descarregar diretamente do store, sem passar pela app nem pelo seu limite de corpo
  • O RADOS Gateway do Ceph implementa o que precisamos: URLs pré-assinados, CORS por bucket, uploads multipart, regras de ciclo de vida

Contras

  • Um store para operar — e o Ceph não é pequeno. Isso cabe ao deployment
  • “Compatível com S3” não é idêntico: as predefinições de checksums e o endereçamento dos buckets variam entre stores

4. O SDK próprio de um fornecedor (Azure Blob, Google Cloud Storage)

Seção intitulada “4. O SDK próprio de um fornecedor (Azure Blob, Google Cloud Storage)”

Prós: as funcionalidades de primeira linha desse fornecedor. Contras: um módulo de armazenamento por fornecedor; mudar um produto obriga a reescrevê-lo.

A API S3 é o padrão da empresa para ficheiros. O Ceph RADOS Gateway é o store predefinido na nossa infraestrutura; um deployment pode usar qualquer store compatível com S3.

Em concreto:

  • Cliente: o S3Client built-in do Bun (import { S3Client } from "bun", ADR-0035), construído a partir do endpoint, da região, das credenciais e do bucket declarados em server/env.ts (ADR-0026). Nunca o cliente implícito Bun.s3, que lê por si as variáveis S3_* e AWS_*. (Alterado em 2026-11-13: este ADR escolheu primeiro o @aws-sdk/client-s3 com o seu presigner; o cliente do Bun substitui ambos.)
  • Endereçamento path-style, a predefinição do cliente (virtualHostedStyle fica false), para que qualquer endpoint compatível com S3 funcione sem DNS wildcard
  • Um só módulo de armazenamento, src/server/storage/, é o único código que importa o S3Client. O código das funcionalidades recebe-o como parâmetro (ADR-0027) e não usa nada que uma API S3 simples não tenha
  • Os buckets são privados. Sem ACLs nem políticas de leitura pública
  • Uma tabela files no Postgres é a fonte de verdade (ADR-0008): chave, tamanho, content type, dono, estado. O store guarda bytes; todas as decisões de acesso leem a linha
  • Os uploads vão do browser para o store. Uma server function POST verifica a sessão, valida com Zod o tipo e o tamanho declarados, escolhe a chave e devolve um URL PUT pré-assinado de curta duração (presign(key, { method: "PUT", expiresIn, type })). É o servidor que escolhe as chaves ({dono}/{uuid}) — nunca o nome de ficheiro do cliente
  • Confirmar antes de confiar. Depois do upload, o servidor lê os metadados do objeto com file(key).stat(), compara o tamanho e o tipo com o que foi declarado e apaga-o se não coincidirem. Só então a linha fica marcada como pronta
  • Os downloads são URLs GET pré-assinados de curta duração, emitidos depois da mesma verificação de autorização. Os tipos não confiáveis são servidos com Content-Disposition: attachment (a opção de presign contentDisposition)
  • Configuração do deployment: o endpoint do store é acessível aos browsers por TLS, com CORS a permitir a origem da app, e regras de ciclo de vida que abortam uploads multipart incompletos e expiram objetos não confirmados
  • A durabilidade e o backup do store decidem-se por deployment, tal como os da base de dados (ADR-0032)
  • Testes: os testes unitários passam um módulo de armazenamento falso; os de integração correm contra um contentor compatível com S3 em Testcontainers (ADR-0014)

Os fatores decisivos: uma só API em todos os deployments, uploads que nunca passam pela app nem pelo seu limite de corpo, e ficheiros que ficam atrás da autorização da própria app. Aceitamos operar um store e verificar o dialeto S3 de cada store contra as chamadas que usamos.

Mais fácil:

  • As réplicas e os deploys nunca perdem um ficheiro; a app mantém-se sem estado
  • Os uploads grandes funcionam sem subir o limite de corpo da edge
  • Mudar entre o Ceph e outro store compatível com S3 é uma alteração de configuração

Mais difícil:

  • Um upload tem dois passos e pode ficar a meio: as linhas e os objetos não confirmados precisam de limpeza
  • O endpoint do store, o CORS e as regras de ciclo de vida são trabalho do deployment que tem de existir antes do primeiro upload. O cliente do Bun cobre as chamadas a objetos — presign, stat, read, write, delete, list — e não a administração de buckets, pelo que o deployment as define com as suas próprias ferramentas
  • Um ficheiro e a sua linha podem discordar; ganha a linha, e os órfãos são limpos

Do que abdicamos: das funcionalidades específicas de cada store (notificações de eventos, object lock, CDNs do fornecedor) como padrão. Aceite — um produto que precise de uma escreve o seu próprio ADR.