Frontend Handbook
06Built

File Upload with Progress

Drag-and-drop uploads with per-file progress, cancel, and retry.

How it works

A serious uploader: drop, pick or paste files (or a whole folder), see each one checked before anything is sent, and watch real progress, speed and time left. Files go up in pieces, so a pause, a failure or even a reload only costs the piece in flight, never the whole file.

  • Pieces at a confirmed offset: the server says how much it has; the client only ever continues from there.
  • Honest progress: the solid bar is what the server stored; the light part is still in flight.
  • A queue, not a stampede: only a few files upload at once; pause, resume, cancel and retry each one.
  • Trust, but verify: a SHA-256 checksum proves the stored copy matches the original.
Read more

Where you'd use it

  • Attachments, profile photos and document uploads in any web app.
  • Large media or backup files where restarting a 2 GB upload after a hiccup isn't acceptable.
  • Bulk imports: dropping a folder of hundreds of files without freezing the page.
  • Mobile users on flaky networks, who switch connections or lock their phone mid-upload.

How we built it

Files arrive from a drop (folders are walked with webkitGetAsEntry(), reading every batch of entries), the file picker, or a paste. Each one is checked against the rules (type by MIME or extension, size, empty, how many) before any byte is sent, and the server checks the same rules again. Images get a thumbnail from URL.createObjectURL, released when the row goes away.

Uploading is a small protocol of our own: create an upload, then send pieces with PUT ?offset=N, then complete it with the file's SHA-256. The server only confirms a piece once it is stored, and answers a wrong offset with the offset it really has, so a retry after a lost response never stores anything twice. Pieces are sent with XMLHttpRequest because fetch() has no upload-progress events, and abort() makes pause and cancel instant.

A module-level store runs a queue: at most N files upload at once, failed pieces retry with growing, randomized delays (up to 12 tries), offline uploads wait for the network, and the store survives navigation. Speed and time left come from confirmed bytes: the browser reports the first couple of megabytes as “sent” instantly because they sit in socket buffers. The demo's speed limit is real: the server reads the request slowly, so the browser is held back by the network, not by a fake timer.

Best practices

  • Do: Send large files in pieces and let the server own the offset.

    Avoid: One request per file: any failure or pause starts the whole file over.

  • Do: Show progress from bytes the server confirmed, and estimate time from that rate.

    Avoid: Trusting upload-progress events alone: buffers make them race ahead, then the bar jumps back on failure.

  • Do: Validate on the client for fast feedback, and again on the server.

    Avoid: Relying on the browser's checks, or on the file's name, for anything that matters.

  • Do: Limit how many files upload at once and keep the rest queued.

    Avoid: Starting 100 uploads together: they fight over bandwidth and nothing finishes early.

  • Do: Keep the drop zone usable without a mouse (real buttons for picking files) and announce results, not every percent.

    Avoid: A drop-only zone, or a live region that reads out every progress update.

Try it with the demo controls

Add sample files
Six files appear: four upload and verify, “empty.txt” and “setup.exe” are rejected with the reason before anything is sent.
Upload speed
At 256 KB/s a 4 MB file takes about 16 s; the shown speed settles near 256 KB/s and the time left counts down accurately. Files uploading together share the speed.
Piece failure
At 50%, rows show “Retrying (try N of 12)” but still finish verified; only failed pieces are sent again.
At once
Drop more files than the limit: the rest show “Queued #N”. Lower it while uploading and the extra files finish their current piece, then wait.
Piece size
Smaller pieces make the solid bar move more often; larger ones mean fewer requests but more to resend after a failure.
Max file size / Max files / Allowed types
Apply to files added afterwards, here and on the server; files over the limits are listed as “Not added” with the reason.
Pause / Resume / Cancel / Retry
Pause stops the network at once and frees the slot; Resume continues from the server's offset (at most one piece is sent again); Cancel also deletes what the server received.
Pause all / Resume all / Cancel all / Clear finished
Act on every file they concern; Clear finished removes done and rejected rows only.
Choose a folder, drop a folder, or paste
Every file inside, including subfolders, is added with its path; a pasted screenshot uploads like a dropped file.
Reload the page mid-upload
The file shows “Interrupted” with its progress; add the same file again and it continues from where it stopped.
Uploaded files on the server
Each finished file with size, time and “Checksum matches”; delete removes it from the server. Storage is capped at 200 MB, oldest first.

Trade-offs and limits

  • Our own small protocol instead of a standard one, so it is easy to read; a production app would likely use a resumable-upload standard or a storage service's multipart API.
  • Resuming after a reload needs the user to add the same file again: browsers don't keep file access across reloads.
  • Files live in a temp folder for the session (cleared when the server starts); no processing, sharing or virus scanning.
  • The checksum is computed from the whole file in memory, which is fine at 50 MB but would need streaming hashing for very large files.

Demo controls

Allowed types

Drop files or a folder here, or paste them

Images, PDF, Text, CSV, JSON, ZIP · up to 50 MB each · 20 files at a time

Uploads

No uploads yet. Add files above, or use “Add sample files” in the demo controls.

Uploaded files on the server

Nothing stored yet.