Skip to content

Repositório de Arquivos — API REST

Esta página documenta a API REST de gestão de arquivos da plataforma HorusBI, disponível para integrações server-to-server (headless). Com ela, você pode fazer upload, listar, consultar metadados, controlar visibilidade, gerar links temporários e excluir arquivos do repositório de um tenant.

Envelope de resposta

Todos os endpoints de /v1/tenant/files* retornam respostas no envelope padrão da plataforma:

Sucesso:

json
{ "status": "success", "data": <payload> }

Erro:

json
{ "status": "error", "message": "Descrição do erro" }

Os exemplos de resposta ao longo desta página já mostram o envelope completo.

Autenticação

Todos os endpoints ficam sob /v1/tenant e usam o Tenant Bearer Token (sys_tenant_api_token):

http
Authorization: Bearer <sys_tenant_api_token>

O token identifica o tenant. Sem ele, todas as rotas retornam 401 Unauthorized.

Escopo: os endpoints REST de Arquivos operam no tenant do token bearer e não são filtrados por usuário (sem owner-scoping). O controle por função (files) e por kind (ETL via flow) aplica-se às telas do HEC/ETL, não à API headless.

Origens dos arquivos

Cada arquivo tem uma origin que indica como foi criado:

OriginQuem criaPadrão de visibilidade
apiREST API (este endpoint)Privado
hecUpload manual via HEC, ou via presign+confirm na RESTPrivado
dwSistema (writeback de Cadastros, exportações)Público
etlPipeline ETLPrivado

Fluxo de upload grande

Para arquivos grandes (ex.: parquets de 700 MB), use o fluxo presigned que evita passar bytes pelo backend:

1. POST /v1/tenant/files/presign   → { id, key, uploadUrl }
2. PUT  <uploadUrl>                 → (direto no S3, Content-Type no header)
3. POST /v1/tenant/files/confirm   → FileDTO completo

Para arquivos pequenos (até ~1 GB), /v1/tenant/files/upload com multipart/form-data também funciona.

Limite por arquivo: o backend lê o tamanho real via HeadObject no S3 ao confirmar o upload (campo file_size não é aceito no body do /files/confirm). O teto é 2 147 483 647 bytes (~2 GB, limite da coluna int4). O /files/upload (multipart) é limitado a ~1 GB pelo middleware de streaming.

Content-Type no PUT do presign (footgun): ao fazer o PUT diretamente na uploadUrl retornada pelo /files/presign, o cliente deve enviar o header Content-Type com o valor exatamente igual ao mime informado na chamada ao /files/presign. O S3 inclui o Content-Type na assinatura da URL; qualquer divergência resulta em SignatureDoesNotMatch (HTTP 403).


Shape: FileDTO

Todos os endpoints de leitura retornam objetos no formato FileDTO:

CampoTipoDescrição
idstring (UUID)Identificador único do arquivo
file_namestringNome do arquivo
file_sizeinteger | nullTamanho em bytes (null enquanto o presign não é confirmado)
mimestring | nullMIME type (ex.: application/parquet, image/png)
originstringOrigem: api, hec, dw ou etl
publicbooleantrue = URL pública sem autenticação; false = requer token assinado
criado_porinteger | nullID do usuário que fez o upload; null quando não há contexto de usuário
criado_emstring (ISO 8601)Data/hora de criação
urlstringURL canônica do arquivo: https://storage.horusbi.com.br/f/{id}/{file_name}
uploaderstringRótulo legível de quem enviou: nome do usuário (quando criado_por != null), ou "API" / "ETL" / "Sistema" (quando criado_por é null, derivado da origin)

uploader — lógica de resolução: se criado_por está preenchido, é o nome do usuário em sys_user; se não está, o rótulo vem da origin: api"API", etl"ETL", dw ou hec"Sistema".


Referência dos endpoints

GET /v1/tenant/files

Lista os arquivos do tenant, paginado, ordenado por data de criação decrescente. Exclui arquivos marcados como excluídos (excluido=true).

Nota: uploads presigned abandonados (reservas criadas pelo /presign mas nunca confirmadas) não aparecem nesta listagem. Apenas arquivos com upload concluído são retornados.

Query params:

ParâmetroTipoPadrãoDescrição
limitinteger50Máximo de registros por página (teto: 200)
offsetinteger0Número de registros a pular (paginação)
originstringFiltro de origem: api, hec, dw ou etl

Resposta (200):

json
{
  "status": "success",
  "data": {
    "rows": [
      {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "file_name": "relatorio-junho.parquet",
        "file_size": 734003200,
        "mime": "application/octet-stream",
        "origin": "api",
        "public": false,
        "criado_por": null,
        "criado_em": "2026-06-26T14:30:00.000Z",
        "url": "https://storage.horusbi.com.br/f/a1b2c3d4-e5f6-7890-abcd-ef1234567890/relatorio-junho.parquet",
        "uploader": "API"
      }
    ],
    "total": 42
  }
}

GET /v1/tenant/files/:id

Retorna os metadados de um único arquivo. Retorna 404 se o arquivo não existir, pertencer a outro tenant, ou estiver excluído.

Path params: :id — UUID do arquivo

Resposta (200):

json
{ "status": "success", "data": { /* FileDTO — ver shape acima */ } }

Resposta (404):

json
{ "status": "error", "message": "Arquivo não encontrado" }

POST /v1/tenant/files/upload

Upload multipart de um arquivo (caminho compatível, adequado para arquivos até ~1 GB). O arquivo é transmitido em streaming direto para o S3 — sem bufferizar em memória.

Tipo de conteúdo: multipart/form-data

CampoTipoObrigatórioDescrição
filefilesimO arquivo a fazer upload (nome do campo deve ser "file")
filenamestringnãoNome desejado para o arquivo; se omitido, usa o nome original do upload

O limite de tamanho é ~1 GB. Para arquivos maiores, use o fluxo presign → PUT → confirm.

Resposta (200):

json
{
  "status": "success",
  "data": {
    "url": "https://storage.horusbi.com.br/f/a1b2c3d4-e5f6-7890-abcd-ef1234567890/relatorio-junho.parquet",
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Nota: esta rota cria o arquivo com origin: "api" e criado_por: null. Arquivos com o mesmo file_name no tenant são reutilizados (idempotente por nome): se já existe um arquivo ativo com esse nome, o upload substitui o conteúdo no S3 e atualiza os metadados.

Respostas de erro:

CódigoSituação
400Nenhum arquivo enviado, ou campo não se chama "file"
500Erro interno (ex.: falha no S3)

POST /v1/tenant/files/presign

Passo 1 do fluxo de upload grande. Cria a linha no banco (origin=hec, public=false) e retorna uma URL pré-assinada para PUT direto no S3. A URL expira em 15 minutos (900 segundos).

Body (JSON):

CampoTipoObrigatórioDescrição
file_namestringsimNome do arquivo a criar
mimestringnãoMIME type (ex.: application/parquet); padrão: application/octet-stream

Resposta (200):

json
{
  "status": "success",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "key": "42/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "uploadUrl": "https://horusbi-uploads.s3.amazonaws.com/42/a1b2c3d4-...?X-Amz-Signature=..."
  }
}

Respostas de erro:

CódigoSituação
400file_name ausente

POST /v1/tenant/files/confirm

Passo 3 do fluxo de upload grande. Após o PUT direto no S3, confirme o upload para preencher file_size, mime e file_path na linha criada pelo /presign. Retorna o FileDTO completo.

Tamanho lido do S3: o backend consulta o S3 via HeadObject para obter o tamanho real do objeto — o campo file_size não é aceito no body. O confirm falha se o PUT não tiver sido concluído (objeto ausente no S3). O teto de ~2 GB é aplicado contra o tamanho real lido.

Body (JSON):

CampoTipoObrigatórioDescrição
idstringsimUUID retornado pelo /presign
mimestringnãoMIME type efetivo do arquivo

Resposta (200):

json
{ "status": "success", "data": { /* FileDTO completo — ver shape acima */ } }

Respostas de erro:

CódigoSituação
400id ausente, ou arquivo não encontrado / já excluído
400"Arquivo não encontrado no storage (upload não concluído)" — PUT nunca foi feito ou falhou
400"Arquivo excede o limite de ~2 GB por arquivo" — tamanho real lido do S3 supera o teto int4

PATCH /v1/tenant/files/:id

Alterna a visibilidade pública/privada do arquivo.

Path params: :id — UUID do arquivo

Body (JSON):

CampoTipoObrigatórioDescrição
publicbooleansimtrue = público (URL sem autenticação); false = privado (requer token assinado)

Resposta (200):

json
{ "status": "success", "data": { "ok": true } }

Respostas de erro:

CódigoSituação
400Arquivo não encontrado

POST /v1/tenant/files/:id/sign

Gera um link temporário assinado para um arquivo privado. O token é armazenado no Redis com TTL; a URL resultante pode ser compartilhada com qualquer pessoa durante a validade.

Path params: :id — UUID do arquivo

Body (JSON):

CampoTipoObrigatórioDescrição
ttlintegernãoValidade em segundos. Intervalo aceito: 60 – 604800 (1 min a 7 dias). Padrão: 3600 (1h). Valores fora do intervalo são clamped para o mínimo ou máximo; ausente/inválido → padrão.

Resposta (200):

json
{
  "status": "success",
  "data": {
    "token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "url": "https://storage.horusbi.com.br/f/a1b2c3d4-e5f6-7890-abcd-ef1234567890/relatorio.parquet?token=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
  }
}

Acesso por visibilidade:

  • Público (public: true): a URL canônica https://storage.horusbi.com.br/f/{id}/{file_name} funciona sem autenticação. O /sign ainda funciona para gerar tokens, mas não é necessário.
  • Privado (public: false): a URL sem ?token= retorna erro. Use /sign para gerar um token temporário e acessar com ?token=<token>. O token é reutilizável até expirar.

Respostas de erro:

CódigoSituação
400Arquivo não encontrado

DELETE /v1/tenant/files/:id

Soft-delete do arquivo: marca como excluído (excluido=true) sem remover do S3 imediatamente. O arquivo deixa de aparecer nas listagens e não é mais acessível. O storage para de ser contado no billing a partir da exclusão.

Restrição: arquivos de origem dw que sejam imagens de Cadastro (writeback) e que ainda estejam referenciados em registros ativos ou histórico são bloqueados. O sistema verifica a URL pública antes de excluir.

Path params: :id — UUID do arquivo

Resposta (200):

json
{ "status": "success", "data": { "ok": true } }

Respostas de erro:

CódigoMensagemSituação
400"Arquivo não encontrado"ID inexistente, de outro tenant, ou já excluído
400"Arquivo ainda referenciado por um Cadastro (writeback)"Imagem de writeback (origin: "dw") com referências ativas em wb_record ou wb_record_history

Ciclo de vida e limpeza automática

Soft-delete e reaper diário

A exclusão via DELETE /files/:id é um soft-delete: o arquivo é marcado como excluído e some imediatamente das listagens e do billing (não entra mais nos snapshots diários de storage). O conteúdo físico no S3 é removido posteriomente pelo reaper diário, que varre os arquivos soft-deletados e os purga do storage — reclamando os bytes e encerrando qualquer cobrança residual.

Reservas de presign abandonadas

Chamadas a POST /files/presign que nunca forem seguidas de um PUT + POST /files/confirm criam entradas "fantasma" no banco (sem file_path). Essas entradas não aparecem nas listagens e são limpas automaticamente pelo mesmo processo de reaper, sem nenhuma ação do integrador.


Fluxo de upload grande (presign → PUT → confirm)

Exemplo em JavaScript para upload de um arquivo grande sem passar pelo backend:

js
const BASE = "https://api.horusbi.com.br/v1/tenant";
const headers = {
  Authorization: `Bearer ${TENANT_TOKEN}`,
  "Content-Type": "application/json",
};

// 1. Solicitar URL pré-assinada
const presignRes = await fetch(`${BASE}/files/presign`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    file_name: "dados-junho.parquet",
    mime: "application/octet-stream",
  }),
});
const { data: { id, uploadUrl } } = await presignRes.json();

// 2. PUT direto no S3 (sem passar pelo backend)
const fileBuffer = await fs.promises.readFile("./dados-junho.parquet");
await fetch(uploadUrl, {
  method: "PUT",
  headers: { "Content-Type": "application/octet-stream" },
  body: fileBuffer,
});

// 3. Confirmar o upload — file_size NÃO é enviado; o backend lê o tamanho do S3
const confirmRes = await fetch(`${BASE}/files/confirm`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    id,
    mime: "application/octet-stream",
  }),
});
const { data: fileDTO } = await confirmRes.json();
console.log("Arquivo criado:", fileDTO.url);

Erros comuns

CódigoSituação
400Validação do body falhou, arquivo não encontrado, ou guard de writeback ativo
401Token ausente ou inválido
404Arquivo não encontrado (em GET /files/:id)
500Erro interno (ex.: falha no S3 ou banco)