14 - Upload de archivos al servidor con Node.js y el módulo 'formidable'


Una actividad muy común en un sitio web es poder enviar desde un cliente (navegador) un archivo para que sea almacenado en el servidor.

Como Node.js es de bastante bajo nivel el implementar todo el código para procesar los datos que llegan desde un formulario html que adjunta uno o mas archivos es bastante complejo vamos a utilizar un módulo desarrollado por la 'comunidad de Node.js' que nos simplifica esta tarea.

Tengamos en cuenta que el módulo que utilizaremos disponemos de todo el código fuente por si necesitamos adaptarlo a una situación particular.

El módulo más famoso para el upload de archivos a un servidor Node.js se llama 'formidable', podemos seguir y consultar las nuevas características aquí

Problema

Confeccionar una aplicación que permita subir fotos al servidor. Debe implementar un formulario para la selección de la foto. Listar todos los archivos subidos al servidor.

Crearemos un directorio llamado ejercicio16. Dentro de este crear el archivo 'ejercicio16.js' que contendrá nuestro programa Node.js.

Dentro de la carpeta ejercicio16 crear una subcarpeta llamada 'public'.

En la carpeta public crear dos archivos html: index.html y formulario.html.

Crear dentro de la carpeta public una subcarpeta llamada upload donde almacenaremos los archivos que suben los usuarios.

Ahora que ya creamos las carpetas de nuestro proyecto instalaremos el módulo 'formidable' que nos facilitará la codificación del upload de archivos:

npm install formidable

Luego de esto ya tenemos creado la carpeta 'node_modules' y dentro de esta la subcarpeta 'formidable' con todo el código fuente de nuestro módulo.

Las páginas HTML de nuestro proyecto son:

index.html
<!doctype html>
<html>
<head>
  <title>Prueba</title>
</head>
<body>
   <a href="formulario.html">Upload de foto</a></p>
   <a href="listadofotos">Listado de fotos</a></p>
</body>
</html>
formulario.html
<!doctype html>
<html>
<head>
</head>
<body>
  <form method="post" action="/subir" enctype="multipart/form-data">
    Seleccione la foto:
    <input type="file" name="foto1">
    <br>
    <input type="submit" value="confirmar">
  </form>
</body>
</html>

Nuestro programa en Node.js completo lo tenemos en el archivo:

ejercicio16.js
const http=require('http');
const url=require('url');
const fs=require('fs');
const formidable=require('formidable');

const mime = {
   'html' : 'text/html',
   'css'  : 'text/css',
   'jpg'  : 'image/jpg',
   'ico'  : 'image/x-icon',
   'mp3'  :	'audio/mpeg3',
   'mp4'  : 'video/mp4'
};

const servidor=http.createServer((pedido,respuesta) => {
  const objetourl = url.parse(pedido.url);
  let camino='public'+objetourl.pathname;
  if (camino=='public/')
    camino='public/index.html';
  encaminar(pedido,respuesta,camino);
});

servidor.listen(8888);


function encaminar (pedido,respuesta,camino) {
  
  switch (camino) {
    case 'public/subir': {
      subir(pedido,respuesta);
      break;
    }	
    case 'public/listadofotos': {
      listar(respuesta);
      break;
    }			
    default : {  
      fs.stat(camino, error => {
        if (!error) {
            fs.readFile(camino,(error,contenido) => {
          if (error) {
            respuesta.writeHead(500, {'Content-Type': 'text/plain'});
            respuesta.write('Error interno');
            respuesta.end();					
          } else {
            const vec = camino.split('.');
            const extension=vec[vec.length-1];
            const mimearchivo=mime[extension];
            respuesta.writeHead(200, {'Content-Type': mimearchivo});
            respuesta.write(contenido);
            respuesta.end();
          }
        });
        } else {
          respuesta.writeHead(404, {'Content-Type': 'text/html'});
          respuesta.write('<!doctype html><html><head></head><body>Recurso inexistente</body></html>');		
          respuesta.end();
        }
      });	
    }
  }	
}


function subir(pedido,respuesta){

  const entrada=new formidable.IncomingForm();
  entrada.uploadDir='upload';
  entrada.parse(pedido);
    entrada.on('fileBegin',(field, file) => {
        file.path = "./public/upload/"+file.name;
    });	
    entrada.on('end', function(){
    respuesta.writeHead(200, {'Content-Type': 'text/html'});
    respuesta.write('<!doctype html><html><head></head><body>'+
                    'Archivo subido<br><a href="index.html">Retornar</a></body></html>');		
    respuesta.end();
    });	
}

function listar(respuesta) {
  fs.readdir('./public/upload/',(error,archivos) => {
    let fotos='';
    for(let x=0;x<archivos.length;x++) {
      fotos += `<img src="upload/${archivos[x]}"><br>`;
    }
    respuesta.writeHead(200, {'Content-Type': 'text/html'});
    respuesta.write(`<!doctype html><html><head></head>
                     <body>${fotos}<a href="index.html">
                     Retornar</a></body></html>`);
    respuesta.end();	  
  });	
}


console.log('Servidor web iniciado');

Desde el menú de opciones cuando llamamos a localhost:8888 se carga la página estática 'index.html':

index.html node.js

Seleccionando la primer opción nos aparece la página estática 'formulario.html' donde podemos seleccionar un archivo del disco duro para subirlo al servidor:

formulario.html node.js

Cuando seleccionamos un archivo hemos indicado en la página html que pase en la propiedad action el valor '/subir' para poder capturar dicha ruta en el servidor. Por otro lado es importante iniciar la propiedad enctype indicando que el formulario adjunta archivos:

  <form method="post" action="/subir" enctype="multipart/form-data">

Veamos ahora que hemos hecho en nuestro programa en Node.js, primero requerimos el módulo 'formidable' para poder utilizarlo:

const formidable=require('formidable');

En la función encaminar capturamos la ruta que corresponde a /subir y llamamos a la función subir para implementar el upload:

function encaminar (pedido,respuesta,camino) { 
  switch (camino) {
    case 'public/subir': {
      subir(pedido,respuesta);
      break;
    }	

En la función subir está toda la lógica que tenemos que implementar para el upload:

function subir(pedido,respuesta){

Creamos un objeto llamando al método IncomingForm:

    const entrada=new formidable.IncomingForm();

Definimos el path donde se almacenará el archivo en el servidor (recordemos que creamos una carpeta llamada upload en la carpeta public):

    entrada.uploadDir='upload';

Llamamos al método parse pasando 'pedido' donde se encuentran los datos del archivo adjunto para ser procesado:

    entrada.parse(pedido);

El evento fileBeing se dispara cuando el archivo se está por grabar en el servidor, aquí definimos el path y nombre del archivo a grabar. Para nuestro problema lo grabamos en la carpeta upload que se encuentra en la carpeta public y el nombre de archivo utilizamos el nombre original que tiene en el cliente y lo podemos sacar del parámetro file:

    entrada.on('fileBegin',(field, file) => {
        file.path = "./public/upload/"+file.name;
    });	

El evento end se dispara cuando el archivo ya se almacenó en el servidor, aquí generamos dinámicamente una página HTML informando de tal situación al visitante:

    entrada.on('end', function(){
    respuesta.writeHead(200, {'Content-Type': 'text/html'});
    respuesta.write('<!doctype html><html><head></head><body>'+
                    'Archivo subido<br><a href="index.html">Retornar</a></body></html>');		
    respuesta.end();
    });	

upload Node.js

Ahora veamos como imprimir todas las fotos que hay almacenadas en la carpeta upload en el servidor.

Cuando se selecciona la segunda opción desde la página 'index.html' se llama:

   <a href="listadofotos">Listado de fotos</a></p>

En nuestro programa Node.js capturamos dicho enlace en el switch donde llamamos a la función listar:

        case 'public/listadofotos': {
            listar(respuesta);
            break;
        }            

La función listar utiliza el módulo 'fs' llamando a la función readdir que lee todos los archivos de un directorio y luego mediante una función anónima recibimos un error y un vector con todos los archivos de dicho directorio:

function listar(respuesta) {
  fs.readdir('./public/upload/',(error,archivos) => {
    let fotos='';
    for(let x=0;x<archivos.length;x++) {
      fotos += `<img src="upload/${archivos[x]}"><br>`;
    }
    respuesta.writeHead(200, {'Content-Type': 'text/html'});
    respuesta.write(`<!doctype html><html><head></head>
                     <body>${fotos}<a href="index.html">
                     Retornar</a></body></html>`);
    respuesta.end();	  
  });	
}

Como podemos ver generamos una página dinámica con las etiquetas HTML 'img' que hacen referencia a todos los nombres de archivos almacenados en la carpeta upload.

Este proyecto lo puede descargar en un zip con todos los archivos desde este enlace : ejercicio16

Retornar