Vai al contenuto
HexaTransfer
Torna al blog
Approfondimenti tecnici

Implementare l'upload di file a blocchi in JavaScript

Implementa upload a blocchi ripristinabili in JavaScript. Gestisci file grandi, monitora il progresso e recupera da interruzioni di rete.

Il GDPR impone misure tecniche adeguate per la trasmissione di dati personali — e un upload a blocchi con crittografia per segmento è oggi lo standard di riferimento. L'upload a blocchi divide un file di grandi dimensioni in pezzi a dimensione fissa (tipicamente 5-10 MB), carica ciascuno con una richiesta HTTP separata e li riassembla sul server. Risolve tre problemi concreti: i browser e i proxy interrompono le richieste oltre i 2 GB, le reti mobili cadono a metà upload, e gli utenti vogliono un feedback sul progresso. Un'implementazione funzionale usa File.slice() per ritagliare i blocchi, fetch con un AbortSignal per blocco, l'assemblaggio lato server tramite S3 multipart o un merger personalizzato, e un indice locale su IndexedDB affinché la ripresa sopravviva ai ricaricamenti della scheda.

Perché i blocchi battono il caricamento singolo

Un file da 4 GB caricato in un'unica richiesta fallisce per ragioni prevedibili: il limite predefinito di Nginx è 1 MB, Cloudflare blocca i caricamenti free-tier sopra i 100 MB per richiesta, AWS API Gateway si ferma a 10 MB, e Safari mobile termina le schede che tengono un ArrayBuffer da 4 GB in memoria. I caricamenti a blocchi evitano tutti questi limiti. Si ottengono barre di progresso che avanzano davvero, riprese che non ripartono da zero, e la possibilità di mettere in pausa e ricominciare. Il compromesso è più stato lato server e più richieste — circa una HTTP per ogni 5 MB, il che su un file da 10 GB significa 2.000 richieste.

Scegliere la dimensione del blocco

La dimensione del blocco è un compromesso tra prestazioni e resilienza. Troppo piccolo (sotto 1 MB) e si spende più tempo negli handshake TLS 1.3 che nel trasferimento dati. Troppo grande (oltre 100 MB) e una connessione caduta spreca minuti di upload. L'optimum per la maggior parte delle reti è 5-10 MB, in linea con il minimo multipart S3 di 5 MB e con le dimensioni tipiche della finestra TCP dopo il slow-start.

Misura prima la rete dell'utente:

const downlink = navigator.connection?.downlink ?? 10;
const chunkSize = downlink > 20 ? 10 * 1024 * 1024 : 5 * 1024 * 1024;

Su una connessione da 100 Mbit, blocchi da 10 MB si completano in circa un secondo ciascuno. Su 4G, blocchi da 5 MB offrono un recupero migliore quando si entra in una galleria.

Sezionare e calcolare l'hash del file

File.slice() restituisce un Blob che fa riferimento agli stessi byte sottostanti sul disco senza copiare, quindi sezionare un file da 20 GB non costa nulla:

function* sliceFile(file, chunkSize) {
  for (let offset = 0; offset < file.size; offset += chunkSize) {
    yield {
      index: Math.floor(offset / chunkSize),
      blob: file.slice(offset, offset + chunkSize),
      start: offset,
      end: Math.min(offset + chunkSize, file.size)
    };
  }
}

Calcola un hash SHA-256 di ogni blocco prima di caricarlo in modo che il server possa verificare l'integrità:

const buffer = await chunk.blob.arrayBuffer();
const digest = await crypto.subtle.digest('SHA-256', buffer);
const hash = Array.from(new Uint8Array(digest))
  .map(b => b.toString(16).padStart(2, '0')).join('');

Per 10 GB di dati, il calcolo dell'hash aggiunge circa 20 secondi su un laptop moderno — ne vale la pena per rilevare corruzioni silenziose su uplink cellulari instabili.

Upload con concorrenza controllata

Gli upload sequenziali sprecano banda; il parallelismo illimitato fa crashare il browser. Un limite di concorrenza di 3-4 blocchi in volo bilancia entrambi:

async function uploadAll(file, sessionId) {
  const queue = [...sliceFile(file, 5 * 1024 * 1024)];
  const workers = Array.from({ length: 4 }, async () => {
    while (queue.length) {
      const chunk = queue.shift();
      await uploadChunk(chunk, sessionId);
      emitProgress(chunk.index);
    }
  });
  await Promise.all(workers);
}

Ogni chiamata a uploadChunk è un PUT /upload/:sessionId/:index con il blob come corpo e l'hash in un header. Usa AbortController per blocco in modo da poter annullare singole richieste senza fermare l'intero batch.

Riprovare senza sovraccaricare il server

Gli errori di rete richiedono un backoff esponenziale, non cicli di ripetizione stretti. Una policy ragionevole: 3 tentativi, ritardo base 500 ms, jitter fino al 50%:

async function uploadChunk(chunk, sessionId, attempt = 0) {
  try {
    const res = await fetch(`/upload/${sessionId}/${chunk.index}`, {
      method: 'PUT', body: chunk.blob, headers: { 'X-Hash': chunk.hash }
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
  } catch (e) {
    if (attempt >= 3) throw e;
    const delay = 500 * 2 ** attempt + Math.random() * 250;
    await new Promise(r => setTimeout(r, delay));
    return uploadChunk(chunk, sessionId, attempt + 1);
  }
}

Tratta le risposte 5xx come ritentabili, le 4xx come fatali (tranne 408 e 429). Su 429, rispetta l'header Retry-After invece del backoff locale.

Riprendere dopo il ricaricamento della scheda

Persisti lo stato dell'upload su IndexedDB dopo ogni blocco riuscito:

await db.put('uploads', {
  sessionId, fileName: file.name, fileSize: file.size,
  completedChunks: [...completedSet], updatedAt: Date.now()
}, sessionId);

Quando l'utente riapre la pagina con lo stesso selettore di file, confronta size, lastModified e nome del file con le sessioni memorizzate. Se c'è una corrispondenza, chiedi al server quali blocchi ha già ricevuto (un semplice GET /upload/:sessionId/status che restituisce una bitmap funziona), poi carica solo quelli mancanti. Il protocollo tus.io formalizza esattamente questo schema con l'header Upload-Offset, e la libreria tus-js-client include un'implementazione solida se non vuoi costruirla da zero.

Assemblare i blocchi sul server

Due opzioni serie: il caricamento multipart S3, dove ogni blocco diventa un PartNumber e un CompleteMultipartUpload finale li unisce, oppure un assemblatore personalizzato che scrive ogni blocco su un file temporaneo e li concatena alla fine. Il multipart S3 è più economico su scala perché non si paga mai l'egress durante l'assemblaggio e R2 offre letture senza costi di uscita. L'approccio personalizzato è più semplice da debuggare e consente di cifrare in streaming durante l'assemblaggio.

Tracciare un progresso credibile

Le barre di progresso che saltano sembrano difettose. Calcola il progresso come byte caricati su byte totali, non blocchi completati, e levigalo con una media mobile su 2 secondi per nascondere il jitter. Usa fetch con un ReadableStream e un Transform per contare i byte, poiché XMLHttpRequest.upload.onprogress non si attiva sempre in modo affidabile su HTTP/3. Mostra un ETA dividendo i byte rimanenti per il throughput recente, ma imposta un minimo di almeno 5 secondi per evitare il classico "2 secondi rimanenti... per 10 minuti".

Evitare gli errori comuni

Tre errori rovinano gli upload a blocchi in produzione: dimenticare di impostare Content-Length per blocco (rompe alcuni proxy edge), riutilizzare lo stesso ID sessione su file diversi (corrompe l'assemblaggio), e lasciare che l'utente cambi il file a metà upload senza versionare la sessione. Calcola sempre l'hash del primo 1 MB del file più la sua dimensione e lastModified per identificare le sessioni. E non fidarti mai solo di lastModified — macOS Finder lo aggiorna sulle modifiche ai metadati.

HexaTransfer usa una pipeline a blocchi e ripristinabile come questa per i suoi upload da 10 GB, con AES-256-GCM lato client aggiunto a ogni blocco prima del PUT. Prova su https://hexatransfer.com — gratuito, senza account, massimo 10 GB.

Mettere tutto insieme

Un upload a blocchi di livello produzione è circa 300 righe di JavaScript: seziona con File.slice, calcola l'hash con SubtleCrypto, carica 3-4 blocchi in parallelo con backoff esponenziale, persisti lo stato della sessione su IndexedDB, e lascia che il server unisca le parti tramite multipart S3 o un merger personalizzato. Testalo contro la modalità aereo, i ricaricamenti della scheda e un file da 15 GB su 4G prima di fidarti. Una volta che funziona, aggiungere crittografia, progresso e ripristinabilità è semplicemente aggiungere strati allo stesso scheletro.

Invia file di grandi dimensioni in modo sicuro con crittografia end-to-end

Trasferisci file fino a 10 GB gratuitamente con crittografia end-to-end. Nessun account necessario. I tuoi file vengono crittografati nel browser prima del caricamento — nessun altro può leggerli.

Invia un file