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:

CampoPara que serve
slugIdentifica 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.
nomeNome exibido no painel
tamanho_maximoMaior arquivo aceito, em bytes (até 4,995 GiB, o limite do R2 por envio)
tipos_permitidosTipos 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_...
http

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étodoRotaO 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 }'
bash

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"
}
json

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"
bash

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" }.

StatusQuando
400Corpo inválido, tipo fora do formato tipo/subtipo, tamanho zero ou id que não é UUID
401Chave ausente ou inválida
403App inativo, ou o slug do caminho não é o do dono da chave
404Arquivo não existe para este app nesta visibilidade
413Arquivo maior que o tamanho_maximo do app
415Tipo não aceito pelos tipos_permitidos do app
422Corpo com campo desconhecido (por exemplo, tentar mandar id ou chave)
502O 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 que o consumo não enxerga
  • O registro nasce quando o app pede a URL de envio. Se o PUT nunca acontecer, o arquivo continua contando no consumo e o download responde 404 do 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 .
Atualizado em 2026/10/09 03:30