Split large files into parts that upload in parallel and retry independently.
"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>
)
}
Chunking solves three problems with large files:
- Reliability: a dropped connection loses one part, not the whole file.
- Speed: several parts upload in parallel, which saturates bandwidth better than one stream.
- Resumability: finished parts are recorded, so uploads continue after a pause or reload.
With S3 or R2
Nothing to write: s3Adapter switches to multipart above its threshold.
s3Adapter({ endpoint: "/api/upload", multipart: { threshold: 32 * MiB, partSize: 8 * MiB, concurrency: 4 } })With any other backend
Use multipartAdapter and implement create, uploadPart
and complete against your API. The example above talks to the same route as
s3Adapter, written out by hand.
Choosing a part size
- S3 and R2 require parts of at least 5 MiB (except the last) and at most 10 000 parts.
getPartSizegrows the part size automatically for huge files. - Larger parts mean fewer requests; smaller parts mean less work lost on failure and finer-grained progress. 8–16 MiB is a good default.
concurrencyof 3–6 is usually optimal; more rarely helps and competes with the page.
Chunk progress
Items expose chunks: { completed, total } alongside byte progress:
const item = useUploadItem()
item.chunks && `${item.chunks.completed}/${item.chunks.total} parts`