Express
Tutorial Express: Almacenamiento de archivos
Aprende estrategias de almacenamiento, organización y gestión eficiente de archivos en Express.js para aplicaciones escalables y seguras.
Aprende Express y certifícateEstrategias de almacenamiento
En aplicaciones Express, la gestión eficiente de archivos requiere definir estrategias claras sobre dónde y cómo almacenar los diferentes tipos de contenido. La elección de la estrategia adecuada impacta directamente en el rendimiento, escalabilidad y mantenimiento de la aplicación.
Almacenamiento local vs. externo
El almacenamiento local en el servidor donde se ejecuta Express es la opción más directa para comenzar. Los archivos se guardan en el sistema de archivos del servidor, lo que permite acceso rápido y control total sobre los datos.
import express from 'express';
import multer from 'multer';
import path from 'path';
const app = express();
// Configuración para almacenamiento local
const localStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './uploads/');
},
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
cb(null, `${uniqueName}${path.extname(file.originalname)}`);
}
});
const upload = multer({ storage: localStorage });
app.post('/upload', upload.single('archivo'), (req, res) => {
res.json({
mensaje: 'Archivo guardado localmente',
ruta: req.file.path,
tamaño: req.file.size
});
});
Sin embargo, el almacenamiento externo se vuelve necesario cuando la aplicación crece. Los servicios en la nube como AWS S3, Google Cloud Storage o Azure Blob Storage ofrecen ventajas significativas en términos de escalabilidad y disponibilidad.
import AWS from 'aws-sdk';
import multer from 'multer';
// Configuración de AWS S3
const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_KEY,
region: process.env.AWS_REGION
});
const memoryStorage = multer.memoryStorage();
const uploadToMemory = multer({ storage: memoryStorage });
app.post('/upload-cloud', uploadToMemory.single('archivo'), async (req, res) => {
try {
const params = {
Bucket: process.env.S3_BUCKET,
Key: `uploads/${Date.now()}-${req.file.originalname}`,
Body: req.file.buffer,
ContentType: req.file.mimetype
};
const resultado = await s3.upload(params).promise();
res.json({
mensaje: 'Archivo subido a S3',
url: resultado.Location,
key: resultado.Key
});
} catch (error) {
res.status(500).json({ error: 'Error al subir archivo' });
}
});
Estrategias de particionado
El particionado de archivos evita que un directorio acumule demasiados archivos, lo que puede degradar el rendimiento del sistema de archivos. Una estrategia común es organizar por fecha o por hash del nombre.
import crypto from 'crypto';
import fs from 'fs/promises';
import path from 'path';
// Particionado por fecha
function crearRutaPorFecha() {
const ahora = new Date();
const año = ahora.getFullYear();
const mes = String(ahora.getMonth() + 1).padStart(2, '0');
const dia = String(ahora.getDate()).padStart(2, '0');
return path.join('uploads', año.toString(), mes, dia);
}
// Particionado por hash
function crearRutaPorHash(nombreArchivo) {
const hash = crypto.createHash('md5').update(nombreArchivo).digest('hex');
const nivel1 = hash.substring(0, 2);
const nivel2 = hash.substring(2, 4);
return path.join('uploads', nivel1, nivel2);
}
const storageConParticionado = multer.diskStorage({
destination: async (req, file, cb) => {
const rutaDestino = crearRutaPorFecha();
try {
await fs.mkdir(rutaDestino, { recursive: true });
cb(null, rutaDestino);
} catch (error) {
cb(error);
}
},
filename: (req, file, cb) => {
const extension = path.extname(file.originalname);
const nombreBase = path.basename(file.originalname, extension);
const timestamp = Date.now();
cb(null, `${nombreBase}-${timestamp}${extension}`);
}
});
Gestión de metadatos
Los metadatos de archivos proporcionan información valiosa sobre el contenido almacenado. Express puede gestionar esta información tanto en bases de datos como en archivos de configuración.
import { promises as fs } from 'fs';
import path from 'path';
class GestorMetadatos {
constructor(rutaBase) {
this.rutaBase = rutaBase;
this.rutaMetadatos = path.join(rutaBase, '.metadata');
}
async guardarMetadatos(archivo, metadatos) {
const infoArchivo = {
nombre: archivo.originalname,
tamaño: archivo.size,
tipo: archivo.mimetype,
fechaSubida: new Date().toISOString(),
ruta: archivo.path,
...metadatos
};
const rutaMetadata = path.join(this.rutaMetadatos, `${archivo.filename}.json`);
try {
await fs.mkdir(this.rutaMetadatos, { recursive: true });
await fs.writeFile(rutaMetadata, JSON.stringify(infoArchivo, null, 2));
return infoArchivo;
} catch (error) {
throw new Error(`Error al guardar metadatos: ${error.message}`);
}
}
async obtenerMetadatos(nombreArchivo) {
const rutaMetadata = path.join(this.rutaMetadatos, `${nombreArchivo}.json`);
try {
const contenido = await fs.readFile(rutaMetadata, 'utf8');
return JSON.parse(contenido);
} catch (error) {
return null;
}
}
}
const gestorMetadatos = new GestorMetadatos('./uploads');
app.post('/upload-con-metadata', upload.single('archivo'), async (req, res) => {
try {
const metadatosAdicionales = {
usuario: req.body.usuario,
categoria: req.body.categoria,
descripcion: req.body.descripcion
};
const metadatos = await gestorMetadatos.guardarMetadatos(
req.file,
metadatosAdicionales
);
res.json({
mensaje: 'Archivo y metadatos guardados',
metadatos: metadatos
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Estrategias de backup y redundancia
La protección de datos requiere implementar estrategias de respaldo que garanticen la disponibilidad de los archivos. Express puede coordinar estas operaciones mediante tareas programadas o eventos.
import cron from 'node-cron';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
class GestorBackup {
constructor(rutaOrigen, rutaDestino) {
this.rutaOrigen = rutaOrigen;
this.rutaDestino = rutaDestino;
}
async crearBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const nombreBackup = `backup-${timestamp}`;
const rutaCompleta = path.join(this.rutaDestino, nombreBackup);
try {
// Crear backup usando rsync (Linux/Mac) o robocopy (Windows)
const comando = process.platform === 'win32'
? `robocopy "${this.rutaOrigen}" "${rutaCompleta}" /E`
: `rsync -av "${this.rutaOrigen}/" "${rutaCompleta}/"`;
await execAsync(comando);
return {
exito: true,
ruta: rutaCompleta,
fecha: new Date().toISOString()
};
} catch (error) {
throw new Error(`Error en backup: ${error.message}`);
}
}
programarBackups() {
// Backup diario a las 2:00 AM
cron.schedule('0 2 * * *', async () => {
try {
const resultado = await this.crearBackup();
console.log('Backup completado:', resultado.ruta);
} catch (error) {
console.error('Error en backup programado:', error.message);
}
});
}
}
const gestorBackup = new GestorBackup('./uploads', './backups');
gestorBackup.programarBackups();
// Endpoint para backup manual
app.post('/admin/backup', async (req, res) => {
try {
const resultado = await gestorBackup.crearBackup();
res.json({
mensaje: 'Backup creado exitosamente',
detalles: resultado
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
La elección de la estrategia de almacenamiento adecuada depende de factores como el volumen de datos, los requisitos de disponibilidad, el presupuesto y la arquitectura general de la aplicación. Combinar múltiples enfoques suele ser la solución más robusta para aplicaciones en producción.
Organización de archivos
Una estructura de directorios bien planificada es fundamental para mantener la escalabilidad y el mantenimiento de aplicaciones Express que manejan archivos. La organización efectiva facilita la localización de contenido, mejora el rendimiento de las operaciones de archivo y simplifica las tareas de administración.
Estructura de directorios por tipo de contenido
La separación por tipo de archivo permite aplicar políticas específicas de seguridad, compresión y acceso según la naturaleza del contenido. Esta aproximación facilita la configuración de middleware específico para cada categoría.
import express from 'express';
import path from 'path';
import { promises as fs } from 'fs';
const app = express();
// Estructura de directorios por tipo
const ESTRUCTURA_ARCHIVOS = {
imagenes: 'uploads/images',
documentos: 'uploads/documents',
videos: 'uploads/videos',
audio: 'uploads/audio',
temporales: 'uploads/temp'
};
// Inicializar estructura de directorios
async function inicializarEstructura() {
for (const [tipo, ruta] of Object.entries(ESTRUCTURA_ARCHIVOS)) {
try {
await fs.mkdir(ruta, { recursive: true });
console.log(`Directorio creado: ${ruta}`);
} catch (error) {
console.error(`Error creando directorio ${ruta}:`, error.message);
}
}
}
// Función para determinar el directorio según el tipo de archivo
function obtenerDirectorioPorTipo(mimetype) {
if (mimetype.startsWith('image/')) return ESTRUCTURA_ARCHIVOS.imagenes;
if (mimetype.startsWith('video/')) return ESTRUCTURA_ARCHIVOS.videos;
if (mimetype.startsWith('audio/')) return ESTRUCTURA_ARCHIVOS.audio;
if (mimetype.includes('pdf') || mimetype.includes('document')) {
return ESTRUCTURA_ARCHIVOS.documentos;
}
return ESTRUCTURA_ARCHIVOS.temporales;
}
inicializarEstructura();
Nomenclatura consistente de archivos
Un sistema de nomenclatura estandarizado previene conflictos de nombres y facilita la identificación de archivos. La combinación de timestamps, identificadores únicos y información descriptiva crea nombres predecibles y únicos.
import crypto from 'crypto';
class GeneradorNombres {
static generarNombreUnico(archivoOriginal, prefijo = '') {
const extension = path.extname(archivoOriginal);
const nombreBase = path.basename(archivoOriginal, extension)
.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
const timestamp = Date.now();
const aleatorio = crypto.randomBytes(4).toString('hex');
const nombreCompleto = prefijo
? `${prefijo}-${nombreBase}-${timestamp}-${aleatorio}${extension}`
: `${nombreBase}-${timestamp}-${aleatorio}${extension}`;
return nombreCompleto;
}
static generarNombrePorCategoria(archivo, categoria, usuario = null) {
const extension = path.extname(archivo.originalname);
const fecha = new Date().toISOString().split('T')[0];
const hora = new Date().toTimeString().split(' ')[0].replace(/:/g, '');
let nombre = `${categoria}-${fecha}-${hora}`;
if (usuario) {
nombre += `-${usuario.replace(/[^a-z0-9]/gi, '')}`;
}
nombre += `-${crypto.randomBytes(3).toString('hex')}${extension}`;
return nombre.toLowerCase();
}
}
// Middleware para generar nombres consistentes
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const directorio = obtenerDirectorioPorTipo(file.mimetype);
cb(null, directorio);
},
filename: (req, file, cb) => {
const categoria = req.body.categoria || 'general';
const usuario = req.user?.username;
const nombreArchivo = GeneradorNombres.generarNombrePorCategoria(
file,
categoria,
usuario
);
cb(null, nombreArchivo);
}
});
Indexación y catalogación
La indexación de archivos permite búsquedas rápidas y gestión eficiente del contenido almacenado. Un sistema de catalogación robusto incluye información sobre ubicación, metadatos y relaciones entre archivos.
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
class CatalogadorArchivos {
constructor(rutaBaseDatos = './catalogo.db') {
this.rutaDB = rutaBaseDatos;
this.db = null;
}
async inicializar() {
this.db = await open({
filename: this.rutaDB,
driver: sqlite3.Database
});
await this.db.exec(`
CREATE TABLE IF NOT EXISTS archivos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre_original TEXT NOT NULL,
nombre_archivo TEXT UNIQUE NOT NULL,
ruta_completa TEXT NOT NULL,
tipo_mime TEXT NOT NULL,
tamaño INTEGER NOT NULL,
categoria TEXT,
usuario TEXT,
fecha_subida DATETIME DEFAULT CURRENT_TIMESTAMP,
etiquetas TEXT,
descripcion TEXT
)
`);
await this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_categoria ON archivos(categoria);
CREATE INDEX IF NOT EXISTS idx_usuario ON archivos(usuario);
CREATE INDEX IF NOT EXISTS idx_tipo_mime ON archivos(tipo_mime);
CREATE INDEX IF NOT EXISTS idx_fecha ON archivos(fecha_subida);
`);
}
async registrarArchivo(infoArchivo) {
const query = `
INSERT INTO archivos (
nombre_original, nombre_archivo, ruta_completa,
tipo_mime, tamaño, categoria, usuario, etiquetas, descripcion
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const resultado = await this.db.run(query, [
infoArchivo.originalname,
infoArchivo.filename,
infoArchivo.path,
infoArchivo.mimetype,
infoArchivo.size,
infoArchivo.categoria || null,
infoArchivo.usuario || null,
infoArchivo.etiquetas || null,
infoArchivo.descripcion || null
]);
return resultado.lastID;
}
async buscarArchivos(filtros = {}) {
let query = 'SELECT * FROM archivos WHERE 1=1';
const parametros = [];
if (filtros.categoria) {
query += ' AND categoria = ?';
parametros.push(filtros.categoria);
}
if (filtros.usuario) {
query += ' AND usuario = ?';
parametros.push(filtros.usuario);
}
if (filtros.tipoMime) {
query += ' AND tipo_mime LIKE ?';
parametros.push(`${filtros.tipoMime}%`);
}
if (filtros.fechaDesde) {
query += ' AND fecha_subida >= ?';
parametros.push(filtros.fechaDesde);
}
query += ' ORDER BY fecha_subida DESC LIMIT ?';
parametros.push(filtros.limite || 50);
return await this.db.all(query, parametros);
}
}
const catalogador = new CatalogadorArchivos();
catalogador.inicializar();
Limpieza automática y archivos temporales
La gestión de archivos temporales evita el crecimiento descontrolado del almacenamiento. Un sistema de limpieza automática elimina archivos obsoletos según criterios de tiempo, uso o espacio disponible.
import cron from 'node-cron';
class LimpiadorArchivos {
constructor(catalogador) {
this.catalogador = catalogador;
this.configuracion = {
diasRetencionTemp: 7,
diasRetencionGeneral: 365,
tamañoMaximoMB: 1000
};
}
async limpiarArchivosTemporales() {
const fechaLimite = new Date();
fechaLimite.setDate(fechaLimite.getDate() - this.configuracion.diasRetencionTemp);
try {
const archivosTemp = await this.catalogador.buscarArchivos({
categoria: 'temporal',
fechaHasta: fechaLimite.toISOString()
});
for (const archivo of archivosTemp) {
await this.eliminarArchivo(archivo);
}
console.log(`Limpieza completada: ${archivosTemp.length} archivos temporales eliminados`);
} catch (error) {
console.error('Error en limpieza automática:', error.message);
}
}
async eliminarArchivo(archivo) {
try {
await fs.unlink(archivo.ruta_completa);
await this.catalogador.db.run(
'DELETE FROM archivos WHERE id = ?',
[archivo.id]
);
} catch (error) {
console.error(`Error eliminando archivo ${archivo.nombre_archivo}:`, error.message);
}
}
async verificarEspacioDisponible() {
const stats = await fs.stat('./uploads');
const tamaño = await this.calcularTamañoDirectorio('./uploads');
const tamaño_MB = tamaño / (1024 * 1024);
if (tamaño_MB > this.configuracion.tamañoMaximoMB) {
console.warn(`Espacio excedido: ${tamaño_MB.toFixed(2)}MB`);
await this.limpiarArchivosMasAntiguos();
}
}
async calcularTamañoDirectorio(directorio) {
let tamaño = 0;
const archivos = await fs.readdir(directorio, { withFileTypes: true });
for (const archivo of archivos) {
const rutaCompleta = path.join(directorio, archivo.name);
if (archivo.isDirectory()) {
tamaño += await this.calcularTamañoDirectorio(rutaCompleta);
} else {
const stats = await fs.stat(rutaCompleta);
tamaño += stats.size;
}
}
return tamaño;
}
programarLimpieza() {
// Limpieza diaria a las 3:00 AM
cron.schedule('0 3 * * *', () => {
this.limpiarArchivosTemporales();
this.verificarEspacioDisponible();
});
// Verificación de espacio cada 6 horas
cron.schedule('0 */6 * * *', () => {
this.verificarEspacioDisponible();
});
}
}
const limpiador = new LimpiadorArchivos(catalogador);
limpiador.programarLimpieza();
Endpoints para gestión de archivos
La interfaz de gestión proporciona endpoints para consultar, organizar y administrar los archivos almacenados. Estos endpoints facilitan la integración con interfaces de usuario y herramientas de administración.
// Endpoint para listar archivos con filtros
app.get('/api/archivos', async (req, res) => {
try {
const filtros = {
categoria: req.query.categoria,
usuario: req.query.usuario,
tipoMime: req.query.tipo,
fechaDesde: req.query.desde,
limite: parseInt(req.query.limite) || 20
};
const archivos = await catalogador.buscarArchivos(filtros);
res.json({
archivos: archivos,
total: archivos.length,
filtros: filtros
});
} catch (error) {
res.status(500).json({ error: 'Error consultando archivos' });
}
});
// Endpoint para obtener estadísticas de almacenamiento
app.get('/api/archivos/estadisticas', async (req, res) => {
try {
const estadisticas = await catalogador.db.all(`
SELECT
categoria,
COUNT(*) as cantidad,
SUM(tamaño) as tamaño_total,
AVG(tamaño) as tamaño_promedio
FROM archivos
GROUP BY categoria
`);
const espacioTotal = await limpiador.calcularTamañoDirectorio('./uploads');
res.json({
por_categoria: estadisticas,
espacio_total_bytes: espacioTotal,
espacio_total_mb: (espacioTotal / (1024 * 1024)).toFixed(2)
});
} catch (error) {
res.status(500).json({ error: 'Error obteniendo estadísticas' });
}
});
// Endpoint para reorganizar archivos
app.post('/api/archivos/:id/mover', async (req, res) => {
try {
const { id } = req.params;
const { nueva_categoria } = req.body;
const archivo = await catalogador.db.get(
'SELECT * FROM archivos WHERE id = ?',
[id]
);
if (!archivo) {
return res.status(404).json({ error: 'Archivo no encontrado' });
}
const nuevaRuta = path.join(
ESTRUCTURA_ARCHIVOS[nueva_categoria] || ESTRUCTURA_ARCHIVOS.temporales,
path.basename(archivo.ruta_completa)
);
await fs.rename(archivo.ruta_completa, nuevaRuta);
await catalogador.db.run(
'UPDATE archivos SET categoria = ?, ruta_completa = ? WHERE id = ?',
[nueva_categoria, nuevaRuta, id]
);
res.json({
mensaje: 'Archivo movido exitosamente',
nueva_ruta: nuevaRuta,
categoria: nueva_categoria
});
} catch (error) {
res.status(500).json({ error: 'Error moviendo archivo' });
}
});
Una organización de archivos efectiva en Express combina estructura de directorios lógica, nomenclatura consistente, indexación robusta y mantenimiento automático. Esta aproximación integral garantiza que el sistema de archivos permanezca eficiente y manejable a medida que la aplicación escala.
Otras lecciones de Express
Accede a todas las lecciones de Express y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Expressjs
Introducción Y Entorno
Instalación De Express
Introducción Y Entorno
Estados Http
Routing
Métodos Delete
Routing
Parámetros Y Query Strings
Routing
Métodos Get
Routing
Ejercicios de programación de Express
Evalúa tus conocimientos de esta lección Almacenamiento de archivos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.