Skip to content

1 Archivos

Introducción

La subida de ficheros a un servidor a través de Node JS es una de las operaciones más complejas que existen, ya que hay que recibir los datos en binario, comprobar el tipo de archivo y escribirlo con el método writeFile.

import { createServer } from 'http';
import { writeFile } from 'fs';

const server = createServer((req, res) => {
    if (req.method === 'POST' && req.url === '/upload-image') {
        let data = [];
        const mimetype = req.headers['content-type'];

        if (!mimetype.startsWith('image/')) {
            res.writeHead(400, { 'Content-Type': 'text/plain' });
            res.end('Solo se admiten imágenes');
            return;
        }

        req.on('data', (chunk) => {
            data.push(chunk);
        });

        req.on('end', () => {
            const buffer = Buffer.concat(data).toString("base64");
            console.log(buffer)
            const filename = `image-${Date.now()}.${mimetype.split('/')[1]}`;

            writeFile(`./uploads/${filename}`, buffer, (err) => {
                if (err) {
                    res.writeHead(500, { 'Content-Type': 'text/plain' });
                    res.end('Error al subir la imagen');
                } else {
                    res.writeHead(200, { 'Content-Type': 'text/plain' });
                    res.end(`Imagen subida exitosamente: ${filename}`);
                }
            });
        });
    }
});

server.listen(3000, () => console.log('Server active'));

Al igual que con express, existen diferentes librerías para realizar la subida de ficheros, como multer.

Multer

Multer es un middleware para manejar multipart/form-data, que es lo principal para la subida de ficheros. Al ser una librería, primero habría que instalarla:

npm install multer

Una vez instalada, será necesario crear un objeto para su configuración, especificando el destino (destination) y el nombre del fichero subido (filename).

La propiedad destination es una función que toma el request, el fichero subido file, y una función callback que será llamada con un objeto de error y la ruta de la carpeta de destino como parámetros. La propiedad filename toma los mismos parámetros que la propiedad destination, pero el callback en lugar de indicar la ruta de la carpeta de destino, indica el nombre del fichero. Para crear este objeto, utilizaremos el método diskStorage de la librería multer:

const multer = require('multer');

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, './uploads');
  },
  filename: (req, file, cb) => {
    cb(null, file.fieldname + '-' + Date.now());
  }
});

const upload = multer({ storage });

La constante upload es el middleware que será usado más tarde para manejar la subida de ficheros. Se usa la función multer para poder crear dicho middleware.

Limites

Podemos indicar un tamaño máximo de subida especificando la propiedad limits de la función multer la cantidad de tamaño máximo permitido en bytes en la subida:

const upload = multer({
  storage,
  limits: {
    fileSize: 1000000 // 1MB
  }
});

Validar tipos

Otra consideración importante a la hora de realizar la subida de ficheros, es el tipo de fichero a subir. En ocasiones, nos interesa subir únicamente un tipo de fichero, por el contrario, no se realizará ninguna subida. Para ello, debemos especificar la propiedad fileFilter del método multer, que es una función que recibe tres parámetros: el request, el file y un callback con el error y el éxito de la operación (verdadero o falso) como parámetros:

const upload = multer({
  storage,
  limits: {
    fileSize: 1000000 // 1MB
  },
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];

    if (!allowedTypes.includes(file.mimetype)) {
      const error = new Error('Invalid file type');
      error.code = 'INVALID_FILE_TYPE';
      return cb(error, false);
    }

    cb(null, true);
  }
});

Subida de ficheros

Una vez realizada la creación del middleware, dicha función será usada en las rutas que nos compete, haciendo uso de diferentes modos:

  • single(name): Permite la subida de un único fichero, donde name es el nombre de acceso al fichero.
  • array(name, size): Permita la subida de varios ficheros, donde name es el nombre de acceso a los ficheros y size el número de fichero permitido a subir.
  • fields(): Permite una subida de múltiple de ficheros pero con diferentes nombre de acceso y tamaños, indicamos como un array de objectos.
  • none(): No permite la subida de ningún fichero.
app.post('/profile', upload.single('avatar'), function (req, res, next) {
  // req.file is the `avatar` file
  // req.body will hold the text fields, if there were any
})

app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) {
  // req.files is array of `photos` files
  // req.body will contain the text fields, if there were any
})

const cpUpload = upload.fields([
    { name: 'avatar', maxCount: 1 }, 
    { name: 'gallery', maxCount: 8 }
])
app.post('/cool-profile', cpUpload, function (req, res, next) {
  // req.files is an object (String -> Array) where fieldname is the key, and the value is array of files
  //
  // e.g.
  //  req.files['avatar'][0] -> File
  //  req.files['gallery'] -> Array
  //
  // req.body will contain the text fields, if there were any
})

app.post('/profile', upload.none(), function (req, res, next) {
  // req.body contains the text fields
})

Desde el lado del front, será necesario tener acceso a un formulario que permite la subida de fichero, indicando un input con el mismo nombre que le hayamos indicado en la funciones anteriores:

<form action="/stats" enctype="multipart/form-data" method="post">
  <div class="form-group">
    <input type="file" class="form-control-file" name="uploaded_file">
    <input type="text" class="form-control" placeholder="Number of speakers" name="nspeakers">
    <input type="submit" value="Get me the stats!" class="btn btn-default">            
  </div>
</form>

También se puede realizar haciendo uso de fetch:

var input = document.querySelector('input[type="file"]')

var data = new FormData()
data.append('file', input.files[0])
data.append('user', 'hubot')

fetch('/avatars', {
  method: 'POST',
  body: data
})

¡Importante!

La subida de ficheros debe realizarse siempre desde un método POST, ya que subir una fichero significar crear un nuevo fichero en el servidor. Un fichero, no puede ser ni actualizado, ni eliminado. Para eliminar un fichero, por ejemplo, no requires la imagen en sí, simplemente se le solicitaría el nombre al usuario y sería eliminada en el método DELETE, y a la hora de actualizar una imagen, se le solicitaría el nombre de la antigua imagen, se eliminaría, subiendo la nueva imagen con dicho nombre, pero para ello se requiere POST ya que la nueva imagen debe ser subida.

Configuración avanzada

Veamos un ejemplo más avanzado:

const express = require('express');
const multer = require('multer');
const mongoose = require('mongoose');

// Configuración de Multer
const storage = multer.diskStorage({
  destination: './uploads/',
  filename: (req, file, cb) => {
    cb(null, `${Date.now()}-${file.originalname}`);
  }
});

const upload = multer({ storage });

// Conexión a MongoDB
mongoose.connect('mongodb://localhost:27017/mydb', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

// Esquema de usuario
const userSchema = new mongoose.Schema({
  nombre: String,
  edad: Number,
  email: String,
  imagenPerfil: String,
});

// Modelo de usuario
const User = mongoose.model('User', userSchema);

// Ruta POST para subir usuario
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/uploads', express.static('uploads'));

app.post('/usuarios', upload.single('imagenPerfil'), async (req, res) => {
  const { nombre, edad, email } = req.body;
  const imagenPerfil = req.file.filename;

  const user = new User({
    nombre,
    edad,
    email,
    imagenPerfil,
  });

  try {
    await user.save();
    res.status(201).json({ message: 'Usuario creado exitosamente' });
  } catch (error) {
    res.status(500).json({ message: 'Error al crear usuario', error });
  }
});

app.listen(3000);

En el ejemplo anterior, nuestro servidor crea un nuevo usuario en MongoDB, subiendo la imagen al servidor.

La línea app.use(express.urlencoded({ extended: true })) es un middleware de Express que se encarga de parsear el cuerpo de las solicitudes HTTP con formato URL-encoded. Esto significa que convierte los datos enviados en el cuerpo de la solicitud en un objeto JavaScript que puede ser accedido en la ruta.

La línea app.use('/uploads', express.static('uploads')); en tu código de Node.js con Express realiza dos funciones:

  1. Define una ruta estática:

  2. Especifica una ruta virtual (/uploads) para acceder a archivos estáticos.

  3. Esta ruta mapea a la carpeta uploads en tu sistema de archivos.

  4. Utiliza el middleware express.static:

  5. Este middleware sirve archivos estáticos desde la carpeta especificada.

  6. Permite al servidor web entregar archivos como imágenes, CSS, JavaScript o - cualquier otro archivo que se encuentre dentro del directorio configurado. Por lo que no será necesario, crear una ruta con el método GET para acceder a dichos ficheros.