mcpField
U
upstash-blob-js

upstash-blob-js

github.com/upstash云与 DevOps34528 次安装1 天前更新
代码仓库

关于 upstash-blob-js

@upstash/blob SDK 是面向 JavaScript/TypeScript 应用的 S3 兼容对象存储 SDK。它提供服务端存取能力,以及让浏览器直接上传到存储的处理器和 React hooks。

主要功能

  1. 01S3 兼容对象存储操作
  2. 02支持流式读写与分段上传
  3. 03公共与私有对象读取
  4. 04浏览器直传及 React hooks
  5. 05复制、移动、删除和列举对象
  6. 06签名读取链接与缓存控制

适用场景

  1. 01在服务端保存和读取文件
  2. 02让浏览器上传文件而不经过应用服务器
  3. 03为私有对象生成限时读取链接

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/upstash/skills --skill upstash-blob-js

安装后在 Claude Code 中输入/upstash-blob-js确认已加载。

详细介绍

@upstash/blob SDK

兼容 S3 的对象存储。Bucket 在服务器上运行;uploadHandler 配合 React hooks 可从浏览器直接上传到存储,因此字节数据不会经过你的应用。

内置文档和源码

@upstash/blob npm 包包含位于 node_modules/@upstash/blob/docs/ 的文档,以及位于 node_modules/@upstash/blob/src/ 的可读 TypeScript 源码。编写或调试 SDK 代码前,请阅读并搜索相关文档和源码。

安装和设置

npm install @upstash/blob

在控制台中创建一个存储桶,并设置 UPSTASH_BLOB_TOKEN。此令牌是整个存储桶的 bearer 密钥——请将其保留在服务器端,切勿放入 NEXT_PUBLIC_ 变量或任何由打包工具内联的变量中。

存储桶可以是公开的(每个对象都有 URL),也可以是私有的(没有 URL;读取需通过 signedReadUrl)。这是一项控制台设置,而非客户端选项——SDK 会从后端获取此设置。

import { 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.env

写入

const 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 takes

请求体支持:Request、Blob/File、ArrayBuffer、类型化数组、string、ReadableStream。流没有长度信息——请传入 size(精确大小,数据以流的方式传输)或 maxSize(缓冲到上限),否则 put 会抛出 length_required。

选项默认值作用
contentType请求体的类型,否则为 application/octet-stream对象存储时使用的类型
contentTypes任意允许列表,例如 ["image/*", "application/pdf"]
maxSize无请求体过大时以 too_large 拒绝
cache存储桶默认值与对象一同存储的 Cache-Control
metadata无x-amz-meta-*;键需为小写,值需为可打印的 ASCII 字符
allowOverwritetrue设为 false 时,若对象已存在则以 already_exists 拒绝
ifUnchanged无一个 etag;若对象已更改,则以 conflict 失败
multipart'16mb'分段上传的阈值;设为 true/false 可强制启用或禁用

大小使用十进制:'20mb' 是 20,000,000 字节。'5mib' 会抛出异常。

import { uniquePath } from "@upstash/blob"

uniquePath`${user.id}/${file.name}`   // 'u7/holiday-pic-3xK9mBqR.png'

对任何不受你控制的值使用 uniquePath。每个 ${} 都会变成一个经过 slug 处理的文件名,绝不会额外添加目录;最终路径还会添加随机后缀,因此两个 photo.png 上传不会发生冲突。模板中的字面部分会按原样传递,因此请自行避免在其中使用 . 和 ..:带有这些片段的路径之后会被使用它的调用拒绝,并抛出 TypeError 而非 BlobError。

bucket.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 })

get/info 会抛出 not_found。数据不会被缓冲——请包装流来读取:

await new Response((await bucket.get("notes/1.md")).body).text()

list 通过 page.cursor 分页(仅当后续还有内容时才会设置),且不包含 contentType 或 metadata。唯一的筛选条件是 prefix——请将自己的表作为索引,并将存储桶视为存储空间,而非可查询的数据存储。

const { url, expiresAt } = await bucket.signedReadUrl("private/report.pdf", {
  expiresIn: "2m",
  downloadAs: "Report Q3.pdf",   // save under this name instead of rendering inline
})

将链接缓存到 expiresAt,切勿自行计算截止时间——链接的有效期不能超过为其签名的凭证,因此实际有效期可能短于你请求的时长。await bucket.publicUrl(path) 会返回公开 URL;对于私有存储桶则返回 undefined。

删除

await 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 prefix

对象已不存在也视为成功,因此删除操作可以安全重试。如果数组删除或前缀删除后仍有对象残留,则会抛出 partial_delete,残留对象位于 e.failed 中。除非传入 all: true,否则会拒绝执行 del({ prefix: '' })。

浏览器上传

处理程序负责授权和记录;数据字节从浏览器直接传到存储,因此不受平台请求正文大小限制。请在下方提供应用的 getUser 和 db.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>}
  </>
}

GET 会提供路由的限制条件,因此 accept 会填充文件选择对话框,超大文件也会在浏览器发出任何请求之前被拒绝。uploadHooks<typeof uploads>() 会在编译时为路由名称和完成数据提供类型;import type 可避免将服务端代码打包进去。

此示例假设使用公开存储桶。对于私有存储桶,url 为 undefined,因此应存储 path,并在客户端需要读取时向其提供 bucket.signedReadUrl(path)。

以下两条规则容易踩坑:

  • onUploadComplete 可能会运行多次。 浏览器会重试,因此应按 uploadId 执行 upsert,而不是插入。
  • 从 onUploadComplete 抛出错误会删除对象。 对于拒绝上传,这是正确的;对于暂时性的数据库错误则不合适——请自行捕获存储错误。

超过 16 MB 的文件会分块上传,因此才能使用 pause()、resume()、逐块重试,以及用户再次选择同一文件后在重新加载页面时恢复上传。status 为 done 之前,percent 最高为 99;请使用 upload.pending 驱动 UI。

在处理程序上使用 multipart: true,即可让所有上传都采用分块方式。这样,关闭的标签页只会留下未完成的分块,而不会留下无人记录的已存储对象;再用一个 cron 清理即可:

await bucket.abortStaleMultipartUploads({ olderThan: "1d" })

对于数据字节必须经过应用的路由,请编写一个调用 bucket.put 的普通路由,并使用 @upstash/blob/react 中的 useServerUpload 驱动上传。

缓存

cache 仅在上传时写入一次,并与对象一起存储——要更改它,必须重新写入对象。

值存储的内容
'immutable'public, max-age=31536000, immutable
'revalidate'public, max-age=0, must-revalidate
'no-store'no-store
时长('15m'、3600)public, max-age=<seconds>

immutable 需要一个会随数据字节变化的路径——可以为每次上传设置 uniquePath,也可以通过 versionedUrl 提供稳定路径。在私有存储桶上,private 会替代 public。

错误

import { BlobError } from "@upstash/blob"

if (BlobError.is(e) && e.code === "not_found") return null

请使用 BlobError.is(),切勿使用 instanceof——ESM 和 CJS 中的副本属于不同的类。错误代码:not_found、already_exists、conflict、content_type_not_allowed、invalid_input、too_large、empty_body、length_required、signature_mismatch、unauthorized、forbidden、rate_limited、not_ready、partial_delete、move_left_a_copy、invalid_content_type_pattern、mint_backoff、request_failed。

拒绝操作会将其错误代码一路传递到浏览器,因此钩子应根据 error.code 分支处理,而不是根据状态码。无效的选项值('5mib'、缺失的令牌)会在写入这些值的位置抛出 TypeError,而不会针对每个请求抛出 BlobError。

S3 客户端

import { 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" }))

存储桶兼容 S3。对于 SDK 未封装的功能(字节范围、条件 GET、标签),请使用此方式。请按原样传递提供程序,以便 AWS SDK 能够刷新过期的凭证。

评论

支持 Markdown