Saltar al contenido principal

Subida de archivos

Enviar archivos a la API y guardarlos en una carpeta o en la base de datos.

Introducción​

Cuando necesitamos enviar archivos a los servicios de la API REST tenemos varias formas de llevar a cabo este desarrollo en Netuno.

Normalmente los archivos se envían en el cuerpo de la petición HTTP estructurados en el formato multipart, es el procedimiento más común en el frontend, por ejemplo cuando rellenamos un formulario y hay un campo para elegir un archivo.

Como alternativa podemos enviar los bytes de los archivos codificados en Base64 como valor en un objeto JSON, cuando queremos una API REST 100% JSON.

Archivos​

Mediante _req.file podemos pasar el nombre del campo y así obtener el objeto del archivo que se ha enviado, vea cómo:

const miArchivo = _req.file("archivo");

Es decir, _req tiene todos los datos que recibimos en el cuerpo de la petición HTTP.

Y mediante el método file permite obtener el objeto que representa el archivo que se ha enviado.

El valor obtenido podrá ser null en caso de que no haya ningún archivo.

Imágenes​

Cuando recibimos imágenes es habitual que necesitemos redimensionarlas, para garantizar uniformidad y reducir el espacio que ocupan las imágenes.

const miArchivo = _req.getFile("imagen");
if (miArchivo != null
&& (miArchivo.isExtension("jpeg")
|| miArchivo.isExtension("jpg")
|| miArchivo.isExtension("png")
)) {
const miImagen = _image.init(miArchivo)
.resize(200, 200)
.file(miArchivo.baseName() +".jpg", "jpeg");
_header.contentTypeJPG();
_out.copy(miImagen);
}

Con el recurso de programación low-code políglota Image, podemos manipular imágenes, recortar, redimensionar, crear nuevas, etc.

Cómo usar Base64 en JSON​

Para realizar la subida de un archivo en el JSON enviado al servicio, podemos definir una propiedad con el valor:

data:image/png;base64,...

Donde en ... sigue la secuencia de bytes del archivo codificados en Base64.

El tipo de archivo va a continuación de data, por ejemplo:

  • PNG: image/png.
  • JPG: image/jpeg
  • TXT: text/plain
  • PDF: application/pdf
  • XLSX: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

Vea el ejemplo de un archivo de una imagen PNG pequeña que se envía en el JSON a un servicio:

{
"archivo": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAFIGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpDb2xvclNwYWNlPSIxIgogICBleGlmOlBpeGVsWERpbWVuc2lvbj0iMyIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjMiCiAgIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiCiAgIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMyIKICAgdGlmZjpJbWFnZVdpZHRoPSIzIgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIgogICB0aWZmOlhSZXNvbHV0aW9uPSI3Mi8xIgogICB0aWZmOllSZXNvbHV0aW9uPSI3Mi8xIgogICB4bXA6TWV0YWRhdGFEYXRlPSIyMDI1LTEyLTI5VDE5OjEyOjI3WiIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjUtMTItMjlUMTk6MTI6MjdaIj4KICAgPHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHhtcE1NOmFjdGlvbj0icHJvZHVjZWQiCiAgICAgIHhtcE1NOnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IERlc2lnbmVyIDEuMTAuNSIKICAgICAgeG1wTU06d2hlbj0iMjAyNS0wMi0yM1QyMjoxNDozM1oiLz4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0icHJvZHVjZWQiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IDMuMC4xIgogICAgICBzdEV2dDp3aGVuPSIyMDI1LTEyLTI5VDE5OjEyOjI3WiIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+UGhcxwAAAYFpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAACiRdZG7SwNBEIe/JEqCRhS0sFAIEq3iG4I2ggmigkiIEXw1yZmHkMdxlyDBVrANKIg2vgr9C7QVrAVBUQSx1lbRRuWc84QEMbPMzre/3Rl2Z8EeSSsZvaYPMtm8Fh4PeObmFzzOZ9y004yL3qiiq6Oh0BRV7f0Wmxmvu81a1c/9a/XLcV0Bm0t4RFG1vPCE8NRqXjV5S7hFSUWXhU+EfZpcUPjG1GMWP5mctPjTZC0SDoK9SdiTrOBYBSspLSMsL8ebSReU3/uYL3HHs7MzEjvE29AJM04AD5OMEcRPP8My++lmgB5ZUSW/7yd/mpzkKjKrFNFYIUmKPD5RC1I9LjEhelxGmqLZ/7991RODA1Z1dwBqHw3jtROcm/BVMoyPA8P4OgTHA5xny/m5fRh6E71U1rx70LgOpxdlLbYNZxvQeq9GteiP5BC3JxLwcgwN89B8BXWLVs9+9zm6g8iafNUl7OxCl5xvXPoGe8Rn71E5kqUAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAySURBVAiZAScA2P8BJYrEogr//l3h5+qlBP4HCF3n/AAA/wP7WwLu7PKk+eHWAPzWyKSmfRbbIj23ZQAAAABJRU5ErkJggg=="
}

Podemos obtener el archivo enviado de la siguiente forma:

const miArchivo = _req.getFile("archivo");
if (miArchivo != null) {
_out.json(
_val.map()
.set("resultado", true)
.set("nombre", miArchivo.name())
.set("tamañoBytes", miArchivo.size())
.set("tamañoKB", miArchivo.sizeKB())
.set("tamañoMB", miArchivo.sizeMB())
.set("tamañoGB", miArchivo.sizeGB())
);
} else {
_out.json(
_val.map()
.set("resultado", false)
);
}

Ejemplo de la salida generada por el servicio anterior:

{
"nombre": "archivo.png",
"resultado": true,
"tamaño": 1849,
"tamañoKB": 1
}

Objeto​

Si el archivo está en una estructura interna del JSON dentro del subobjeto:

{
"persona": {
"archivo": "data:image/png;base64,..."
}
}

Se puede obtener de la siguiente forma:

const miArchivo = _req.getValues("persona").getFile("archivo");

Array​

Si el archivo está en una estructura interna del JSON dentro del array:

{
"archivos": [
"data:image/png;base64,..."
]
}

Se puede obtener de la siguiente forma:

const miArchivo = _req.getValues("archivos").getFile(0);

OpenAPI​

Netuno admite la integración con OpenAPI y utiliza la definición de OpenAPI para validar la petición HTTP.

Más sobre la OpenAPI en Netuno.

Con el tipo file, Netuno valida si el valor comienza con el formato de archivo en el JSON, por ejemplo:

  • data:image/png;base64,...

Para validar si se recibe un archivo en una propiedad del objeto JSON:

post.in.json
{
"summary": "Subida de archivo",
"description": "Servicio que recibe un archivo en el objeto JSON.",
"type": "object",
"properties": {
"archivo": {
"type": "file"
}
},
"required": [
"archivo"
]
}

Para validar si se recibe una lista de archivos en un array en el JSON:

post.in.json
{
"summary": "Subida de archivos",
"description": "Servicio que recibe una lista de archivos en un array en el JSON.",
"properties": {
"archivos": {
"type": "array",
"items": {
"type": "file"
}
}
},
"required": [
"archivos"
]
}

Guardar el archivo​

Normalmente, cuando recibimos un archivo en el servicio necesitamos guardarlo, como por ejemplo:

  • Base de datos: Archivo relacionado con un registro, como el de un usuario.
  • Storage: Carpeta interna de la aplicación que sirve para gestionar archivos en general.
  • Carpeta: Colocar el archivo en una carpeta cualquiera del sistema.

Base de datos​

En la gestión de los campos de los formularios, Netuno admite el tipo image y file, siendo:

  • Image: Archivos de imagen en general, JPG y PNG.
  • File: Cualquier tipo de archivo.

Imagine que tiene un campo de Imagen en el formulario Producto; podemos guardar el archivo enviado mediante la subida de la siguiente forma:

Ejemplo utilizando DB Form:​

Observe el uso de _db.form:

services/producto/imagen/post.js
const productoUid = _req.getString("uid");
const productoImagen = _req.getFile("imagen");

if (productoImagen == null) {
_header.status(400);
_out.json(
_val.map()
.set("resultado", false)
.set("error", "imagen-obligatoria")
);
_exec.stop();
}

const dbResultado = _db.form("producto")
.set("imagen", productoImagen)
.where(_db.where("uid").equals(productoUid))
.update();

if (dbResultado.getInt("producto") == 1) {
_out.json(
_val.map()
.set("resultado", true)
);
} else {
_header.status(404);
_out.json(
_val.map()
.set("resultado", false)
.set("error", "uid-producto-no-existe")
);
}

Ejemplo utilizando el update clásico:​

Observe el uso de _db.update:

services/producto/imagen/post.js
const productoUid = _req.getString("uid");
const productoImagen = _req.getFile("imagen");

if (productoImagen == null) {
_header.status(400);
_out.json(
_val.map()
.set("resultado", false)
.set("error", "imagen-obligatoria")
);
_exec.stop();
}

const dbResultado = _db.update(
"producto",
productoUid,
_val.map()
.set("imagen", productoImagen)
);

if (dbResultado == 1) {
_out.json(
_val.map()
.set("resultado", true)
);
} else {
_header.status(404);
_out.json(
_val.map()
.set("resultado", false)
.set("error", "uid-producto-no-existe")
);
}

Descarga del archivo de la base de datos​

Para mostrar el archivo almacenado debemos crear un servicio que proporcione los bytes del archivo.

Cabe señalar que, en los archivos guardados en la base de datos, como en el ejemplo anterior, el archivo en sí se coloca en el storage de la aplicación y en la base de datos solo se almacena el nombre final del archivo.

El storage de la aplicación es una carpeta destinada a almacenar y organizar estructuras de archivos genéricos.

Por tanto, en el ejemplo anterior los archivos de base de datos del formulario Producto y del campo Imagen se almacenan en:

  • storage/database/producto/imagen/*

Netuno gestiona automáticamente el nombre del archivo almacenado para evitar nombres de archivos repetidos.

Para obtener el archivo, se puede crear un servicio que muestre el archivo de imagen del producto, por ejemplo:

services/producto/imagen/get.js
const productoUid = _req.getString("uid");

// Formulario DB:
const dbProducto = _db.form("producto")
.where(_db.where("uid").equals(productoUid))
.first();
// Alternativa con DB clásico:
// const dbProducto = _db.get("producto", productoUid)

if (dbProducto == null) {
_header.status(404);
_out.json(
_val.map()
.set("resultado", false)
.set("error", "producto-no-encontrado")
);
_exec.stop();
}

const dbProductoImagenNombre = dbProducto.getString("imagen");

const storageProductoImagenArchivo = _storage.database(
"producto",
"imagen",
dbProductoImagenNombre
);

if (storageProductoImagenArchivo.extension() == "jpg" || storageProductoImagenArchivo.extension() == "jpeg") {
_header.contentTypeJPG();
} else {
_header.contentTypePNG();
}

_header.noCache();

_out.copy(storageProductoImagenArchivo.inputStream());

Observe que el recurso _storage (Storage) se utiliza para obtener el archivo:

_storage.database(
"producto", // Nombre del formulario/tabla.
"imagen", // Nombre del campo/columna.
dbProductoImagenNombre // Nombre del archivo.
);

En la base de datos solo se almacena el nombre del archivo, y mediante _storage.database podemos obtener el archivo en sí.

Storage​

Podemos guardar un archivo directamente en el storage de la aplicación.

Para no mezclarlo con los archivos de base de datos, debemos utilizar la carpeta filesystem, que tiene la finalidad de almacenar archivos genéricos que no están asociados a la base de datos.

Para recibir el archivo, en este ejemplo se muestra cómo un archivo enviado en el servicio POST se almacena directamente en el storage:

services/archivo/post.js
const archivo = _req.getFile("archivo");
_storage.filesystem("servidor", "subidas").ensurePath();
archivo.save(_storage.filesystem("servidor", "subidas", archivo.name()));

El archivo se almacena en la carpeta storage de la aplicación, en:

  • storage/filesystem/servidor/subidas/*

El recurso Storage se utiliza para obtener la ruta de carpeta y de archivo:

_storage.filesystem(
"servidor", // Nombre de la subcarpeta en el filesystem.
"subidas", // Nombre o ruta de la subcarpeta interna.
archivo.name() // Nombre final del archivo.
);

Vea más sobre la documentación de Storage.

Para descargar el archivo, en este ejemplo se muestra cómo se pueden obtener los bytes del archivo mediante el servicio GET:

services/archivo/get.js
const archivoNombre = _req.getString("archivo");

const archivo = _storage.filesystem(
"servidor",
"subidas",
archivoNombre
).file();

if (archivo.exists() == false) {
_header.status(404);
_out.json(
_val.map()
.set("resultado", false)
.set("error", "archivo-no-existe")
);
_exec.stop();
}

_header.downloadFile(archivo.name());
_out.copy(archivo.input());

Carpeta​

Podemos guardar un archivo directamente en cualquier carpeta del ordenador.

Hay dos recursos que son muy útiles en estos casos:

  • _app - Podemos obtener cualquier carpeta o archivo de la aplicación.
  • _os - Podemos obtener cualquier carpeta o archivo del ordenador.

Vea la documentación de los respectivos recursos App y OS, ambos recursos tienen los métodos folder y file, que permiten indicar cuál es la ruta de la carpeta o del archivo, respectivamente.

Para recibir el archivo, en este ejemplo se muestra cómo un archivo enviado en el servicio POST se almacena directamente en una carpeta de la aplicación:

services/archivo/post.js
const archivo = _req.getFile("archivo");
const carpeta = _app.folder("temporal");
if (carpeta.exists() == false) {
carpeta.mkdir();
}
archivo.save(_app.file(`temporal/${archivo.name()}`));

El código anterior creará la carpeta temporal dentro de la aplicación y guardará el archivo recibido dentro de esta carpeta.

Para descargar el archivo, en este ejemplo se muestra cómo se pueden obtener los bytes del archivo mediante el servicio GET:

services/archivo/get.js
const archivoNombre = _req.getString("archivo");
const archivo = _app.file(`temporal/${archivoNombre}`);

if (archivo.exists() == false) {
_header.status(404);
_out.json(
_val.map()
.set("resultado", false)
.set("error", "archivo-no-existe")
);
_exec.stop();
}

_header.downloadFile(archivo.name());
_out.copy(archivo.input());

Conclusión​

Como se puede ver, podemos integrar la subida de archivos en los servicios de la API REST, incluso integrada con OpenAPI.

Podemos guardar el archivo en la base de datos, dentro de la carpeta storage de la aplicación o en cualquier carpeta del ordenador.

Vea la documentación de los recursos utilizados en este tutorial que permiten manipular los archivos:

En la base de datos podemos guardar imágenes en campos de tipo image, y podemos guardar cualquier tipo de archivo en campos de tipo file; cómo guardamos u obtenemos archivos es igual en cualquier caso, ya sea imagen o archivo genérico.