Buckets do R2

O TermoBucket usa quatro buckets do Cloudflare R2, dois por ambiente. O backend conversa com eles pela API compatível com S3 (crate aws-sdk-s3, endpoint https://<account_id>.r2.cloudflarestorage.com, região auto), com um único cliente criado na subida do serviço.

BucketRota da APIAcesso direto
tt-arquivos-public-prod/public/<app> em produçãoLeitura pública pelo domínio do bucket
tt-arquivos-private-prod/private/<app> em produçãoNenhum: só por URL pré-assinada
tt-arquivos-public-dev/public/<app> em dev e sandboxLeitura pública pelo domínio do bucket
tt-arquivos-private-dev/private/<app> em dev e sandboxNenhum: só por URL pré-assinada

Dentro de cada bucket, todo objeto fica sob o prefixo do app: <app>/<uuid>.<extensão>.

Variáveis do backend

VariávelValor
R2_ACCOUNT_IDId da conta Cloudflare (aparece na URL do painel do R2)
R2_ACCESS_KEY_IDAccess key de um token do R2
R2_SECRET_ACCESS_KEYSecret do mesmo token
R2_AMBIENTEprod ou dev; escolhe o par de buckets
R2_URL_PUBLICAOpcional. Domínio público do bucket público, sem barra no fim (ex.: https://<dominio-do-bucket-publico>)

As quatro primeiras são obrigatórias: sem elas o backend não sobe. Crie o token do R2 em R2 > Manage R2 API Tokens com permissão Object Read & Write restrita aos dois buckets do ambiente. Essas credenciais existem só no TermoBucket; nenhum outro app recebe chave do R2.

Acesso público

  • Buckets public: ligue um domínio próprio em Settings > Custom Domains (o domínio r2.dev é só para testes) e coloque o endereço em R2_URL_PUBLICA. Assim GET /public/<app>/<id> devolve também a url_publica, que não expira.
  • Buckets private: nunca ligue domínio público nem r2.dev. O único caminho até o objeto é a URL pré-assinada de 15 minutos que o TermoBucket gera depois de conferir a chave do app.

CORS

As URLs pré-assinadas só funcionam no navegador com CORS configurado no bucket. O backend do app pode chamar a URL sem CORS, mas um upload feito direto do navegador precisa dele.

Configure em R2 > bucket > Settings > CORS Policy. Em AllowedOrigins, liste os frontends dos apps que enviam ou baixam arquivos daquele ambiente.

tt-arquivos-private-prod (troque pelos endereços de produção dos frontends que enviam ou baixam arquivos):

[
  {
    "AllowedOrigins": ["https://<frontend-do-app-1>", "https://<frontend-do-app-2>"],
    "AllowedMethods": ["GET", "HEAD", "PUT"],
    "AllowedHeaders": ["Content-Type"],
    "ExposeHeaders": ["ETag", "Content-Disposition"],
    "MaxAgeSeconds": 3600
  }
]
json

tt-arquivos-public-prod: leitura liberada para qualquer origem, envio só dos apps:

[
  {
    "AllowedOrigins": ["*"],
    "AllowedMethods": ["GET", "HEAD"],
    "MaxAgeSeconds": 86400
  },
  {
    "AllowedOrigins": ["https://<frontend-do-app-1>", "https://<frontend-do-app-2>"],
    "AllowedMethods": ["PUT"],
    "AllowedHeaders": ["Content-Type"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]
json

Buckets dev: as mesmas regras, com as origens de dev de cada app (a porta x0 da faixa dele, como http://localhost:51010 do TermoPortal e http://localhost:51020 do TermoSell) e as de sandbox (https://sandbox-*.termotubos.com.br, uma por app).

Só o Content-Type precisa estar em AllowedHeaders. O Content-Length também faz parte da assinatura, mas o navegador o preenche sozinho com o tamanho real do corpo, sem passar pelo preflight.

Ciclo de vida por prefixo

As regras ficam em R2 > bucket > Settings > Object lifecycle rules. Cada regra pode valer para o bucket inteiro (prefixo vazio) ou só para um prefixo, e o prefixo de um app é sempre <app>/ (com a barra, para termo não pegar também termosell/).

BucketRegraPrefixoAção
Todosabortar-envios-incompletosvazioAbortar envios multipart incompletos após 1 dia
*-devexpirar-devvazioApagar objetos após 30 dias
*-prodnenhuma por padrãoArquivo de produção só sai pela API ou pela limpeza do app
*-prodsob pedido do app<app>/Apagar após N dias, só para apps de arquivos temporários
tt-arquivos-private-prodopcional<app>/Mover para Infrequent Access após 90 dias, para arquivos raramente baixados
A regra de ciclo de vida não avisa o TermoBucket

Quando o R2 expira um objeto, o registro na tabela arquivo continua ativo: ele segue contando no consumo, aparece na listagem e o download responde 404 do R2. Por isso não há expiração por padrão em produção. Um app que pedir regra de expiração no prefixo dele precisa tratar esse 404 como "arquivo vencido". Nos buckets dev, o mesmo vale para o banco de dev e do sandbox.

Para remover de vez todos os arquivos de um app, use a limpeza pelo painel ( API de arquivos ): ela apaga o prefixo nos dois buckets e marca os registros como deletados, mantendo o consumo correto.

Atualizado em 2026/10/09 03:30