Descarga de archivos (/file)
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}/fileLa 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"
}| Campo | Descripción |
|---|---|
method | Obligatorio. schema.function, invocada como fn(params::jsonb). |
params | Se pasa a la función como jsonb (por defecto {}). |
options.filename | Nombre del archivo/ZIP descargado. Por defecto: el nombre propio del archivo, o export-YYYYMMDD-HHMMSS.zip. |
options.force_zip | Devolver siempre un ZIP, incluso para un solo archivo. |
options.compression_level | 0 = 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;
$$;pathdebe 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.txtyA.TXTchocan en macOS/Windows), y un archivo no puede ser también un directorio (aya/b).
contentNULLes un archivo vacío.mime_typese usa solo si es un tipo de medio bien formado; en caso contrario se deduce de la extensión (alternativaapplication/octet-stream).store_only = truealmacena la entrada del ZIP sin comprimir (necesario p. ej. para el archivomimetypede EPUB).
Respuesta
| Filas | force_zip | Resultado |
|---|---|---|
| 0 | cualquiera | 404 (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 |
| 1 | false | el propio archivo, con su tipo de medio |
| 1 | true | ZIP con una entrada |
| ≥ 2 | cualquiera | ZIP (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}}' -OJEl 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.