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('node:http')
const fs = require('node: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 url = new URL('http://localhost:8888' + pedido.url)
  let camino = 'public' + url.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 form = new formidable.IncomingForm()
  form.parse(pedido, function (err, fields, files) {
    let path = files.foto1.filepath
    let nuevoPath = './public/upload/' + files.foto1.originalFilename
    fs.rename(path, nuevoPath, function (error) {
      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 form = new formidable.IncomingForm()

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

  form.parse(pedido, function (err, fields, files) {
    let path = files.foto1.filepath
    let nuevoPath = './public/upload/' + files.foto1.originalFilename
    fs.rename(path, nuevoPath, function (error) {
      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()
    })
  })

El archivo se guarda en una carpteta temporal y podemos obtener el path con la sintaxis:

    let path = files.foto1.filepath

En una segunda variable guardamos el path donde queremos mover el archivo que se acaba de subir al servidor:

    let nuevoPath = './public/upload/' + files.foto1.originalFilename

Mediante el objeto fs y llamando al método rename procedemos a mover el archivo:

    fs.rename(path, nuevoPath, function (error) {
      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