A provider-agnostic engine for chunked, parallel, resumable uploads.
"use client"
import {
type CreateMultipartResponse,
MiB,
type SignPartResponse,
type StoredObject,
multipartAdapter,
postJson,
} from "@uploadcn/core"
import { demoTransport } from "@/examples/_demo"
import {
Upload,
UploadDropzone,
UploadDropzoneDescription,
UploadDropzoneTitle,
UploadQueue,
} from "@/components/ui/upload"
/**
* `multipartAdapter` is provider-agnostic: bring `create`, `uploadPart` and
* `complete`. Here it talks to the same upload route as `s3Adapter`, with
* 5 MiB parts, 3 in parallel, each retried independently.
*/
const adapter = multipartAdapter<
{ key: string; uploadId: string },
StoredObject
>({
partSize: 5 * MiB,
concurrency: 3,
create: ({ file, signal }) =>
postJson<CreateMultipartResponse>(
"/api/upload",
{
action: "create-multipart",
file: { name: file.name, type: file.type, size: file.size },
},
{ signal }
),
async uploadPart({ session, partNumber, blob, signal, onProgress }) {
const { url } = await postJson<SignPartResponse>(
"/api/upload",
{ action: "sign-part", ...session, partNumber },
{ signal }
)
// Docs only: `demoTransport` plays the bucket when the demo has no
// credentials. In your app, use `xhrTransport` from @uploadcn/core.
const response = await demoTransport({
method: "PUT",
url,
body: blob,
signal,
onUploadProgress: onProgress,
})
return { etag: response.headers.get("etag") ?? "" }
},
complete: ({ session, parts, file, signal }) =>
postJson<StoredObject>(
"/api/upload",
{
action: "complete-multipart",
...session,
parts: parts.map(({ partNumber, etag }) => ({ partNumber, etag })),
file: { name: file.name, type: file.type, size: file.size },
},
{ signal }
),
abort: ({ session }) =>
postJson("/api/upload", { action: "abort-multipart", ...session }).then(
() => {}
),
})
export default function ChunkedUploadExample() {
return (
<Upload adapter={adapter}>
<UploadDropzone>
<UploadDropzoneTitle>Drop a file larger than 10 MB</UploadDropzoneTitle>
<UploadDropzoneDescription>
Split into 5 MB parts · 3 in parallel · per-part retry · pausable
</UploadDropzoneDescription>
</UploadDropzone>
<UploadQueue />
</Upload>
)
}
multipartAdapter splits files into parts, uploads them in parallel, retries failed
parts independently, reports aggregate and chunk progress, and records finished parts
so uploads resume after a pause, a network loss, or a page reload.
You bring three functions; it works with S3's multipart API, GCS's XML API, Azure block blobs, or your own chunk endpoint.
import { multipartAdapter, MiB } from "@uploadcn/core"
const adapter = multipartAdapter({
partSize: 8 * MiB,
concurrency: 4,
retry: { retries: 3, baseDelay: 500 },
create: ({ file }) => api.createUpload(file), // → session
uploadPart: ({ session, partNumber, blob, signal, onProgress }) =>
api.putPart(session, partNumber, blob, { signal, onProgress }), // → { etag }
complete: ({ session, parts }) => api.complete(session, parts), // → result
abort: ({ session }) => api.abort(session),
listParts: ({ session }) => api.listParts(session), // optional
})Options
Prop
Type
Resume state
After each finished part the adapter saves:
{ session, partSize, fileSize, parts: [{ partNumber, etag, size }] }With persistence, this survives reloads. When the saved state doesn't match the file (different size or part size), the upload starts over cleanly.
Retries at two levels
- A failed part is retried with backoff without touching other parts.
- If a part exhausts its retries, the item fails; the engine's own retry policy then retries the item, which resumes from the parts already stored.