Download de arquivos (/file)

4 min de leitura

Download de arquivos

O endpoint /file serve arquivos binários gerados por uma função PostgreSQL — sem base64 dentro do JSON-RPC. Um único arquivo retornado é enviado como está; vários arquivos são empacotados em um arquivo ZIP em tempo real.

POST /{prefixo}/{banco_de_dados}/file

Autenticação, troca de papel (SET LOCAL ROLE), rate limiting e limite de tamanho da requisição são idênticos aos de /jsonrpc. O chamador precisa de EXECUTE na função.

Requisição

{
  "method": "api.export_documents",
  "params": { "count": 3 },
  "options": { "filename": "export.zip", "force_zip": false, "compression_level": 6 },
  "idempotencyKey": "optional"
}
CampoDescrição
methodObrigatório. schema.function, chamada como fn(params::jsonb).
paramsPassado à função como jsonb (padrão {}).
options.filenameNome do arquivo/ZIP baixado. Padrão: o nome do próprio arquivo, ou export-YYYYMMDD-HHMMSS.zip.
options.force_zipSempre retorna um ZIP, mesmo para um único arquivo.
options.compression_level0 = apenas armazenar, 1–9; padrão 6.

Contrato com o banco de dados

A função recebe um argumento jsonb e retorna um conjunto de linhas. path e content são obrigatórios; mime_type e store_only são opcionais.

CREATE FUNCTION api.export_documents(params jsonb)
RETURNS TABLE (path text, content bytea, mime_type text, store_only boolean)
LANGUAGE sql AS $$
  SELECT 'report.csv', convert_to('a,b' || E'\n' || '1,2', 'UTF8'), 'text/csv', false;
$$;
  • path deve ser um caminho relativo simples que possa ser extraído com segurança em qualquer sistema operacional. Um caminho é rejeitado (a requisição falha com 500 e o problema é registrado no log) se:
    • for vazio, tiver mais de 512 bytes, não for UTF-8 válido ou contiver caracteres de controle;
    • for absoluto (/…, C:…) ou contiver uma barra invertida;
    • tiver um segmento vazio, . ou ..;
    • tiver um segmento terminando em ponto ou espaço (o Windows os remove, então ".. " viraria ..);
    • contiver : (alternate data stream do NTFS) ou algum de < > " | ? *;
    • usar como segmento um nome de dispositivo reservado do Windows, com ou sem extensão (CON, PRN, AUX, NUL, COM0–COM9, LPT0–LPT9);
    • colidir com outro caminho na mesma resposta: os nomes são comparados sem diferenciar maiúsculas de minúsculas (a.txt e A.TXT entram em conflito no macOS/Windows), e um arquivo não pode ser também um diretório (a e a/b).
  • content NULL é um arquivo vazio.
  • mime_type é usado apenas se for um media type bem formado; caso contrário, é deduzido da extensão (fallback application/octet-stream).
  • store_only = true armazena a entrada do ZIP sem compressão (necessário, por exemplo, para o arquivo mimetype do EPUB).

Resposta

Linhasforce_zipResultado
0qualquer404 (erro JSON); a transação sofre rollback, portanto nada do que a função fez é persistido e um idempotencyKey não é consumido
1falseo próprio arquivo, com seu media type
1trueZIP com uma entrada
≥ 2qualquerZIP (application/zip)
curl -X POST http://localhost:8080/db/my_database/file \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"method":"api.export_documents","params":{"count":3}}' -OJ

O nome de download (options.filename ou o nome do próprio arquivo) é sanitizado: separadores de caminho, aspas e caracteres reservados viram _, e caracteres de controle, de substituição bidirecional, de largura zero e separadores de linha são removidos, de modo que um nome não possa ser disfarçado de outro tipo de arquivo.

Os erros são sempre JSON, nunca binário: 400 requisição inválida, 401 não autenticado, 403 sem permissão, 404 função desconhecida ou nenhuma linha, 413 a resposta excede FILE_MAX_BYTES / FILE_MAX_ENTRIES, 500 a função falhou ou retornou uma estrutura inválida.

Seguro por construção

O banco de dados controla apenas nomes de arquivos, conteúdos e media types — nunca cabeçalhos HTTP. As respostas são sempre Content-Disposition: attachment com X-Content-Type-Options: nosniff, Content-Security-Policy: sandbox e Cache-Control: no-store, de modo que uma função que retorna text/html ou SVG não pode executar script na origem da API. Os nomes de arquivo são sanitizados. O resultado inteiro é mantido em memória: o limite real por requisição é FILE_MAX_BYTES mais a maior linha individual, porque o driver do banco de dados lê uma linha por completo antes que o limite possa ser verificado — tenha em mente o número de downloads simultâneos. O ZIP em si é transmitido por streaming.

Descoberta

Uma função set-returning declarada com RETURNS TABLE (…) ou parâmetros OUT que inclua as colunas path e content é reportada por capabilities com "kind": "file" e endpoint /file. A especificação OpenAPI documenta uma operação real POST /{prefixo}/{banco_de_dados}/file (apenas para papéis que podem executar uma função de arquivo); as funções de arquivo não são expostas como ferramentas MCP.