@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/httpcontentDisposition这样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 }仍在编译.tigriss3 代码与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> 就像失败一样.