W3docs

Téléversement de fichier reprisable

Apprenez à créer des téléversements reprisables en JavaScript : transfert par morceaux, reprise après interruption, serveur Node.js, resumable.js et File.slice + fetch.

Téléverser une vidéo de 2 Go sur une connexion mobile instable avec une seule requête fetch est risqué : une connexion perdue à 95 % oblige l'utilisateur à tout recommencer depuis le début. Les téléversements de fichiers reprisables résolvent ce problème en découpant le fichier en petits morceaux, en les envoyant un par un, et en mémorisant ceux qui sont déjà arrivés — ainsi, un téléversement interrompu reprend là où il s'est arrêté, au lieu de repartir de zéro.

Cette page couvre l'ensemble du sujet : le fonctionnement conceptuel des téléversements par morceaux et reprisables, un serveur Node.js + Express opérationnel qui stocke et reconstitue les morceaux, un client construit avec la bibliothèque resumable.js, et une version native sans dépendance utilisant File.slice et fetch. Vous verrez également l'erreur de reconstitution courante à éviter ainsi que des conseils de robustesse pour la production.

Fonctionnement des téléversements reprisables

L'idée de base est simple et repose sur trois éléments qui fonctionnent ensemble :

  1. Découper le fichier en morceaux. Le navigateur divise le fichier sélectionné en pièces de taille fixe (par exemple, 1 Mo chacune) à l'aide de la méthode Blob.slice héritée par File. Le fichier lui-même n'est jamais chargé entièrement en mémoire.
  2. Téléverser les morceaux un (ou quelques-uns) à la fois. Chaque morceau est une requête HTTP distincte portant son index (morceau 3 sur 17), le nombre total de morceaux, le nom du fichier et un identifiant stable qui identifie de manière unique cette session de téléversement.
  3. Reconstituer côté serveur. Le serveur enregistre chaque morceau sur le disque en l'indexant par son numéro. Une fois tous les morceaux reçus, il les concatène dans l'ordre pour former le fichier final.

La reprisabilité découle de l'étape 3 combinée à une étape vérification avant envoi côté client. Avant d'envoyer un morceau, le client demande au serveur « avez-vous déjà le morceau N ? » (généralement via une requête HTTP HEAD). Si oui, il ignore ce morceau. Ainsi, après un crash ou un rechargement, le client réanalyse le fichier et n'envoie à nouveau que les morceaux manquants. L'identifiant stable permet au serveur de reconnaître un téléversement en cours interrompu.

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 order

Avantages des téléversements de fichiers reprisables

  • Meilleure expérience utilisateur : les utilisateurs peuvent reprendre les téléversements sans recommencer depuis le début.
  • Efficacité : seules les parties manquantes sont transférées après une défaillance, pas l'intégralité du fichier.
  • Fiabilité sur les réseaux instables : les interruptions réseau sont gérées élégamment, ce qui est particulièrement important pour les fichiers volumineux et les connexions mobiles.
  • Moins de pression mémoire : travailler avec de petits morceaux évite de charger un fichier de plusieurs gigaoctets en mémoire.

Implémenter des téléversements de fichiers reprisables en JavaScript

Configurer l'environnement

Avant de plonger dans l'implémentation, assurez-vous de disposer des outils et bibliothèques suivants :

  • Un navigateur web moderne avec prise en charge de JavaScript.
  • Un serveur capable de gérer les téléversements de fichiers.
  • La bibliothèque resumable.js (ou une bibliothèque similaire) pour gérer la logique côté client.

Installez les dépendances Node.js requises :

npm install express cors

Configuration côté serveur

Commencez par configurer votre serveur pour gérer les morceaux de fichiers et stocker les métadonnées sur les fichiers téléversés. Voici un exemple avec Node.js et Express. Notez que resumable.js envoie les métadonnées des morceaux dans la chaîne de requête par défaut, donc nous lisons depuis req.query et utilisons un répertoire temporaire par fichier pour gérer en toute sécurité l'arrivée des morceaux dans le désordre.

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}`);
});
Avertissement

Reconstituez les morceaux séquentiellement, pas simultanément. Une erreur courante consiste à rediriger le flux de lecture de chaque morceau vers un seul flux d'écriture en même temps (fs.createReadStream(...).pipe(writeStream) dans une boucle). Les flux se disputent la ressource, les octets s'entrelacent dans le mauvais ordre et le premier flux à se terminer ferme prématurément le flux d'écriture — produisant un fichier corrompu. Lisez et ajoutez un morceau à la fois, comme indiqué ci-dessus.

Implémentation côté client

Implémentons maintenant la logique côté client en utilisant JavaScript et la bibliothèque resumable.js. Assurez-vous d'inclure la bibliothèque resumable.js dans votre projet. Nous utilisons la version 2.1.0 pour une compatibilité moderne. Pour les environnements de production, envisagez le protocole standardisé tus ou File.slice natif avec fetch pour un meilleur contrôle et une compatibilité multiplateforme.

<!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>

Alternative native : File.slice + fetch

Pour les projets qui préfèrent zéro dépendance, vous pouvez implémenter des téléversements reprisables nativement en utilisant la méthode File.slice et fetch. Cela vous donne un contrôle total sur les en-têtes, les tentatives de reconnexion et — surtout — la logique de reprise. La fonction ci-dessous construit la chaîne de requête de chaque morceau, demande au serveur si le morceau existe déjà via une requête HEAD, et n'envoie que ceux qui manquent. La rappeler après une interruption ignore tout ce qui a déjà été transmis :

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');
}

Pour rendre cela prêt pour la production, vous devriez envelopper chaque POST dans une boucle de réessai avec un backoff exponentiel et prendre en charge l'annulation avec un AbortController.

Gestion des métadonnées

Il est crucial de gérer les métadonnées du fichier téléversé et de ses morceaux — l'index du morceau, le nombre total, le nom du fichier et l'identifiant stable. Ces informations permettent au serveur de reprendre un téléversement à partir du bon morceau après une interruption. La logique serveur pour suivre et assembler les morceaux est couverte dans la section précédente.

Pour la production, évitez de vous fier uniquement au système de fichiers pour suivre la progression : il manque de garanties de persistance et n'est pas sûr lorsque plusieurs morceaux arrivent en même temps (la vérification de longueur via readdir peut créer une condition de course). Utilisez une base de données ou un cache (comme Redis) pour enregistrer quels morceaux ont été complétés, et assemblez le fichier uniquement une fois que chaque index est confirmé. Si vous devez envoyer des métadonnées structurées supplémentaires avec un morceau, l'API FormData vous permet de regrouper des champs et l'objet blob binaire dans une seule requête.

Exemple : téléverser des fichiers volumineux

La configuration côté client reste identique à l'exemple précédent. Pour optimiser les fichiers volumineux, vous pouvez augmenter la chunkSize (par exemple à 5 Mo) et ajuster simultaneousUploads en fonction de la capacité de votre serveur et des conditions réseau.

Conseils professionnels pour les téléversements de fichiers reprisables

  • Optimiser la taille des morceaux : ajustez la taille des morceaux en fonction de la vitesse réseau moyenne et de la taille des fichiers pour trouver le bon équilibre entre vitesse de téléversement et fiabilité.
  • Gestion des erreurs : mettez en place des mécanismes robustes de gestion des erreurs pour faire face aux interruptions réseau et aux problèmes serveur.
  • Retour d'information à l'utilisateur : fournissez un retour en temps réel aux utilisateurs sur la progression du téléversement et les éventuels problèmes rencontrés.
  • Sécurité : assurez-vous que le processus de téléversement est sécurisé en validant les types de fichiers et en mettant en œuvre une authentification et une autorisation appropriées.
  • Alternatives modernes : pour les environnements de production, envisagez des protocoles standardisés comme tus ou File.slice natif avec fetch pour un meilleur contrôle, une reprisabilité et une compatibilité multiplateforme.

En suivant ces directives et ces exemples, vous pouvez implémenter un système de téléversement de fichiers reprisable robuste et efficace en JavaScript — capable de survivre aux réseaux instables et de donner aux utilisateurs la confiance qu'un téléversement volumineux ne sera pas perdu.

Sujets connexes

  • Fetch API — la méthode moderne pour envoyer chaque morceau au serveur.
  • Fetch : progression du téléchargement — lire un corps de réponse en streaming pour signaler la progression.
  • Fetch : annulation — annuler un téléversement en cours avec AbortController.
  • Blob — le type renvoyé par File.slice, qui représente chaque morceau.
  • File et FileReader — lire le fichier sélectionné par l'utilisateur.
  • FormData — regrouper des données binaires avec des champs supplémentaires dans une seule requête.

Pratique

Pratique
Lesquels des éléments suivants sont des avantages de l'utilisation des téléversements de fichiers reprisables ?
Lesquels des éléments suivants sont des avantages de l'utilisation des téléversements de fichiers reprisables ?
Was this page helpful?