API de arquivos
O TermoBucket guarda os arquivos dos apps da Termotubos no Cloudflare R2. As credenciais do R2 ficam só no TermoBucket: cada app fala com a API de arquivos usando uma chave de acesso própria, e os bytes trafegam direto entre o cliente e o R2 por URLs pré-assinadas.
Cadastro do app
Quem tem permissão na área Aplicativos do painel (/painel/aplicativos) cadastra o app com:
| Campo | Para que serve |
|---|---|
slug | Identifica o app nas rotas e é o prefixo das chaves no R2. De 3 a 50 caracteres: letras minúsculas, números e hífen, sem hífen nas pontas. Não muda depois de criado. |
nome | Nome exibido no painel |
tamanho_maximo | Maior arquivo aceito, em bytes (até 4,995 GiB, o limite do R2 por envio) |
tipos_permitidos | Tipos MIME aceitos, como application/pdf, ou um curinga como image/* |
Ao criar, o painel mostra a chave de acesso (ttb_…) uma única vez. O TermoBucket guarda só o hash SHA-256 dela. Se a chave se perder ou vazar, use Gerar nova chave na edição do app: a antiga para de funcionar na hora.
Autenticação
Toda chamada leva a chave no cabeçalho:
Authorization: Bearer ttb_... A chave identifica o app. O slug que vem no caminho tem que ser o do dono da chave, senão a resposta é 403. Mesmo assim, toda consulta usa o app da chave, nunca o do caminho: trocar o slug na URL não dá acesso a nada de outro app.
Rotas
Os dois grupos têm o mesmo comportamento; muda só o bucket.
| Método | Rota | O que faz |
|---|---|---|
POST | /public/<app> ou /private/<app> | Registra o arquivo e devolve a URL de envio |
GET | /public/<app> ou /private/<app> | Lista os arquivos do app, do mais novo para o mais antigo (?pagina=1&tamanho=10) |
GET | /public/<app>/<id> ou /private/<app>/<id> | Devolve o registro e a URL de download |
DELETE | /public/<app>/<id> ou /private/<app>/<id> | Apaga o objeto no R2 e marca o registro como deletado |
/public usa o bucket tt-arquivos-public-<ambiente> e /private o tt-arquivos-private-<ambiente>. Um arquivo enviado por /private não aparece em /public e vice-versa.
Enviar um arquivo
1. Pedir a URL de envio com o nome, o tipo e o tamanho exato em bytes:
curl -X POST http://localhost:51121/private/termosell \
-H "Authorization: Bearer $CHAVE_TERMOBUCKET" \
-H "Content-Type: application/json" \
-d '{ "nome_original": "pedido 1234.pdf", "tipo": "application/pdf", "tamanho": 48213 }' O TermoBucket confere o tamanho e o tipo contra os limites do app, gera o id (o cliente nunca escolhe o id nem a chave do objeto) e responde 201:
{
"arquivo": {
"id": "0b6f8f0e-5a7c-4a4e-9a49-3c1f6c2d8e11",
"app": "termosell",
"bucket": "tt-arquivos-private-prod",
"chave": "termosell/0b6f8f0e-5a7c-4a4e-9a49-3c1f6c2d8e11.pdf",
"nome_original": "pedido 1234.pdf",
"tipo": "application/pdf",
"tamanho": 48213,
"visibilidade": "privado",
"criado_em": "2026-10-03T23:40:00Z",
"atualizado_em": "2026-10-03T23:40:00Z"
},
"url_envio": "https://<conta>.r2.cloudflarestorage.com/tt-arquivos-private-prod/termosell/0b6f...pdf?X-Amz-...",
"metodo": "PUT",
"cabecalhos": { "content-length": "48213", "content-type": "application/pdf" },
"expira_em": "2026-10-03T23:55:00Z"
} 2. Enviar os bytes com um PUT na url_envio, mandando exatamente os cabecalhos da resposta. O Content-Type e o Content-Length fazem parte da assinatura: se o arquivo tiver outro tamanho ou outro tipo, o R2 recusa o envio.
curl -X PUT "$URL_ENVIO" \
-H "Content-Type: application/pdf" \
--data-binary @"pedido 1234.pdf" A URL vale 15 minutos. A chave do objeto segue sempre o formato <app>/<id>.<extensão>, com a extensão tirada do nome original (ou bin quando não há uma válida).
Baixar um arquivo
GET /private/<app>/<id> devolve o registro, a url_download (válida por 15 minutos, já com o nome original no download) e o expira_em. No grupo /public, a resposta também traz url_publica, o endereço permanente pelo domínio público do bucket, quando ele está configurado (R2_URL_PUBLICA).
Erros
As respostas de erro vêm como { "erro": "mensagem" }.
| Status | Quando |
|---|---|
400 | Corpo inválido, tipo fora do formato tipo/subtipo, tamanho zero ou id que não é UUID |
401 | Chave ausente ou inválida |
403 | App inativo, ou o slug do caminho não é o do dono da chave |
404 | Arquivo não existe para este app nesta visibilidade |
413 | Arquivo maior que o tamanho_maximo do app |
415 | Tipo não aceito pelos tipos_permitidos do app |
422 | Corpo com campo desconhecido (por exemplo, tentar mandar id ou chave) |
502 | O R2 falhou ao assinar a URL ou ao apagar o objeto |
Consumo e limpeza
Cada arquivo vira uma linha na tabela arquivo (id, app, bucket, chave, nome_original, tipo, tamanho, visibilidade, criado_em). A lista de Aplicativos do painel soma essa tabela e mostra, por app, a quantidade e o tamanho dos arquivos públicos e privados.
Para remover todos os arquivos de um app, um master do TermoAuth usa Apagar todos os arquivos na exclusão do app (DELETE /aplicativo/<id>/arquivos). O TermoBucket apaga o prefixo <app>/ nos dois buckets e só depois marca os registros como deletados. Um app só pode ser excluído quando não tem mais nenhum arquivo, porque o slug fica livre para um app novo.
- O registro nasce quando o app pede a URL de envio. Se o
PUTnunca acontecer, o arquivo continua contando no consumo e o download responde404do R2. - Uma regra de ciclo de vida do R2 que expira objetos não avisa o TermoBucket: o registro continua ativo. Veja Buckets do R2 .