Upload Store
Persist multipart uploads and resume when the user selects the same file again.
An UploadStore keeps what is needed to resume a multipart upload: the
upload ticket, the object key, the upload id, and the part layout. It does not
keep the File. After a reload, the user selects the same local file and the
upload continues from the parts S3 already has.
import { createLocalStorageStore } from "@dimah-s3/react/engine";
export const uploadStore = createLocalStorageStore();"use client";
import { useUpload } from "@dimah-s3/react";
import { uploadStore } from "@/lib/upload-store";
export function VideoUploader() {
const upload = useUpload({ route: "video", uploadStore });
return (
<div {...upload.getRootProps()}>
<input {...upload.getInputProps()} />
{upload.isPending
? `Uploading: ${upload.progress.percent}%`
: "Upload a large video"}
</div>
);
}Only multipart uploads are stored: the route must allow multipart, and the
file must be at least 50 MiB (DEFAULT_MULTIPART_THRESHOLD).
How resume works
- After
multipart.init, the engine saves aStoredUploadbefore it sends any part. detach(), a closed tab, or an unmount stops the upload but keeps the record and the server session.- When the same file is selected again, the engine finds the record, lists the parts already uploaded, and sends only the rest.
- Once the upload completes or is cancelled, the record is deleted.
- A failure that may pass on a later try, such as a network error or S3 answering
429or5xx, keeps the record. A failure that retrying cannot fix, such asFORBIDDENorCHECKSUM_MISMATCH, aborts the server upload and deletes the record.
If the ticket has expired, or the server no longer knows the upload, the engine aborts it, deletes the record, and starts over.
The record is looked up by multipartResumeKey: the route, the file's name,
size, and last-modified time, plus the request's fileName, contentType,
and metadata. So a file resumes only with the same request options, and
never completes into another folder. The key is a lookup key, not a
collision-resistant hash.
Built-in stores
import {
createLocalStorageStore,
createMemoryStore,
} from "@dimah-s3/react/engine";
const localStore = createLocalStorageStore();
const memoryStore = createMemoryStore();createLocalStorageStore()— keeps records inlocalStorageacross reloads. It does nothing whenlocalStorageis unavailable, such as during SSR, in private browsing, or with a full quota.createMemoryStore()— keeps records in memory, so uploads resume only within the page session. Safe for SSR and tests.
Records with an unknown shape, such as ones written by 1.x, are ignored and deleted.
Custom store
Implement UploadStore to keep records anywhere else, such as IndexedDB or
your own API:
import type { UploadStore } from "@dimah-s3/react/engine";
export const apiStore: UploadStore = {
async get(resumeKey, fileSize) {
const res = await fetch(`/api/uploads/${encodeURIComponent(resumeKey)}`);
if (!res.ok) return null;
const upload = await res.json();
return upload.fileSize === fileSize ? upload : null;
},
async set(upload) {
await fetch(`/api/uploads/${encodeURIComponent(upload.resumeKey)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(upload),
});
},
async delete(resumeKey) {
await fetch(`/api/uploads/${encodeURIComponent(resumeKey)}`, {
method: "DELETE",
});
},
};UploadStore
Prop
Type
StoredUpload
Prop
Type
Frequently asked questions
No. @dimah-s3/db tracks pending uploads on the server. An
UploadStore maps a browser-side resume key to the ticket and upload id. Use
createLocalStorageStore() for reloads in the same browser, or a custom store
for other devices. Either way, the user must select the same local file.
Set uploadStore for large files that users may need to resume after a
refresh or a lost connection. Small files upload in one request and are never
stored.
Files whose call had an uploadStore are detached, so they can resume. Files
without one are cancelled.