Descarga de archivos (/file)

4 min de lectura

Descarga de archivos

El endpoint /file sirve archivos binarios generados por una función de PostgreSQL — sin base64 dentro de JSON-RPC. Un único archivo devuelto se envía tal cual; varios archivos se empaquetan al vuelo en un archivo ZIP.

POST /{prefix}/{database}/file

La autenticación, el cambio de rol (SET LOCAL ROLE), el rate limiting y el límite de tamaño de la petición son idénticos a /jsonrpc. El llamador necesita EXECUTE sobre la función.

Petición

{
  "method": "api.export_documents",
  "params": { "count": 3 },
  "options": { "filename": "export.zip", "force_zip": false, "compression_level": 6 },
  "idempotencyKey": "optional"
}
CampoDescripción
methodObligatorio. schema.function, invocada como fn(params::jsonb).
paramsSe pasa a la función como jsonb (por defecto {}).
options.filenameNombre del archivo/ZIP descargado. Por defecto: el nombre propio del archivo, o export-YYYYMMDD-HHMMSS.zip.
options.force_zipDevolver siempre un ZIP, incluso para un solo archivo.
options.compression_level0 = solo almacenar, 1–9; por defecto 6.

Contrato con la base de datos

La función recibe un argumento jsonb y devuelve un conjunto de filas. path y content son obligatorios; mime_type y store_only son opcionales.

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 debe ser una ruta relativa simple que se pueda extraer con seguridad en cualquier sistema operativo. Una ruta se rechaza (la petición falla con 500 y el problema se registra en el log) si:
    • está vacía, supera los 512 bytes, no es UTF-8 válido o contiene caracteres de control;
    • es absoluta (/…, C:…) o contiene una barra invertida;
    • tiene un segmento vacío, . o ..;
    • tiene un segmento que termina en punto o espacio (Windows los elimina, así que ".. " se convertiría en ..);
    • contiene : (flujo de datos alternativo de NTFS) o alguno de < > " | ? *;
    • usa como segmento un nombre de dispositivo reservado de Windows, con o sin extensión (CON, PRN, AUX, NUL, COM0–COM9, LPT0–LPT9);
    • colisiona con otra ruta de la misma respuesta: los nombres se comparan sin distinguir mayúsculas y minúsculas (a.txt y A.TXT chocan en macOS/Windows), y un archivo no puede ser también un directorio (a y a/b).
  • content NULL es un archivo vacío.
  • mime_type se usa solo si es un tipo de medio bien formado; en caso contrario se deduce de la extensión (alternativa application/octet-stream).
  • store_only = true almacena la entrada del ZIP sin comprimir (necesario p. ej. para el archivo mimetype de EPUB).

Respuesta

Filasforce_zipResultado
0cualquiera404 (error JSON); la transacción se revierte, por lo que no se conserva nada de lo que hizo la función y no se consume un idempotencyKey
1falseel propio archivo, con su tipo de medio
1trueZIP con una entrada
≥ 2cualquieraZIP (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

El nombre de descarga (options.filename o el nombre del propio archivo) se sanea: los separadores de ruta, las comillas y los caracteres reservados se convierten en _, y se eliminan los caracteres de control, de anulación bidireccional, de ancho cero y separadores de línea, de modo que un nombre no pueda disfrazarse de otro tipo de archivo.

Los errores son siempre JSON, nunca binarios: 400 petición no válida, 401 no autenticado, 403 sin permiso, 404 función desconocida o sin filas, 413 la respuesta supera FILE_MAX_BYTES / FILE_MAX_ENTRIES, 500 la función falló o devolvió una estructura no válida.

Seguro por construcción

La base de datos controla únicamente los nombres de archivo, los contenidos y los tipos de medio — nunca las cabeceras HTTP. Las respuestas son siempre Content-Disposition: attachment con X-Content-Type-Options: nosniff, Content-Security-Policy: sandbox y Cache-Control: no-store, de modo que una función que devuelva text/html o SVG no pueda ejecutar scripts en el origen de la API. Los nombres de archivo se sanean. El resultado completo se almacena en búfer en memoria: el límite real por petición es FILE_MAX_BYTES más la fila individual más grande, porque el controlador de la base de datos lee una fila completa antes de que se pueda comprobar el límite — tenga en cuenta el número de descargas simultáneas. El ZIP en sí se transmite en streaming.

Descubrimiento

Una función que devuelve un conjunto, declarada con RETURNS TABLE (…) o parámetros OUT que incluyan las columnas path y content, es reportada por capabilities con "kind": "file" y el endpoint /file. La especificación OpenAPI documenta una operación real POST /{prefix}/{database}/file (solo para roles que pueden ejecutar una función de archivo); las funciones de archivo no se exponen como herramientas MCP.