Course lessons, screen recordings, a creator upload form: files of a few hundred megabytes to a few gigabytes, picked in the browser and played back on a page.
Three choices make that work:
multipart: true. Every video goes up in parts, whatever it weighs, which is what buys pause, resume and per-part retry on a two hour upload.- A unique path, cached forever.
uniquePathpluscache: 'immutable'is one object per video, never overwritten, so its URL can be cached for a year. - A row per video. Written as
pendingbefore the bytes and flipped toreadyafter them, so a page never links a file that is only half there.
The bytes go from the browser straight to storage. Your server authorizes the upload and records what landed, and never carries a gigabyte through a function.
This recipe uses a public bucket. If you have not created one yet, start with the Quickstart.
The handler#
Unlike the other recipes, the row is written in onBeforeUpload, before a byte exists, and marked pending. A tab that dies halfway leaves a row still saying pending, which is what a cron can find and sweep. onUploadComplete only flips it to ready.
onBeforeUpload runs once per upload, so one upload is one row, and the row is the placeholder the rest of the app renders while the upload runs. A resume does not run it again: if the tab closes and the user picks the same file later, the SDK sends only the missing parts, and onUploadComplete flips the row that already exists.
multipart: true also means nothing is stored at the path until the upload completes. A tab that dies halfway leaves parts, which abortStaleMultipartUploads() on a cron clears, and a row still pending, which the same cron deletes once the parts are gone. See Abandoned uploads.
A throw out of onUploadComplete deletes the object, so the catch turns a database failure into a refusal the user can retry from. not_ready is the 503 code, the one that means try again. See Upload handler and Errors.
The route#
The upload component#
A gigabyte takes minutes, so the controls matter more than they do for a picture.
canPause is true throughout because the route sets multipart: true. percent sits at 99 while onUploadComplete runs, which is why finishing gets its own line rather than a bar that looks stuck. If the tab is closed and the user picks the same file again later, the upload resumes from the parts that landed, with no API to call. Large files covers all of that.
Playing it back#
Render the URL from your own row, and let status keep half-uploaded videos off the page.
url is the public object URL, so the browser fetches the file directly and nothing streams through your app. The object is stored once and never overwritten, which is what makes cache: 'immutable' correct here: there is no stale version to worry about, and every play after the first can be served from cache.
For videos that must not be watchable by anyone holding the link, use a private bucket and sign each play with signedReadUrl, asking for an expiresIn comfortably longer than the video runs. See Private documents for that shape.
Deleting#
Delete the row first, then the object. The page stops linking the video immediately, and if the second step fails the leftover is an object nobody links to rather than a player pointing at a 404. del treats an already missing object as success, so it is safe to retry.
Closing an account is the same thing in bulk. The rows are the only thing that knows the paths, so read them first, delete the objects, then drop the rows. If it fails partway, run it again: