Carga de archivos reanudable
Aprende a implementar cargas de archivos reanudables en JavaScript: transferencia por fragmentos, reanudación tras interrupciones, servidor Node.js y File.slice con fetch.
Subir un vídeo de 2 GB a través de una conexión móvil inestable con una sola solicitud fetch es arriesgado: una conexión caída al 95% obliga al usuario a empezar desde cero. Las cargas de archivos reanudables resuelven esto dividiendo el archivo en fragmentos pequeños, subiéndolos uno a la vez y recordando cuáles ya llegaron al servidor — así, una carga interrumpida continúa desde donde se quedó en lugar de reiniciarse.
Esta página cubre el panorama completo: cómo funcionan conceptualmente las cargas fragmentadas y reanudables, un servidor funcional con Node.js + Express que almacena y reensambla fragmentos, un cliente construido con la biblioteca resumable.js, y una versión nativa sin dependencias que usa File.slice y fetch. También verás el error de reensamblaje más común que debes evitar y consejos para producción.
Cómo funcionan las cargas reanudables
La idea central es sencilla y se apoya en tres piezas que trabajan juntas:
- Dividir el archivo en fragmentos. El navegador divide el archivo seleccionado en piezas de tamaño fijo (por ejemplo, 1 MB cada una) usando el método
Blob.slicequeFilehereda. El archivo nunca se carga completamente en memoria. - Subir los fragmentos de uno en uno (o unos pocos a la vez). Cada fragmento es una solicitud HTTP separada que lleva su índice (
chunk 3 of 17), el total de fragmentos, el nombre del archivo y un identificador estable que etiqueta de forma única esta sesión de carga. - Reensamblar en el servidor. El servidor guarda cada fragmento en disco identificado por su índice. Una vez que han llegado todos los fragmentos, los concatena en orden para formar el archivo final.
La capacidad de reanudar proviene del paso 3 más un paso de verificación previa en el cliente. Antes de subir un fragmento, el cliente pregunta al servidor «¿ya tienes el fragmento N?» (normalmente mediante una solicitud HTTP HEAD). Si la respuesta es afirmativa, omite ese fragmento. Así, tras un fallo o recarga, el cliente reanaliza el archivo y solo reenvía las piezas que faltan. El identificador estable es lo que permite al servidor reconocer una carga que regresa a medio camino.
File (2.5 MB)
└─ slice into 1 MB chunks ──► [chunk 1] [chunk 2] [chunk 3 (0.5 MB)]
│ │ │
HEAD /upload?chunk=N (already there? skip : send)
▼ ▼ ▼
POST /upload (one request per missing chunk)
└────────┬─────────┘
server saves chunk-N.bin, then concatenates in orderVentajas de las cargas de archivos reanudables
- Mejor experiencia de usuario: los usuarios pueden reanudar las cargas sin empezar de nuevo.
- Eficiencia: solo se transfieren las partes que faltan tras un fallo, no el archivo completo.
- Fiabilidad en redes deficientes: las interrupciones de red se gestionan correctamente, algo especialmente importante para archivos grandes y conexiones móviles.
- Menor presión sobre la memoria: trabajar con fragmentos pequeños evita cargar en memoria archivos de varios gigabytes.
Implementar cargas de archivos reanudables en JavaScript
Configuración del entorno
Antes de entrar en la implementación, asegúrate de contar con las siguientes herramientas y bibliotecas:
- Un navegador web moderno con soporte para JavaScript.
- Un servidor capaz de gestionar cargas de archivos.
- La biblioteca
resumable.js(o una similar) para gestionar la lógica del lado del cliente.
Instala las dependencias de Node.js necesarias:
npm install express corsConfiguración del lado del servidor
Primero, configura tu servidor para gestionar los fragmentos del archivo y almacenar los metadatos sobre los archivos subidos. A continuación se muestra un ejemplo con Node.js y Express. Ten en cuenta que resumable.js envía los metadatos de los fragmentos en la cadena de consulta por defecto, por lo que leemos de req.query y usamos un directorio temporal por archivo para gestionar de forma segura la llegada de fragmentos fuera de orden.
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;
app.use(cors());
// Handle chunk verification for testChunks: true
app.head('/upload', (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
const chunkNumber = parseInt(req.query.resumableChunkNumber);
const identifier = req.query.resumableIdentifier;
const chunkPath = path.join('uploads', identifier, `chunk-${chunkNumber}.bin`);
fs.promises.access(chunkPath)
.then(() => res.status(200).end())
.catch(() => res.status(404).end());
});
app.post('/upload', async (req, res) => {
try {
const chunkNumber = parseInt(req.query.resumableChunkNumber);
const totalChunks = parseInt(req.query.resumableTotalChunks);
const identifier = req.query.resumableIdentifier;
const fileName = req.query.resumableFilename;
const chunkDir = path.join('uploads', identifier);
await fs.promises.mkdir(chunkDir, { recursive: true });
// Read raw body (resumable.js sends chunks as application/octet-stream)
const buffer = await new Promise((resolve, reject) => {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
const chunkPath = path.join(chunkDir, `chunk-${chunkNumber}.bin`);
await fs.promises.writeFile(chunkPath, buffer);
const receivedChunks = (await fs.promises.readdir(chunkDir)).length;
if (receivedChunks === totalChunks) {
// Concatenate chunks IN ORDER, one at a time (see warning below).
const finalPath = path.join('uploads', fileName);
await fs.promises.writeFile(finalPath, ''); // start with an empty file
for (let i = 1; i <= totalChunks; i++) {
const data = await fs.promises.readFile(
path.join(chunkDir, `chunk-${i}.bin`)
);
await fs.promises.appendFile(finalPath, data);
}
await fs.promises.rm(chunkDir, { recursive: true, force: true });
res.status(200).send('File uploaded successfully');
} else {
// resumable.js expects a 200 OK for successful chunk uploads
res.status(200).send('Chunk uploaded successfully');
}
} catch (error) {
console.error('Upload error:', error);
res.status(500).send('Server error during upload');
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});Reensambla los fragmentos de forma secuencial, no concurrente. Un error habitual es conectar el stream de lectura de cada fragmento a un mismo stream de escritura de forma simultánea (fs.createReadStream(...).pipe(writeStream) dentro de un bucle). Los streams compiten entre sí, por lo que los bytes se intercalan en el orden incorrecto y el primer stream en finalizar cierra el stream de escritura prematuramente — produciendo un archivo corrupto. Lee y añade un fragmento a la vez, como se muestra arriba.
Implementación del lado del cliente
Ahora implementemos la lógica del lado del cliente usando JavaScript y la biblioteca resumable.js. Asegúrate de incluir la biblioteca resumable.js en tu proyecto. Usamos la versión 2.1.0 para compatibilidad moderna. Para entornos de producción, considera el protocolo estandarizado tus o el uso nativo de File.slice con fetch para mayor control y compatibilidad multiplataforma.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Resumable File Upload</title>
</head>
<body>
<input type="file" id="fileInput" />
<button id="uploadButton">Upload</button>
<p id="progress">Ready</p>
<script src="https://unpkg.com/[email protected]/resumable.min.js"></script>
<script>
const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');
const progressEl = document.getElementById('progress');
const r = new Resumable({
target: '/upload',
chunkSize: 1 * 1024 * 1024, // 1MB chunks
simultaneousUploads: 1,
testChunks: true,
throttleProgressCallbacks: 1,
});
r.assignBrowse(fileInput);
uploadButton.addEventListener('click', () => {
if (r.files.length > 0) {
r.upload();
} else {
alert('Please select a file to upload.');
}
});
r.on('progress', (file, loaded, total) => {
const percent = Math.round((loaded / total) * 100);
progressEl.textContent = `Uploading ${file.fileName}: ${percent}%`;
});
r.on('fileSuccess', (file, message) => {
console.log(`File ${file.fileName} uploaded successfully.`);
progressEl.textContent = 'Upload complete!';
});
r.on('fileError', (file, message) => {
console.error(`Error uploading file ${file.fileName}: ${message}`);
progressEl.textContent = 'Upload failed.';
});
</script>
</body>
</html>Alternativa nativa: File.slice + fetch
Para proyectos que prefieren cero dependencias, puedes implementar cargas reanudables de forma nativa usando el método File.slice y fetch. Esto te da control total sobre las cabeceras, los reintentos y — lo más importante — la lógica de reanudación. La función que se muestra a continuación construye la cadena de consulta de cada fragmento, pregunta al servidor si el fragmento ya existe mediante una solicitud HEAD, y solo sube los que faltan. Al llamarla de nuevo tras una interrupción, omite todo lo que ya llegó al servidor:
async function uploadFileNative(file) {
const chunkSize = 1 * 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
// A stable identifier so a re-run resumes the same upload session.
const identifier = `${file.name}-${file.size}`;
for (let i = 0; i < totalChunks; i++) {
const params = new URLSearchParams({
resumableChunkNumber: i + 1,
resumableTotalChunks: totalChunks,
resumableIdentifier: identifier,
resumableFilename: file.name,
});
const url = `/upload?${params}`;
// Resume support: skip chunks the server already has.
const probe = await fetch(url, { method: 'HEAD' });
if (probe.status === 200) continue;
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end); // a Blob, sent as the request body
await fetch(url, { method: 'POST', body: chunk });
}
console.log('Native upload complete');
}Para llevar esto a producción, envuelve cada POST en un bucle de reintentos con retroceso exponencial y añade soporte para la cancelación mediante un AbortController.
Gestión de metadatos
Es fundamental gestionar los metadatos sobre el archivo subido y sus fragmentos: el índice del fragmento, el total, el nombre del archivo y el identificador estable. Esta información es la que permite al servidor reanudar una carga desde el fragmento correcto tras una interrupción. La lógica del servidor para rastrear y ensamblar los fragmentos se cubre en la sección anterior.
Para producción, evita depender solo del sistema de archivos para rastrear el progreso: no ofrece garantías de persistencia y no es seguro cuando varios fragmentos llegan al mismo tiempo (la comprobación de longitud con readdir puede generar condiciones de carrera). Usa una base de datos o caché (como Redis) para registrar qué fragmentos se han completado, y ensambla el archivo solo cuando se confirme que todos los índices han llegado. Si necesitas enviar metadatos estructurados adicionales junto con un fragmento, la API FormData te permite agrupar campos y el blob binario en una sola solicitud.
Ejemplo: subir archivos grandes
La configuración del cliente es idéntica a la del ejemplo anterior. Para optimizar la carga de archivos grandes, puedes aumentar el chunkSize (por ejemplo, a 5 MB) y ajustar simultaneousUploads en función de la capacidad de tu servidor y las condiciones de la red.
Consejos profesionales para cargas de archivos reanudables
- Optimiza el tamaño del fragmento: ajusta el tamaño del fragmento según la velocidad media de la red y el tamaño del archivo para equilibrar velocidad y fiabilidad en la carga.
- Gestión de errores: implementa mecanismos robustos de gestión de errores para manejar interrupciones de red y problemas del servidor.
- Retroalimentación al usuario: proporciona información en tiempo real sobre el progreso de la carga y cualquier incidencia que se produzca.
- Seguridad: asegura el proceso de carga de archivos validando los tipos de archivo e implementando una autenticación y autorización adecuadas.
- Alternativas modernas: para entornos de producción, considera protocolos estandarizados como
tuso el uso nativo deFile.sliceconfetchpara mayor control, capacidad de reanudación y compatibilidad multiplataforma.
Siguiendo estas pautas y ejemplos, puedes implementar un sistema de carga de archivos reanudable robusto y eficiente en JavaScript — uno que resista redes inestables y dé a los usuarios la confianza de que una carga grande no se desperdiciará.
Temas relacionados
- Fetch API — la forma moderna de enviar cada fragmento al servidor.
- Fetch: progreso de descarga — lee el cuerpo de una respuesta en streaming para informar del progreso.
- Fetch: cancelar — cancela una carga en curso con
AbortController. - Blob — el tipo devuelto por
File.slice, que representa cada fragmento. - File y FileReader — lectura del archivo seleccionado por el usuario.
- FormData — agrupa datos binarios con campos adicionales en una sola solicitud.