Files
tamabox/docs/theme-assets-cdn.md
tamakyi 12c4f3e1be 生成器可检测与管理远端主题素材(列表 / 差异 / 覆盖 / 删除)
「传上去」之后还得知道「传得对不对」:同一 slug 传过三次、换过 slug 旧图还在、
重新导出后远端仍是旧版,这些在页面上都看不出来。

- serve.py:s3_request 支持 query;新增 ListObjectsV2(含分页)与 DeleteObject,
  端点 /api/s3/list、/api/s3/delete。
- index.html:远端素材清单按对象键名跟本地比 —— 一致 / 旧图(同名但大小不同)/
  多余(当前主题已不引用)/ 缺失。「补齐差异」只传缺的与旧图,「删除多余」批量删,
  单行可 ↻ 覆盖或 ✕ 删除;删除走 confirm 二次确认。改过 slug 会提示重新检测。
- 修:list 返回完整键而 upload/delete 收相对键,不剥前缀会删到不存在的键
  (S3 回 204,看起来删成功其实没删)。加 relKey() 统一。
- check_sigv4.py 补带 query 的用例(ListObjectsV2 三参数、乱序 token)→ 8/8;
  selftest_upload.py 补列表/分页/幂等删除 → 20 项全过。
2026-10-21 21:29:35 +08:00

6.9 KiB
Raw Permalink Blame History

把主题素材放到对象存储(任意 S3 兼容)

这里的「对象存储」不特指某一家:Cloudflare R2、自建 MinIO、Backblaze B2、 阿里云 OSS 的 S3 兼容端点、腾讯云 COS 的 S3 兼容端点都走同一套 SigV4, 差别只在 Endpoint、Region、是否路径风格。放本地还是放对象存储,由部署者自己决定 —— 主题生成器里「素材托管」就是这两选一。

现状:主题素材在 static/images/themes/<slug>/,通过 static/fs.go 的 //go:embed images/themes 随二进制发布。好处是部署即生效、不依赖外部服务;代价是换一张图就要重新编译部署, 而且一张 1920×1080 的场景图会让二进制大几百 KB。

主题素材只有一处引用:主题 CSS 里的 url(...)(--tb-page-image / --tb-page-pattern / --tb-deco)。分享卡是 Go 里用 imaging 画出来的,不读主题素材。所以只要解决「CSS 里的地址指向哪」, 就把整套素材搬走了。

下面三种做法按「改动量从 0 到有代码」排列,选一个即可,不必叠加。


A. 生成器直接写绝对地址 + 就近上传(已支持,推荐)

dev/theme-builder 左侧「素材托管」有两个选项:本地(随二进制)/ S3 兼容存储。

选 S3 后填:

Endpoint   https://<账号>.r2.cloudflarestorage.com   ← MinIO / B2 / OSS / COS 填各自的
Region     auto                                       ← R2 用 auto;MinIO 常为 us-east-1
桶名       my-theme-assets
路径前缀   themes
公开前缀   https://cdn.example.com/themes             ← 留空 = 按 Endpoint 推导
Access Key / Secret Key

然后:

  1. 测试连接 —— HEAD 桶(连通+凭据)→ PUT 探针(写权限)→ GET 读回(内容一致) → 匿名 GET(能不能被浏览器直接读到)→ 删除探针。失败会把 S3 的 <Code> 翻成人话。
  2. 上传素材 —— 逐张 PUT 本次主题引用到的图,键为 <路径前缀>/<slug>/<id>.webp, 带 public, max-age=31536000, immutable。成功后自动把导出用的「素材前缀」填好。
  3. 检测远端 —— ListObjectsV2 列出 <前缀>/<slug>/ 下所有对象,按对象键名跟本地比: 一致 / 旧图(同名但大小不同)/ 多余(当前主题已不引用)/ 缺失。 可以「补齐差异」(只传缺的与旧图)、「删除多余」,也可以单行覆盖或删除。

上传经本机 serve.py 代理(/api/s3/test、/api/s3/upload),浏览器不直连对象存储: 不需要给桶配 CORS,密钥只在当前标签页的 sessionStorage 与内存里, 不进草稿、不进工程 json、不进导出 zip。

不想用面板也行,导出的 zip 里图片照旧在 static/images/themes/<slug>/*.webp, 用任何 S3 客户端推上去即可:

aws s3 sync static/images/themes/shiroko/ s3://my-bucket/themes/shiroko/ \
  --endpoint-url https://<你的 Endpoint> --cache-control "public, max-age=31536000, immutable"
sed -i 's#/static/images/themes/shiroko/#https://cdn.example.com/themes/shiroko/#g' \
  static/css/themes/shiroko.css     # 已有主题不想重导时,等价于「填前缀再导一次」
  • 优点:零服务端改动、零跳转、缓存最优(CDN 直接命中);
  • 缺点:域名烘焙进 CSS —— 换域名/换路径要重新导出(旧主题可以用 sed 批量替换);
  • 仓库里可以删掉 static/images/themes/<slug>/ 来缩小编译产物,确认 CSS 已是绝对地址再删。

B. 应用内 301 跳转(配置驱动,CSS 一行都不用改)

conf/app.ini:

[theme]
asset_base = https://cdn.example.com/themes   ; 留空 = 本地静态目录

中间件必须在 flamego.Static 之前注册(internal/route/route.go 里 f.Use(flamego.Static(...)) 前面):

// 主题素材转发到对象存储:命中前缀就 301 出去,其余请求原样放过。
// 必须挂在 static 中间件之前——Static 命中本地文件就直接返回了,轮不到这里。
func themeAssetRedirect() flamego.Handler {
	const prefix = "/static/images/themes/"
	return func(ctx flamego.Context) {
		base := theme.AssetBase() // 从 conf 读,空串 = 不接管
		p := ctx.Request().URL.Path
		if base == "" || !strings.HasPrefix(p, prefix) {
			return
		}
		w := ctx.ResponseWriter()
		w.Header().Set("Cache-Control", "public, max-age=86400")
		http.Redirect(w, ctx.Request(), base+strings.TrimPrefix(p, prefix), http.StatusMovedPermanently)
	}
}
  • 优点:已有主题、生成器导出的相对地址都能用,切 CDN 只改配置;
  • 缺点:冷缓存时每张图多一跳;301 会被浏览器长期缓存,以后想切回本地, 老访客可能还在访问 CDN(介意的话用 302,或一开始就用方案 A);
  • 本地开发记得把 asset_base 留空,否则断网时素材全 404。

C. 边缘 / 反代改写(零代码)

素材路径有稳定前缀,最适合在边缘处理:

  • Cloudflare:Origin Rule / Transform Rule,把 /static/images/themes/* 的 Host 改成 R2 自定义域(如 cdn.example.com),或 Redirect Rule 直接 301 过去;

  • nginx:location /static/images/themes/ { proxy_pass https://cdn.example.com/themes/; }, 或 rewrite 成 CDN 地址后 return 301。

  • 优点:应用完全不知道素材在哪,不占应用带宽;

  • 缺点:绑定部署环境,本地跑没有等效层(要考虑 fallback)。


三种方案共同要处理的细节

  1. 缓存:对象存储上建议 Cache-Control: public, max-age=31536000, immutable(文件名带版本时)。 换图不要覆盖同名对象——要么换 id(生成器里改文件名),要么给 URL 加 ?v=; Cloudflare 默认把 query string 计入缓存键,所以 ?v= 是有效的。
  2. CORS 不用配:background-image 不触发 CORS,匿名 GET 即可。
  3. 主题 CSS 本身不用上 CDN:一个主题只有几 KB,而且随 StaticVersion 走缓存键, 留在 static/css/themes/ 更省事。
  4. 深色模式没有素材(深色块 --tb-deco 恒为 none),别只传浅色图就以为漏传了。
  5. 分享卡不受影响:route/question/share.go 用 imaging 直接画,不加载主题图片。
  6. 迁完之后 go build 会小一圈,但主题 css 仍是编译期 embed:改 CSS 还是要重编译部署。

建议

  • 只是想让部署产物变小、换图不用重编译 → A(生成器里配好 S3、测通、一键上传,最省事,缓存也最好)。
  • 想保留「素材可以随时换、CSS 不用重发」的灵活性 → B(配一个 asset_base,接受多一跳)。
  • 站点已经在 Cloudflare 上、且愿意在边缘配规则 → C(最干净,但迁移成本在运维侧)。
  • 就用本地目录、不想依赖任何外部服务 → 什么都不用做(默认行为,素材随二进制发布)。