Implementacja chunked upload plików w JavaScript
Zaimplementuj wznawiany upload plików oparty na chunkach w JavaScript. Obsługa dużych plików, śledzenie postępu i odzyskiwanie po przerwach sieciowych.
Fragmentowany upload pliku w JavaScript dzieli duży plik na kawałki stałego rozmiaru (zazwyczaj 5–10 MB), przesyła każdy jako osobne żądanie HTTP i składa je ponownie na serwerze. Wzorzec rozwiązuje trzy realne problemy: przeglądarki i proxy przerywają żądania przekraczające 2 GB, sieci mobilne zrywają połączenia w trakcie uploadu, a użytkownicy chcą informacji zwrotnej o postępie. Działająca implementacja używa File.slice() do tworzenia fragmentów, fetch z AbortSignal per fragment, składania po stronie serwera przez S3 multipart lub własny łącznik, oraz lokalnego indeksu w IndexedDB, żeby wznowienie przeżywało przeładowanie karty. Protokół tus.io formalizuje dokładnie ten wzorzec z nagłówkiem Upload-Offset.
Dlaczego fragmenty biją jednorazowe uploady
Plik 4 GB przesłany jako jedno żądanie zawodzi z przewidywalnych przyczyn: domyślne client_max_body_size Nginxa to 1 MB, Cloudflare ogranicza darmowe uploady do 100 MB na żądanie, AWS API Gateway zatrzymuje się twardо przy 10 MB, a mobilny Safari zabija karty trzymające 4 GB ArrayBuffer w pamięci. Fragmentowane uploady omijają każdy z tych pułapów. Dostajesz też paski postępu, które faktycznie się przesuwają, ponawiania prób, które nie zaczynają od zera, oraz możliwość pauzy i wznowienia. Kompromisem jest więcej stanu po stronie serwera i więcej podróży w obie strony — mniej więcej jedno żądanie HTTP na każde 5 MB, co przy 10 GB oznacza 2000 żądań.
Dobór rozmiaru fragmentu
Rozmiar fragmentu to kompromis między przepustowością a odpornością. Zbyt mały (poniżej 1 MB) i spędzasz więcej czasu na handshake TLS niż na danych. Zbyt duży (powyżej 100 MB) i zerwane połączenie marnuje minuty uploadu. Optymalna wartość dla większości sieci to 5–10 MB, co odpowiada minimum multipart S3 i dobrze wyrównuje się z typowymi rozmiarami okna TCP po slow-start.
const downlink = navigator.connection?.downlink ?? 10;
const chunkSize = downlink > 20 ? 10 * 1024 * 1024 : 5 * 1024 * 1024;
Na połączeniu 100 Mbit fragmenty 10 MB kończą się w około sekundy. Na 4G fragmenty 5 MB dają lepsze odzyskiwanie gdy trafisz w tunel.
Krojenie i haszowanie pliku
File.slice() zwraca Blob odwołujący się do tych samych bajtów na dysku bez kopiowania — krojenie pliku 20 GB nic nie kosztuje:
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)
};
}
}
Oblicz hash SHA-256 każdego fragmentu przed przesłaniem, żeby serwer mógł weryfikować integralność:
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('');
Dla 10 GB danych haszowanie dodaje może 20 sekund na nowoczesnym laptopie — warto to zrobić, żeby wykryć cichą korupcję na niestabilnych łączach komórkowych.
Upload z kontrolowaną współbieżnością
Sekwencyjne uploady marnują przepustowość; nieograniczona równoległość crasha przeglądarkę. Limit współbieżności 3–4 fragmentów w locie balansuje obie strony:
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);
}
Każde wywołanie uploadChunk to PUT /upload/:sessionId/:index z blobem jako ciałem i hashem w nagłówku. Używaj AbortController per fragment, żeby móc anulować pojedyncze żądania bez zabijania całej partii.
Ponawianie prób bez bombardowania serwera
Błędy sieciowe wymagają wykładniczego cofania, nie ciasnych pętli ponowień. Rozsądna polityka: 3 próby, bazowe opóźnienie 500 ms, jitter do 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);
}
}
Traktuj odpowiedzi 5xx jako ponawialne, 4xx jako fatalne (z wyjątkiem 408 i 429). Na 429 honoruj nagłówek Retry-After zamiast lokalnego cofania.
Wznawianie po przeładowaniu karty
Utrwalaj stan uploadu w IndexedDB po każdym udanym fragmencie:
await db.put('uploads', {
sessionId, fileName: file.name, fileSize: file.size,
completedChunks: [...completedSet], updatedAt: Date.now()
}, sessionId);
Gdy użytkownik ponownie otworzy stronę z tym samym selektorem plików, porównaj size, lastModified i nazwę pliku z przechowanymi sesjami. Jeśli jest dopasowanie, zapytaj serwer, które fragmenty już otrzymał (proste GET /upload/:sessionId/status zwracające bitmapę działa), a następnie prześlij tylko brakujące. Protokół tus.io formalizuje dokładnie ten wzorzec z nagłówkiem Upload-Offset, a biblioteka tus-js-client dostarcza solidną implementację, jeśli nie chcesz pisać własnej.
Składanie fragmentów na serwerze
Dwie poważne opcje: multipart upload S3, gdzie każdy fragment staje się PartNumber i końcowe CompleteMultipartUpload je skleja, lub własny łącznik zapisujący każdy fragment do pliku tymczasowego i konkatenujący na końcu. Multipart S3 jest tańszy na dużą skalę, bo nie płacisz za egress podczas składania, a R2 daje zerowe odczyty. Własne podejście jest prostsze do debugowania i pozwala na szyfrowanie strumieniowe podczas składania.
const upload = await s3.createMultipartUpload({ Bucket, Key });
// per chunk: s3.uploadPart({ UploadId, PartNumber, Body })
await s3.completeMultipartUpload({ UploadId, MultipartUpload: { Parts } });
Uważaj na limit 10 000 części — dla plików powyżej 50 GB potrzebujesz fragmentów 5 MB+ żeby się zmieścić.
Śledzenie postępu, któremu użytkownicy ufają
Paski postępu skaczące w kółko wyglądają na zepsute. Obliczaj postęp jako bajty przesłane do całkowitych bajtów, nie ukończone fragmenty, i wygładź to średnią ruchomą przez 2 sekundy, żeby ukryć jitter. Wyświetlaj ETA dzieląc pozostałe bajty przez końcową przepustowość, ale ogranicz wyświetlanie do co najmniej 5 sekund, żeby uniknąć słynnego "2 sekundy pozostałe... przez 10 minut".
HexaTransfer używa fragmentowanego i wznawialanego pipeline jak ten pod maską dla uploadów 10 GB, z AES-256-GCM po stronie klienta dodanym do każdego fragmentu przed PUT. Wypróbuj na https://hexatransfer.com — za darmo, bez konta, maks. 10 GB.
Łącząc to wszystko
Produkcyjny fragmentowany uploader to może 300 linii JavaScript: krój przez File.slice, haszuj przez SubtleCrypto, przesyłaj 3–4 fragmenty równolegle z wykładniczym cofaniem, utrwalaj stan sesji w IndexedDB i pozwól serwerowi zszywać części przez multipart S3 lub własny łącznik. Testuj to na trybie samolotowym, przeładowaniach kart i pliku 15 GB na 4G przed zaufaniem mu. Gdy to działa, dodanie szyfrowania, postępu i wznawiania to tylko dodatkowe warstwy na tym samym szkielecie.
Wysyłaj duże pliki bezpiecznie z szyfrowaniem end-to-end
Przesyłaj pliki do 10 GB za darmo z szyfrowaniem end-to-end. Bez rejestracji. Twoje pliki są szyfrowane w przeglądarce przed przesłaniem — nikt inny nie może ich odczytać.
Wyślij plik