Skip to main content

概述

后台「媒体库」用于管理博客图片。上传采用 浏览器直传 R2(与音乐同款 S3 预签名模式),图片文件不经过 Worker,D1 media 表只存元数据。
  • 新上传:POST /api/media/upload-url 签发预签名 PUT URL → 浏览器直接 PUT 到 R2 → POST /api/media 登记元数据
  • 图片本体一律存 R2 的媒体专用桶 R2_MEDIA_BUCKET(与音乐桶相互独立),D1 media 表只存元数据(url 为 R2 公开地址)
  • 降级:未配置好 R2 媒体桶时,upload-url 返回 503,只读列表仍可用
  • 所有媒体接口(含列表)都需要会话 token

签发预签名上传 URL(需登录)

请求体:
响应:

签名细节(浏览器直传必须对齐)

因为 Content-Type 参与签名,浏览器 PUT 时必须原样发送与响应 contentType 完全一致的 Content-Type;并且 R2 桶的 CORS 配置里 AllowedHeaders 必须包含 Content-Type,否则预检(OPTIONS)失败,上传表现为「直接失败、看不到原因」。R2 桶 CORS 建议同时允许 PUT 方法与你的站点来源;AllowedOrigins 填博客域名。
约束与错误:
503 的判定要同时具备五项:R2_MEDIA_BUCKET、R2_MEDIA_PUBLIC_BASE、R2_ACCESS_KEY_ID、R2_SECRET_ACCESS_KEY、R2_ENDPOINT。只配桶不配公开域名也会降级为 503——否则能签发上传 URL 却拼不出 publicUrl,上传完只会在桶里留下无法回收的孤儿对象。
浏览器拿到 uploadUrl 后直接 PUT 上传(Content-Type 与响应一致),完成后再登记元数据。

登记媒体(需登录)

请求体:
url 协议白名单:仅允许 http(s)://——R2 公开地址或外部图床链接;data: / javascript: / vbscript: 等协议一律 400 拒绝(图片本体一律走 R2 直传,不再接受 base64 内嵌;历史遗留的 base64 记录已由迁移 0014 清除)。 响应(201):
id 由服务端生成(m- + 12 字节随机 hex),name 截断到 200 字符、type 截断到 64 字符。

获取媒体列表(需登录)

响应: { "ok": true, "media": [ ... ] },按 created_at DESC, id DESC 排序,响应头 Cache-Control: no-store(增删即时可见)。

删除媒体(需登录)

删除顺序是先 R2、后 D1:
  1. 查该行 url,用 extractMediaR2Key 解析对象 key——同时校验路径前缀 /media/ 与 origin 必须等于 R2_MEDIA_PUBLIC_BASE,因此形如 https://evil.example/media/x 的外链不会被当成桶内对象
  2. 命中则向媒体桶发起服务端签名 DELETE(签名头 host;x-amz-content-sha256;x-amz-date + UNSIGNED-PAYLOAD)
  3. R2 删除失败不阻塞(try/catch 吞掉),继续删除 D1 元数据行——避免留下「前端删不掉、库里还有」的幽灵记录;外部图床链接不触发 R2 删除
  4. 删除成功后按 created_at DESC, id DESC 的列表立即少掉该项

环境变量(GitHub Secrets)

只有一组 R2 凭据,该令牌必须对你启用的媒体桶 / 音乐桶 / 备份桶都有读写权限。变量的逐项获取方式见 部署密钥指南。

表结构

迁移 0015_hot_path_indexes.sql 为 media 表补了 idx_media_created_id,让上面的列表排序走索引。