Saltar al contenido

Carga de archivos reanudable

En este artículo, exploraremos el concepto de cargas de archivos reanudables en JavaScript, proporcionando una guía completa con ejemplos prácticos. Esta técnica es esencial para mejorar la experiencia del usuario, especialmente al trabajar con archivos grandes o conexiones de red poco confiables. Al implementar cargas reanudables, podemos garantizar que nuestros usuarios puedan continuar subiendo archivos desde donde lo dejaron, minimizando la pérdida de datos y la frustración.

Introducción a las cargas de archivos reanudables

Las cargas de archivos reanudables permiten a los usuarios subir archivos en fragmentos, asegurando que si una carga se interrumpe debido a un problema de red o cualquier otra razón, pueda reanudarse desde el último fragmento cargado con éxito. Esta técnica es particularmente útil para archivos grandes y puede mejorar significativamente la fiabilidad de las cargas de archivos.

Ventajas de las cargas de archivos reanudables

  • Mejora de la experiencia del usuario: Los usuarios pueden reanudar las cargas sin empezar de nuevo.
  • Eficiencia: Reduce la cantidad de datos transferidos al subir solo las partes faltantes.
  • Gestión de errores: Maneja las interrupciones de red de manera eficiente.

Implementación de cargas de archivos reanudables en JavaScript

Configuración del entorno

Antes de sumergirnos 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 manejar 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 requeridas:

bash
npm install express cors

Configuración del lado del servidor

Primero, configura tu servidor para manejar los fragmentos de archivos y almacenar metadatos sobre los archivos cargados. Aquí tienes un ejemplo usando 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 desde req.query y usamos un directorio temporal por archivo para manejar de forma segura la llegada de fragmentos fuera de orden.

javascript
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) {
      const finalPath = path.join('uploads', fileName);
      const writeStream = fs.createWriteStream(finalPath);
      const finishPromise = new Promise((resolve, reject) => {
        writeStream.on('finish', resolve);
        writeStream.on('error', reject);
      });

      for (let i = 1; i <= totalChunks; i++) {
        const chunkPath = path.join(chunkDir, `chunk-${i}.bin`);
        fs.createReadStream(chunkPath).pipe(writeStream);
      }
      await finishPromise;
      await fs.promises.rmdir(chunkDir);
      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}`);
});

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 File.slice nativo con fetch para un mejor control y soporte multiplataforma.

html
<!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 la API File.slice y fetch. Este enfoque te da control total sobre los encabezados, reintentos y ensamblaje de fragmentos.

javascript
async function uploadFileNative(file) {
  const chunkSize = 1 * 1024 * 1024; // 1MB
  const totalChunks = Math.ceil(file.size / chunkSize);
  const identifier = `${file.name}-${Date.now()}`;

  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);

    const url = new URL('/upload', window.location.origin);
    url.searchParams.set('resumableChunkNumber', i + 1);
    url.searchParams.set('resumableTotalChunks', totalChunks);
    url.searchParams.set('resumableIdentifier', identifier);
    url.searchParams.set('resumableFilename', file.name);

    await fetch(url, { method: 'POST', body: chunk });
  }
  console.log('Native upload complete');
}

Gestión de metadatos

Es crucial gestionar los metadatos sobre el archivo cargado y sus fragmentos. Esta información ayuda a reanudar la carga desde el fragmento correcto en caso de interrupciones. La lógica del servidor para rastrear y ensamblar fragmentos se cubre en la sección anterior. Para entornos de producción, evita el almacenamiento solo en memoria o en el sistema de archivos, ya que carece de persistencia y seguridad de subprocesos. En su lugar, utiliza una base de datos o caché (por ejemplo, Redis) para rastrear la finalización de fragmentos y ensamblar archivos de manera confiable.

Ejemplo: carga de archivos grandes

La configuración del cliente permanece idéntica al ejemplo anterior. Para optimizar para archivos grandes, puedes aumentar el chunkSize (por ejemplo, a 5 MB) y ajustar simultaneousUploads según 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 de red promedio y el tamaño del archivo para equilibrar la velocidad de carga y la fiabilidad.
  • Gestión de errores: Implementa mecanismos robustos de gestión de errores para hacer frente a interrupciones de red y problemas del servidor.
  • Retroalimentación al usuario: Proporciona retroalimentación en tiempo real a los usuarios sobre el progreso de la carga y cualquier problema encontrado.
  • Seguridad: Asegúrate de que el proceso de carga de archivos sea seguro 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 tus o File.slice nativo con fetch para un mejor control, capacidad de reanudación y compatibilidad multiplataforma.

Siguiendo estas pautas y ejemplos, podemos implementar un sistema de carga de archivos reanudable robusto y eficiente en JavaScript, mejorando la experiencia del usuario y garantizando cargas de archivos confiables.

Práctica

¿Cuáles de las siguientes son ventajas de utilizar cargas de archivos reanudables?

¿Te resulta útil?

Vista previa dual-run — compárala con las rutas Symfony en producción.