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.
| Bucket | Rota da API | Acesso direto |
|---|---|---|
tt-arquivos-public-prod | /public/<app> em produção | Leitura pública pelo domínio do bucket |
tt-arquivos-private-prod | /private/<app> em produção | Nenhum: só por URL pré-assinada |
tt-arquivos-public-dev | /public/<app> em dev e sandbox | Leitura pública pelo domínio do bucket |
tt-arquivos-private-dev | /private/<app> em dev e sandbox | Nenhum: 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ável | Valor |
|---|---|
R2_ACCOUNT_ID | Id da conta Cloudflare (aparece na URL do painel do R2) |
R2_ACCESS_KEY_ID | Access key de um token do R2 |
R2_SECRET_ACCESS_KEY | Secret do mesmo token |
R2_AMBIENTE | prod ou dev; escolhe o par de buckets |
R2_URL_PUBLICA | Opcional. 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ínior2.devé só para testes) e coloque o endereço emR2_URL_PUBLICA. AssimGET /public/<app>/<id>devolve também aurl_publica, que não expira. - Buckets
private: nunca ligue domínio público nemr2.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
}
] 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
}
] 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/).
| Bucket | Regra | Prefixo | Ação |
|---|---|---|---|
| Todos | abortar-envios-incompletos | vazio | Abortar envios multipart incompletos após 1 dia |
*-dev | expirar-dev | vazio | Apagar objetos após 30 dias |
*-prod | nenhuma por padrão | Arquivo de produção só sai pela API ou pela limpeza do app | |
*-prod | sob pedido do app | <app>/ | Apagar após N dias, só para apps de arquivos temporários |
tt-arquivos-private-prod | opcional | <app>/ | Mover para Infrequent Access após 90 dias, para arquivos raramente baixados |
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.