W3docs

Caricamento di file ripristinabile

Come implementare upload ripristinabili in JavaScript: trasferimento a blocchi, ripresa dopo interruzioni, server Node.js, resumable.js e File.slice + fetch.

Caricare un video da 2 GB su una connessione mobile instabile con una singola richiesta fetch è rischioso: una connessione persa al 95% e l'utente ricomincia da zero. I caricamenti di file ripristinabili risolvono questo problema suddividendo il file in piccoli pezzi, caricandoli uno alla volta e ricordando quali pezzi sono già arrivati — così un caricamento interrotto riprende da dove si era fermato anziché ricominciare.

Questa pagina illustra il quadro completo: come funzionano concettualmente i caricamenti a blocchi e ripristinabili, un server Node.js + Express funzionante che archivia e riassembla i blocchi, un client costruito con la libreria resumable.js e una versione nativa senza dipendenze che usa File.slice e fetch. Vedremo anche il comune errore di riassemblaggio da evitare e i suggerimenti per il rafforzamento in produzione.

Come funzionano i caricamenti ripristinabili

L'idea di base è semplice e si basa su tre componenti che lavorano insieme:

  1. Suddividere il file in blocchi. Il browser divide il file selezionato in pezzi di dimensione fissa (ad esempio, 1 MB ciascuno) usando il metodo Blob.slice ereditato da File. Il file stesso non viene mai caricato completamente in memoria.
  2. Caricare i blocchi uno (o pochi) alla volta. Ogni blocco è una richiesta HTTP separata che trasporta il proprio indice (blocco 3 di 17), il numero totale di blocchi, il nome del file e un identificatore stabile che contrassegna univocamente questa sessione di caricamento.
  3. Riassemblare sul server. Il server salva ogni blocco su disco indicizzato per numero. Una volta arrivati tutti i blocchi, li concatena in ordine nel file finale.

La ripristinabilità deriva dal passaggio 3 più un passaggio di verifica prima dell'invio sul client. Prima di caricare un blocco, il client chiede al server "hai già il blocco N?" (tipicamente tramite una richiesta HTTP HEAD). In caso affermativo, salta quel blocco. Quindi, dopo un crash o un aggiornamento della pagina, il client analizza di nuovo il file e invia solo i pezzi mancanti. L'identificatore stabile è ciò che consente al server di riconoscere un caricamento parziale che ritorna.

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

Vantaggi dei caricamenti di file ripristinabili

  • Migliore esperienza utente: Gli utenti possono riprendere i caricamenti senza ricominciare da capo.
  • Efficienza: Dopo un errore vengono trasferite solo le parti mancanti, non l'intero file.
  • Affidabilità su reti instabili: Le interruzioni di rete vengono gestite correttamente, il che è particolarmente importante per i file di grandi dimensioni e le connessioni mobili.
  • Minore pressione sulla memoria: Lavorare con piccoli blocchi evita di caricare in memoria un file da diversi gigabyte.

Implementazione dei caricamenti di file ripristinabili in JavaScript

Configurazione dell'ambiente

Prima di iniziare con l'implementazione, assicurati di avere i seguenti strumenti e librerie:

  • Un browser web moderno con supporto JavaScript.
  • Un server in grado di gestire i caricamenti di file.
  • La libreria resumable.js (o una libreria simile) per gestire la logica lato client.

Installa le dipendenze Node.js necessarie:

npm install express cors

Configurazione lato server

Prima di tutto, configura il server per gestire i blocchi di file e memorizzare i metadati sui file caricati. Di seguito è riportato un esempio con Node.js e Express. Nota che resumable.js invia i metadati dei blocchi nella query string per impostazione predefinita, quindi leggiamo da req.query e usiamo una directory temporanea per file per gestire in modo sicuro l'arrivo dei blocchi fuori ordine.

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

Riassembla i blocchi in sequenza, non in parallelo. Un errore comune è collegare ogni read stream di blocco a un unico write stream contemporaneamente (fs.createReadStream(...).pipe(writeStream) all'interno di un ciclo). Gli stream si eseguono in parallelo, così i byte si mescolano nell'ordine sbagliato e il primo stream a terminare chiude prematuramente il write stream — producendo un file corrotto. Leggi e aggiungi un blocco alla volta, come mostrato sopra.

Implementazione lato client

Ora implementiamo la logica lato client usando JavaScript e la libreria resumable.js. Assicurati di includere la libreria resumable.js nel tuo progetto. Usiamo la versione v2.1.0 per la compatibilità moderna. Per gli ambienti di produzione, considera il protocollo standardizzato tus o l'uso nativo di File.slice con fetch per un migliore controllo e supporto multipiattaforma.

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

Per i progetti che preferiscono zero dipendenze, è possibile implementare caricamenti ripristinabili in modo nativo usando il metodo File.slice e fetch. Questo offre pieno controllo su intestazioni, tentativi ripetuti e — aspetto fondamentale — la logica di ripresa. La funzione seguente costruisce la query string di ogni blocco, chiede al server se il blocco esiste già con una richiesta HEAD e carica solo quelli mancanti. Richiamarla dopo un'interruzione salta tutto ciò che è già stato trasmesso:

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

Per rendere questo codice adatto alla produzione, dovresti racchiudere ogni POST in un ciclo di tentativi con backoff esponenziale e supportare la cancellazione tramite AbortController.

Gestione dei metadati

È fondamentale gestire i metadati sul file caricato e sui suoi blocchi — l'indice del blocco, il conteggio totale, il nome del file e l'identificatore stabile. Queste informazioni permettono al server di riprendere un caricamento dal blocco corretto dopo un'interruzione. La logica server per il tracciamento e l'assemblaggio dei blocchi è trattata nella sezione precedente.

In produzione, evita di affidarti esclusivamente al file system per tracciare i progressi: manca di garanzie di persistenza e non è sicuro quando diversi blocchi arrivano contemporaneamente (il controllo sulla lunghezza di readdir può generare race condition). Usa un database o una cache (come Redis) per registrare quali blocchi sono stati completati e assembla il file solo una volta confermati tutti gli indici. Se hai bisogno di inviare metadati strutturati aggiuntivi insieme a un blocco, l'API FormData ti consente di raggruppare campi e il blob binario in un'unica richiesta.

Esempio: Caricamento di file di grandi dimensioni

La configurazione del client rimane identica all'esempio precedente. Per ottimizzare i file di grandi dimensioni, puoi aumentare chunkSize (ad esempio a 5 MB) e regolare simultaneousUploads in base alla capacità del server e alle condizioni della rete.

Suggerimenti professionali per i caricamenti di file ripristinabili

  • Ottimizza la dimensione dei blocchi: Regola la dimensione dei blocchi in base alla velocità media della rete e alla dimensione del file per bilanciare velocità di caricamento e affidabilità.
  • Gestione degli errori: Implementa meccanismi robusti di gestione degli errori per affrontare interruzioni di rete e problemi del server.
  • Feedback all'utente: Fornisci feedback in tempo reale agli utenti sull'avanzamento del caricamento e su eventuali problemi riscontrati.
  • Sicurezza: Assicurati che il processo di caricamento dei file sia sicuro validando i tipi di file e implementando autenticazione e autorizzazione appropriate.
  • Alternative moderne: Per gli ambienti di produzione, considera protocolli standardizzati come tus o l'uso nativo di File.slice con fetch per un migliore controllo, ripristinabilità e compatibilità multipiattaforma.

Seguendo queste linee guida ed esempi, puoi implementare un sistema di caricamento di file ripristinabile robusto ed efficiente in JavaScript — uno che sopravvive a reti instabili e dà agli utenti la certezza che un caricamento di grandi dimensioni non vada sprecato.

Argomenti correlati

  • Fetch API — il modo moderno per inviare ogni blocco al server.
  • Fetch: Download progress — legge un corpo di risposta in streaming per segnalare i progressi.
  • Fetch: Abort — annulla un caricamento in corso con AbortController.
  • Blob — il tipo restituito da File.slice, che rappresenta ogni blocco.
  • File and FileReader — lettura del file selezionato dall'utente.
  • FormData — raggruppa dati binari con campi aggiuntivi in un'unica richiesta.

Esercitazione

Pratica
Quali dei seguenti sono vantaggi dell'utilizzo dei caricamenti di file ripristinabili?
Quali dei seguenti sono vantaggi dell'utilizzo dei caricamenti di file ripristinabili?
Was this page helpful?