Multipart Hooks
Large-file multipart uploads — the server's part layout, part authorization, and assembly.
Enable multipart support on large-file routes by setting upload.multipart: true or passing a MultipartConfig object.
import { dimahS3, route } from "@dimah-s3/server";
import { awsS3 } from "./aws-s3";
export const s3 = dimahS3({
client: awsS3,
bucket: process.env.S3_BUCKET!,
routes: {
video: route({
upload: {
fileTypes: ["video/*"],
maxFileSize: 500 * 1024 * 1024, // 500MB
multipart: true,
onConfirmed: async ({ key, object }) => {
// Runs once multipart complete has verified the object
},
},
}),
},
});Multipart lifecycle flow
- upload.guardInit
- CreateMultipartUploadS3
- onStarthook
- multipart.guardEach part, list, complete, abort
- CompleteMultipartUploadS3
- confirmGuardhook
- onConfirmedhook
- Init runs
upload.object, picks the part layout, runsupload.guard, creates the multipart upload, and runsupload.onStart(kind: "multipart"). IfonStartthrows, the upload is aborted. The response has theuploadId, the upload ticket,partSize,partCount, and whether parts need achecksum. - Sign part runs
multipart.guardwithaction: "part"and signs a PUT URL for exactly that part's size. - List parts (
action: "list") returns the parts S3 already has, so a client can resume. - Complete (
action: "complete") checks every part, assembles the object, reads it with HeadObject, and runsconfirmGuardandonConfirmed, the same as a single-shot confirm. - Abort (
action: "abort") aborts the upload and runsmultipart.onAbort. Like delete, it is idempotent: an upload that is already gone still succeeds and runsonAbort. An upload that was completed fails with409 CONFLICT.
Every call after init sends only the ticket. The route guard runs on each one.
Part layout
The server picks the layout at init from the declared size, and the client follows it. Init does not accept a part size.
partSizestarts atupload.multipart.partSize(8 MiB by default).- It grows when the file would need more than 10,000 parts, is rounded up to a whole MiB, and stays between 5 MiB and 5 GiB.
- Every part except the last is exactly
partSize. The last one holds the rest. - Files over 5 TiB return
FILE_TOO_LARGE.
Sign part rejects a part number outside 1..partCount with
MULTIPART_PART_INVALID, and never calls ListParts. Complete reads every
ListParts page and checks:
- parts
1..partCountare all present, otherwiseMULTIPART_PART_MISSING; - each part has exactly its layout size, otherwise
UPLOAD_SIZE_MISMATCH; - with
checksumon, each part has a SHA-256, otherwiseCHECKSUM_REQUIRED.
Part checksums
With upload.checksum (or verifyContent) on, every part must send its
SHA-256 to sign part, and the server signs it into the part URL. S3 rejects a
part whose bytes do not match. The React client hashes each part as it
streams.
Retries
Complete is safe to retry. When an earlier call already completed the
upload, the stored object is verified and the hooks run again, as long as it
carries this upload's marker. Like confirm, onConfirmed runs at least once:
dedupe on uploadToken.
Ticket lifetime
A multipart ticket is valid for upload.multipart.ticketExpiresIn seconds,
one day by default. After that, every call returns UPLOAD_TICKET_EXPIRED,
except abort, which accepts the ticket for 7 more days.
upload: {
multipart: {
partSize: 16 * 1024 * 1024,
ticketExpiresIn: 6 * 60 * 60,
},
},Add an S3 lifecycle rule that aborts incomplete multipart uploads, so parts of uploads nobody completes or aborts do not stay in the bucket.
Type reference
import type {
MultipartConfig,
MultipartGuardContext,
MultipartOnAbortContext,
MultipartUploadContext,
} from "@dimah-s3/server";MultipartConfig
Prop
Type
MultipartUploadContext
Shared by multipart.guard and multipart.onAbort. Init uses
UploadGuardContext and UploadOnStartContext from
Upload Hooks.
Prop
Type
MultipartGuardContext
Prop
Type
MultipartOnAbortContext
Prop
Type
Frequently asked questions
No. Complete runs upload.confirmGuard, queries HeadObject, and calls
upload.onConfirmed.
Set upload.maxFileSize. Init checks the declared size against it before
S3 is called, and the layout fixes each part's size, so the parts add up to
exactly the declared size. Complete rejects any part of another size, and
confirm checks the stored size again.
Branch on action in multipart.guard:
upload: {
multipart: {
guard: ({ action, locals }) => {
if (action === "complete" && !locals.userId) {
throw s3Error("UNAUTHORIZED");
}
},
},
},