Resumable uploads
Pause, resume, and abort a large upload through a control handle — and resume it in a later process from a serializable session token. A per-call option on upload, for the adapters whose provider exposes a resumable session.
A long upload over a flaky connection shouldn't have to start over when it's interrupted. Pass an UploadControl to upload and you can pause() it, resume() it, or abort() it — and, because the control holds a serializable session token, you can persist it and resume the upload in a later process, after a crash, or on the next page load.
import { Files, UploadControl } from "files-sdk";
const control = new UploadControl();
const result = files.upload("backups/db.tar", file, {
control,
multipart: { partSize: 16 * 1024 * 1024 },
onProgress: ({ loaded, total }) =>
console.log(total ? `${Math.round((loaded / total) * 100)}%` : `${loaded}`),
});
control.pause(); // in-flight parts settle; `result` stays pending
control.resume(); // pick up where it left off
await result;It's an AbortSignal-style handle: a plain object you construct and hand in, then drive from the outside. The upload call returns its usual promise, which resolves when the upload completes (and stays pending while paused).
Resuming across processes
control.toJSON() returns a small JSON-serializable token describing the provider-side session. Persist it wherever you like — disk, localStorage, a database row keyed by the user's upload. Later, rebuild a control from it with UploadControl.from(token) and call upload again with the same body: the SDK discovers what already landed server-side and uploads only the rest.
// First run — pause and persist.
const control = new UploadControl();
files.upload("backups/db.tar", file, { control }).catch(() => {});
// …once a session exists, control.toJSON() is populated…
localStorage.setItem("upload", JSON.stringify(control.toJSON()));
// Later — a new tab, a new process, after a crash.
const token = JSON.parse(localStorage.getItem("upload")!);
const result = await files.upload("backups/db.tar", file, {
control: UploadControl.from(token),
});Because resuming re-reads the bytes, control requires a body with a known length — a File, Blob, ArrayBuffer, typed array, or string. A bare ReadableStream is rejected: a consumed stream can't be replayed. (Keep the File handle around, the way a browser upload widget does.)
Pause, abort, and the session
pause()stops dispatching new parts. Parts already in flight finish, then the upload waits. The session is preserved, so this is the moment totoJSON()and persist.resume()continues a paused upload.abort()cancels the upload and discards the provider-side session — the partial upload a provider might otherwise bill or retain is cleaned up. It's terminal: the token can no longer be resumed.
Aborting via the signal option instead cancels the call but keeps the session, so you can resume it later. control.status ("idle", "uploading", "paused", "completed", "aborted", "error") and control.loaded / control.total track progress for a UI.
What each adapter does
The token maps onto whatever resumable primitive the provider exposes. Most adapters resume across processes — persist the token, resume after a crash:
- S3 and the S3-compatible adapters (incl. R2 over HTTP) drive S3's native multipart API directly — the token carries the
UploadId, resume lists already-uploaded parts withListParts, andabort()issuesAbortMultipartUpload. (This is a separate path from themultipartflag's@aws-sdk/lib-storageupload, which doesn't expose the upload id.) - GCS, Firebase Storage, and Google Drive resume against a stored resumable-session URI.
- Azure Blob stages blocks and commits them, skipping blocks already staged on resume.
- OneDrive drives a Graph upload session and reads
nextExpectedRangesto resume. - Dropbox drives an upload session, tracking the byte offset in the token.
- Vercel Blob drives its native multipart API; the token carries the completed parts (it exposes no server-side list).
- The local filesystem writes a
.fls-parttemp file and resumes from its size, renaming it into place on completion. - FTP and SFTP resume by querying the remote file's size and appending the rest.
- Supabase drives the resumable TUS endpoint; Appwrite and Cloudinary drive their chunked
Content-Rangeuploads.
A few providers expose no serializable session, so they support pause/resume in-process only — control.toJSON() can't be resumed in a new process: Box (its commit requires a whole-file digest), bun-s3, and memory (no upload id). They buffer the body in the running process and upload on completion.
Every remaining adapter — Netlify Blobs, UploadThing, PocketBase, Bunny, Convex, and the rest — throws a clear "not supported" FilesError when control is passed, the same way an unsupported range download does. control is a single-key option; it isn't available in the array form of upload.
The Supabase (TUS), Appwrite, and Cloudinary drivers are built to each provider's documented chunked-upload protocol and covered by mocked tests, but haven't been exercised against a live account — verify them end-to-end before relying on them in production.
Parts, retries, and progress
partSize and concurrency come from the multipart option and tune the same trade-off. Each part (or chunk) is retried on its own under the call's retry policy, so a transient failure mid-upload doesn't restart the whole transfer. onProgress fires as parts confirm, and on resume it starts from the bytes already uploaded.
Read-only
Lock a Files instance to reads only with `readonly: true` or `files.readonly()`, and make every write surface fail consistently with `FilesError { code: "ReadOnly" }`.
Retries
Automatic retries for transient provider failures, with a configurable backoff curve and clear rules for what is and isn't retried.