生成器可检测与管理远端主题素材(列表 / 差异 / 覆盖 / 删除)

「传上去」之后还得知道「传得对不对」:同一 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 项全过。
This commit is contained in:
tamakyi
2026-10-21 21:29:35 +08:00
parent 9c961b3c05
commit 12c4f3e1be
9 changed files with 549 additions and 23 deletions
+27
View File
@@ -573,3 +573,30 @@ base.css 还原成 CRLF,插入行要用同一行尾,否则变 MIXED。
细节与取舍写进新文档 `docs/theme-assets-cdn.md`(含缓存、CORS、深色无素材、迁移 sed 命令)。
⚠️ UI 坑:面板 240px 里 4 个中文字的 label 会被 flex 压缩、末字被输入框切掉 →
label 要 `flex:0 0 auto; white-space:nowrap`,宽度给 84px 才够(68px 仍会切)。
**3) 素材托管:本地 / 任意 S3 兼容,生成器内测通即可上传**
用户要求「放本地还是放 S3 兼容(不只 R2)由部署者自己定,并且能配好即测、测通即传」。
- `serve.py` 手写 SigV4(零依赖,PUT/GET/DELETE/HEAD),新增 `/api/s3/ping|test|upload`。
测试连接四步:HEAD 桶 → PUT 探针 → GET 读回比对 → 匿名 GET(能否被浏览器直读)→ 删探针。
4xx 把 `<Code>` 翻成人话(`S3_HINTS`),签名失败直接指向密钥/Region/时钟三件事。
- `index.html` 新增「素材托管」面板,上传集合复用导出用的 `usedAssetIds()`;
成功后自动回填「素材前缀」。密钥只进 sessionStorage,不进工程 json(有断言)。
- **两个真 bug**:①`usedAssetIds()` 返回 Set,`!ids.length` 恒真 → 永远「还没有素材」;
②字段填完 700ms 内刷新会丢(scheduleSave 防抖)→ 失焦/切模式/上传成功时立即 `saveDraft()`。
- **验证手段**:`check_sigv4.py` 用 aws-sdk-go-v2 的 signer 逐字节对拍(6/6)——
裸 SignHTTP 会二次转义 URI、而 S3 client 关了它,所以用例只用 ASCII,
中文 key 的编码规则跟随 S3 client(单次 RFC3986)。
`selftest_upload.py` 假 S3 跑 16 项;`mock_s3.py` 可手动点面板。
- ⚠️ **8777 端口陷阱**:Windows `SO_REUSEADDR` 让新进程 bind 成功却抢不到 accept,
症状是「代码改了但接口还是 404」。查 `Get-NetTCPConnection -LocalPort 8777`,或换端口验证。
**4) 远端素材检测与管理(用户「还没测,想先看到 S3 上有什么」)**
- serve.py:`s3_request` 支持 query,新增 `list_objects`(ListObjectsV2 + 分页)与
`delete_objects`;端点 `/api/s3/list`、`/api/s3/delete`。
- index.html:远端素材区,按对象键名跟本地比 → 一致 / 旧图(同名大小不同)/ 多余 / 缺失;
「补齐差异」只传缺的与旧图,「删除多余」批量删,单行 ↻ 覆盖、✕ 删除。
- ⚠️ **键的两种形态**:list 返回**完整键**(含 `<路径前缀>/`),upload/delete 接口收
**相对键**。不剥这一层,服务端会再拼一次前缀 → 删一个不存在的键,S3 还回 204,
表现为「显示删除成功但东西还在」。加 `relKey()` 统一转换。
- ⚠️ `check_sigv4.py` 补了带 query 的用例(ListObjectsV2 三参数、乱序传入的分页 token),
8/8 一致 —— canonical query 的排序与百分号编码也必须对拍,漏测会静默失效。
+24
View File
@@ -44,3 +44,27 @@
- 导出面板「素材前缀」`S.assetBase`:空 = `/static/images/themes/<slug>/`;填了 = CSS 写绝对 URL
(R2/OSS)。**图片地址只能由 `urlOf()` 决定** —— `layerCSS` 曾复制过一份拼接,改一处会出现
「背景走 CDN、装饰还指本地」。文档见 `docs/theme-assets-cdn.md`。
- **素材托管(2026-10-21)**:面板两选一 `S.host.mode = local|s3`,字段
endpoint/region/bucket/prefix/pathStyle/publicBase 进工程,**accessKey/secretKey 只进
sessionStorage**(`SEC_SESSION_KEY`),绝不进草稿/工程 json/导出 zip(有断言)。
`publicBaseOf()` 手填优先,空则按 endpoint 推导(path 风格 `<ep>/<桶>/<前缀>`)——
对 R2 这个地址不可匿名读,测试的「匿名可读」只给 warn 不拦。
- **上传走 `serve.py` 代理**,浏览器不直连对象存储:不用配 CORS,密钥不发给第三方。
端点 `/api/s3/ping`(探测能力,`python -m http.server` 没有 → 按钮禁用)、
`/api/s3/test`、`/api/s3/upload`。上传集合与导出共用 `usedAssetIds()`。
- ⚠️ **`usedAssetIds()` 返回 Set**:`!ids.length` 恒真、`ids.map` 不存在 → 上传永远报
「还没有素材」。用 `[...usedAssetIds()]` 展开。
- ⚠️ **SigV4 手写,必须对拍**:`dev/theme-builder/check_sigv4.py` 调
`dev/sigv4ref`(aws-sdk-go-v2 的 `signer.SignHTTP`)逐字节比。6/6 一致才可信。
- 只用 ASCII 用例:裸 `SignHTTP` 默认**二次转义** URI,而 S3 client 内部把
`DisableURIPathEscaping` 设成 true(单次编码)。我们跟随 S3 client / AWS 文档 RFC3986。
- canonical headers 三件套 `host;x-amz-content-sha256;x-amz-date`,与 SDK 相同;
CanonicalHeaders 自身以 `\n` 结尾,故其后与 SignedHeaders 之间有一个空行(规范如此)。
- ⚠️ **HEAD 桶失败拿不到 `<Code>`**(HEAD 响应无 body)→ 只能按状态码归因
(403→AccessDenied / 404→NoSuchBucket / 301|307→PermanentRedirect)。想拿真错误码要靠 PUT。
- ⚠️ **Windows 上 8777 可能被上一个进程继续占着**(SO_REUSEADDR 让新进程 bind 成功但抢不到
accept)。症状:`/api/s3/ping` 返回旧版 404,而代码明明改了。用
`Get-NetTCPConnection -LocalPort 8777` 查 PID,或换端口验证。
- 自测:`python dev/theme-builder/check_sigv4.py`(签名对拍)、
`python dev/theme-builder/selftest_upload.py`(假 S3,16 项)、
`python dev/theme-builder/mock_s3.py --port 9900`(手动点面板)。