Multipart & chunked

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

  1. A failed part is retried with backoff without touching other parts.
  2. 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.

On this page