@broberg/media

中文文档
0.7.0发布于 2026/11/03MITTypeScriptESM + CJS构建来源已验证近 30 天下载 3203

车队的提供者-不可知媒体存储外观——一个创建Media() API (upload = 获得 = 署名 Url = 删除 = public Url) 超越可互换存储提供者, 因此提供者之间的后一动作从不触及呼叫站点 。 a Cloudflare R2供应商(S3-co)

中文使用文档
文档目录8 节
  1. 使用量
  2. API 密码
  3. 公用URL (v
  4. 信件的附件(v. 4.0, F006.7)
  5. 列出并删除文件夹( v0. 5.0, F006. 8)
  6. 大小、时间、底格里斯和桶清单(0.7.0、F059.4)
  7. 提供水桶
  8. 多段上传——一个请求的文件太大了(0.6.0,F099.20)

@broberg/媒体

舰队的 提供者不可知的媒体存储外观 一个 createMedia() API 密码 (单位:千美元)upload • 妇女 signedUrl • 妇女 delete超过可互换存储供应商,所以稍后 在后端之间移动从不触及呼叫站( @broberg/ai-sdk 模式, 为对象存储。 船舶 云纹R2; 配置扩展为 S3 / Supabase / GCS 不改变代码.

npm i @broberg/media

R2供应商会说 S3 API 并用 aws4fetch (tiny, 0-dep,运行于节点 ^ Bun → 边缘 / Workers)-无 AWS SDK.

使用量

import { createMedia } from "@broberg/media";

const media = createMedia({
  provider: "r2",
  accountId: process.env.R2_ACCOUNT_ID!,
  accessKeyId: process.env.R2_ACCESS_KEY_ID!,
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
  bucket: "assets",
  jurisdiction: "eu",          // pin EU data-residency (GDPR); must match how the bucket was created
  keyPrefix: "tenants/acme/",  // optional multi-tenant isolation
  publicBaseUrl: "https://media.example.com", // optional — enables publicUrl() (public bucket)
});

const { key } = await media.upload("logo.png", bytes, { contentType: "image/png" });
const signed = await media.signedUrl(key, { expiresIn: 600 }); // presigned GET, no public bucket
const stable = media.publicUrl(key);                            // stable public URL (needs publicBaseUrl)
await media.delete(key);                                        // idempotent — a missing key is not an error

API 密码

方法回返页:1
upload(key, body, opts?){ key }返回时 逻辑 键( 安全地反馈给其他人); opts.contentType 编号 : opts.cacheControl
signedUrl(key, opts?)string (同步)限时预签名获取( E)opts.expiresIn 秒,默认为3600 ——不需要公共桶
publicUrl(key)string (单位:千美元)同步(中文(简体) )稳定、非过期的公共 URL; 需要 publicBaseUrl + 公共桶; 掷出 若为 publicBaseUrl 未设置
delete(key)void一能者(404人容忍)

每一种方法都采用 逻辑 键; keyPrefix 内部应用(带有 单人 /被剥去的鞭子 keyPrefix:"tenants/acme" + 键 "/logo.png" → tenants/acme/logo.png。因为 upload() 返回逻辑密钥,并存储 传回它,从不双前缀.

v.2.0(行为): upload() 现在返回 逻辑 (无前缀)密钥, 而不是前缀的,所以绕行进入 signedUrl编号 :delete编号 :publicUrl 实值 对称 仅在使用时可观察到 keyPrefix (和圆通被打破) (见前文)。 还新建: publicUrl(key) + 键 publicBaseUrl。 。 。 .

公用URL (v

signedUrl() 过期 — 嵌入已发布内容的图像错误 (新闻丰富的文本,a 发送 (电子邮件)。 准备 publicBaseUrl (这桶的R2) 自定义域或 r2.dev URL)和使用 publicUrl(key) ——稳定而无期 URL, 构建同步( 没有签名/ IO) , 因此它会直接降入模板 .

仅包 构造 uRL——水桶必须实际公开 可读取. 绑定自定义域名( 通过 dns-mcp,比如说. media.example.com)或启用 这桶 r2.dev 公共 URL,然后设置为 publicBaseUrl直到它被设定, publicUrl() 扔出(公众不要走——没有意外暴露).

信件的附件(v. 4.0, F006.7)

@broberg/media/attachments 是让存储文件成为保险箱的部分 附录:文件的真面目是什么, 无论它可能进来, 它居住的地方, 和如何服务它。 纯函数,无 I/O;从卡片上传路径被解除.

import { createMedia } from "@broberg/media";
import { validateAttachment, attachmentKey, attachmentHeaders, sha256Hex, type AttachmentRecord } from "@broberg/media/attachments";

const store = createMedia({ provider: "r2", /* … */ jurisdiction: "eu", keyPrefix: `tenants/${tenantId}/` });

const v = validateAttachment({ bytes, filename });          // the BYTES decide
if (!v.ok) return new Response(v.reason, { status: 400 });
const id = crypto.randomUUID();
const key = attachmentKey(id);                               // attachments/<id> — never the file name
await store.upload(key, bytes, { contentType: v.contentType });
const row: AttachmentRecord = { id, messageId, key, contentType: v.contentType, bytes: v.bytes, filename, sha256: await sha256Hex(bytes) };

// serving it through your app:
const obj = await store.get(row.key);
return new Response(obj!.bytes, { headers: attachmentHeaders(row.contentType, row.bytes, { filename: row.filename }) });
  • 字节决定类型. 第一个字节是嗅觉(png, jpeg, gif, webp,), (原始内容存档于2018-09-28). heic, pdf, office. 存储 v.contentType,从来没有浏览器。 一个名字 承诺一个图像或一个PDF 字节不交付会被拒绝,因此 程序,无论它叫什么。 故意没有参数 索赔类型.
  • 默认列表 : 图像,pdf, doc/docx,xls/xlsx,ppt/pptx,txt,csv,md,json. 没有 svg/html(他们可以携带脚本),也没有档案. 每个应用程序扩展 allowedExt禁止的地板(exe, s, js, dmg,.)永远不能重新开放.
  • 限制 默认的 25 MB(maxBytes) (中文(简体) ).
  • 服务: 图像和 PDF 渲染内入; 其他的都是后方下载 社区支助方案 sandbox + 键 nosniff文件名通过 @broberg/http contentDisposition这样 faktura-æøå.pdf 保留它的名字.
  • 不在这里: hTTP 路由和数据库表格(您的), PDF 缩略图, 病毒扫描、图像重编码( name@broberg/media-transform) (中文(简体) ).

列出并删除文件夹( v0. 5.0, F006. 8)

const { keys, cursor } = await media.list("kunder/42", { limit: 1000 }); // logical keys, sorted
const { deleted } = await media.deletePrefix("kunder/42");              // e.g. a GDPR erasure
  • 前缀为文件夹. "kunder/1" 意味着一切在 kunder/1/永远 kunder/10/… ——作为平庸的字符串匹配,一个GDPR删除会占用另一个客户的文件.
  • 钥匙回来 逻辑 (键前缀剥去),喜欢 upload() 返回它们。 页码 1–1000( 默认为 1000); cursor 仅当更多时才设置.
  • deletePrefix("") 拒绝:它会清空商店。 如果一个删除报告成功但对象保留, deletePrefix 停止错误而不是循环.
  • r2使用S3 List ObjectsV2;卷行文件夹. 从0.7.0 r2's开始 list() 以欧盟活桶衡量(见下文).

大小、时间、底格里斯和桶清单(0.7.0、F059.4)

const { objects = [] } = await media.list("db");          // [{ key, size, lastModified }] — same order as keys
const newest = [...objects].sort((a, b) => b.lastModified.localeCompare(a.lastModified))[0];

const tigris = createMedia({ provider: "tigris", accessKeyId, secretAccessKey, bucket: "backups" }); // fly.storage.tigris.dev
const buckets = await listBuckets({ provider: "r2", accountId, jurisdiction: "eu", accessKeyId, secretAccessKey });
// [{ name, createdAt }]
  • objects 坐在旁边 keys 与 size (字节)和 lastModified (国际标准化组织,协调世界时)。 每个内置提供者都填充它;它只是可选的,所以手写测试双还原 { keys } 仍在编译.
  • tigris s3 代码与 r2 在另一个主机上endpoint 覆盖 https://fly.storage.tigris.dev因此,每个 MediaStore 方法也一样。 错误说 media(tigris)。 。 。 .
  • listBuckets() 是一个单独的函数,因为一个桶列表属于账户,而不是一个桶。 为R2, jurisdiction 选择主机:一个欧盟桶只在欧盟主机上出现. 它需要一个 账户级别 密钥 —— 被桶镜瞄准的 R2 令牌 403 AccessDenied,然后被扔出,从不作为空列表返回.
  • 测量了 3/11 2026: list() 对欧盟活桶 broberg-call-recordings 返回大小、时间和光标的顺序与 keys。 。 。 . 未进行现场测量: 一个成功的 listBuckets() (我们的钥匙被用桶镜扫描并得到了上面的403)和底格里斯(我们没有底格里斯钥匙)——两者都用被接住的取来来测试真实的S3 XML.

提供水桶

这个包 消耗量 现有桶 + S3 证书。 创建 欧盟管辖的R2桶+范围认证 100%在程序上(没有仪表板), 使用舰队的 dns-mcp R2 Client / MCP 工具(r2_create_bucket, (中文(简体) ), r2_create_scoped_token)——见 云雾花 开卡 broberg.ai (英语)。 。 。 . 欧盟的管辖范围设定在创建时,而且不可改变.


部分 broberg.ai 共享库存搜索 在构建之前 : GET https://discovery.broberg.ai/api/search?q=storage。 。 。 .

多段上传——一个请求的文件太大了(0.6.0,F099.20)

对于1GB的视频,字节应该从客户端直接到桶上,部分,从不通过你的服务器. r2 商店有 store.multipart (单位:千美元)volume 不——客户端不能 PUT 到磁盘 :

const { key, uploadId } = await media.multipart!.create("clips/film.mp4", { contentType: "video/mp4" });
const urls = await Promise.all([1, 2, 3].map((n) => media.multipart!.presignPart(key, uploadId, n, { expiresIn: 900 })));
// the client PUTs each part (≥ 5 MiB except the last) to its url and keeps the ETag header of each answer
await media.multipart!.complete(key, uploadId, [{ partNumber: 1, etag }, /* … ascending */]);
// or, on failure: await media.multipart!.abort(key, uploadId);

服务器保留了 R2 密钥;客户端只看到短寿命的预签名部分地址. 对于浏览器客户端,桶的 CORS 必须允许 PUT 从客户的来源并曝光 ETag。 。 。 . complete 治疗200 携带一个 <Error> 就像失败一样.

版本与发布记录

@broberg/media 共发布 9 个版本,首次发布于 2026/06/15;过去 12 个月发布了 9 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.7.0latest2026/11/035 天前
0.6.02026/10/1425 天前
0.5.02026/10/1227 天前
0.4.02026/10/031 个月前
0.3.12026/09/221 个月前
0.3.02026/09/171 个月前

关于 @broberg/media 的常见问题

@broberg/media 是什么?

车队的提供者-不可知媒体存储外观——一个创建Media() API (upload = 获得 = 署名 Url = 删除 = public Url) 超越可互换存储提供者, 因此提供者之间的后一动作从不触及呼叫站点 。 a Cloudflare R2供应商(S3-co)

如何安装 @broberg/media?

在项目目录运行 npm install @broberg/media,或使用 pnpm add / yarn add / bun add。

@broberg/media 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@broberg/media 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@broberg/media 可以商用吗?

@broberg/media 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@broberg/media 的下载量有多少?

根据 npm 官方下载统计,@broberg/media 在 2026/10/09 – 2026/11/07 这 30 天内被下载 3,203 次,日均约 107 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@broberg/media 还在维护吗?

最新版本 0.7.0 发布于 2026/11/03(5 天前);至今共发布 9 个版本,其中过去 12 个月发布了 9 个;首次发布于 2026/06/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。