Loading...
Loading...
Work with the @upstash/blob TypeScript/JavaScript SDK for S3-compatible object storage with direct browser uploads, presigned URLs, multipart, and signed reads. Use when storing files or blobs, uploading avatars, images, videos, attachments or user documents, letting a browser upload straight to storage without proxying bytes through a server, generating public or time-limited signed URLs, serving private files, streaming large files with pause and resume, setting cache headers on stored objects, or reaching an S3-compatible bucket from the AWS SDK.
npx skill4agent add upstash/skills upstash-blob-jsBucketuploadHandlernpm install @upstash/blobUPSTASH_BLOB_TOKENNEXT_PUBLIC_signedReadUrlimport { Bucket } from "@upstash/blob"
export const bucket = Bucket.fromEnv() // reads UPSTASH_BLOB_TOKEN
Bucket.fromEnv({ cache: "immutable" }) // default variable, plus options
Bucket.fromEnv("MEDIA_TOKEN", { cache: "immutable" }) // another variable, plus options
new Bucket({ token: env.UPSTASH_BLOB_TOKEN }) // Workers: no process.envconst blob = await bucket.put("reports/q3.pdf", pdf, { contentType: "application/pdf" })
blob.url // public URL, undefined on a private bucket
blob.versionedUrl // url + ?v=<etag>, changes whenever the bytes do
blob.etag // what ifUnchanged takesRequestBlobFileArrayBufferstringReadableStreamsizemaxSizeputlength_required| Option | Default | What it does |
|---|---|---|
| the body's, else | What the object is stored as |
| any | Allow list, e.g. |
| none | Refuse a bigger body with |
| bucket default | |
| none | |
| | |
| none | An etag; fails with |
| | Threshold for going up in parts; |
'20mb''5mib'import { uniquePath } from "@upstash/blob"
uniquePath`${user.id}/${file.name}` // 'u7/holiday-pic-3xK9mBqR.png'uniquePath${}photo.png...TypeErrorBlobErrorbucket.copy(from, to, { contentType, cache, metadata })bucket.move(from, to, options)bucket.updateJson(path, fn, { maxAttempts: 6 })const res = await bucket.get("reports/q3.pdf") // record + body: ReadableStream
const info = await bucket.info("reports/q3.pdf") // same record, no bytes (HEAD)
await bucket.exists("avatars/u7.png") // boolean instead of a throw
const page = await bucket.list({ prefix: "avatars/", limit: 1000 })getinfonot_foundawait new Response((await bucket.get("notes/1.md")).body).text()listpage.cursorcontentTypemetadataprefixconst { url, expiresAt } = await bucket.signedReadUrl("private/report.pdf", {
expiresIn: "2m",
downloadAs: "Report Q3.pdf", // save under this name instead of rendering inline
})expiresAtawait bucket.publicUrl(path)undefinedawait bucket.del("avatars/me.png") // one path
await bucket.del(["a.png", "b.png"]) // an array, batched by 1000
await bucket.del({ prefix: "tmp/" }) // everything under a prefixpartial_deletee.faileddel({ prefix: '' })all: truegetUserdb.files.upsert// lib/uploads.ts
import "server-only"
import { BlobError, uniquePath, uploadHandler } from "@upstash/blob"
import { getUser } from "@/lib/auth"
import { db } from "@/lib/db"
export const uploads = uploadHandler({
constraints: { maxSize: "20mb", contentTypes: ["image/*", "application/pdf"] },
onBeforeUpload: async ({ request, file }) => {
const user = await getUser(request)
if (!user) throw new BlobError("unauthorized") // nothing is signed
return { path: uniquePath`${user.id}/${file.name}`, metadata: { owner: user.id } }
},
onUploadComplete: async ({ uploadId, path, url, metadata }) => {
if (!metadata.owner) throw new BlobError("unauthorized")
await db.files.upsert({ id: uploadId, owner: metadata.owner, path, url })
return { path } // becomes upload.blob.data
},
})// app/api/upload/route.ts
import { uploads } from "@/lib/uploads"
export const { GET, POST } = uploads// lib/upload-hooks.ts
"use client"
import { uploadHooks } from "@upstash/blob/react"
import type { uploads } from "./uploads"
export const { useUpload } = uploadHooks<typeof uploads>()"use client"
import { useUpload } from "@/lib/upload-hooks"
export function UploadForm() {
const { start, upload, accept } = useUpload()
return <>
<input type="file" accept={accept} onChange={(e) => start({ file: e.target.files?.[0] })} />
{upload?.pending && <progress value={upload.percent} max={100} />}
{upload?.status === "done" && <a href={upload.blob.url}>{upload.blob.data.path}</a>}
{upload?.status === "error" && <p>{upload.error.message}</p>}
</>
}GETacceptuploadHooks<typeof uploads>()import typeurlundefinedpathbucket.signedReadUrl(path)onUploadCompleteuploadIdonUploadCompletepause()resume()percentstatusdoneupload.pendingmultipart: trueawait bucket.abortStaleMultipartUploads({ olderThan: "1d" })bucket.putuseServerUpload@upstash/blob/reactcache| Value | Stored |
|---|---|
| |
| |
| |
a duration ( | |
immutableuniquePathversionedUrlprivatepublicimport { BlobError } from "@upstash/blob"
if (BlobError.is(e) && e.code === "not_found") return nullBlobError.is()instanceofnot_foundalready_existsconflictcontent_type_not_allowedinvalid_inputtoo_largeempty_bodylength_requiredsignature_mismatchunauthorizedforbiddenrate_limitednot_readypartial_deletemove_left_a_copyinvalid_content_type_patternmint_backoffrequest_failederror.code'5mib'TypeErrorBlobErrorimport { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"
const config = bucket.s3()
const s3 = new S3Client(config) // endpoint and credentials are async providers
// config.bucket is the underlying bucket id, available nowhere else
await s3.send(new GetObjectCommand({ Bucket: config.bucket, Key: "reports/q3.pdf" }))