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

「传上去」之后还得知道「传得对不对」:同一 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
+18 -11
View File
@@ -30,19 +30,25 @@ TS = "20130524T000000Z"
EMPTY = serve.EMPTY_SHA256
BODY_HASH = serve._sha256(b"TamaBox theme builder probe\n")
# (说明, method, endpoint, extra cfg, key)
# (说明, method, endpoint, extra cfg, key, query)
CASES = [
("path-style 桶级 HEAD", "HEAD", "http://127.0.0.1:9000", dict(bucket="tb-assets"), ""),
("path-style 桶级 HEAD", "HEAD", "http://127.0.0.1:9000", dict(bucket="tb-assets"), "", None),
("path-style 带前缀对象", "PUT", "http://127.0.0.1:9000",
dict(bucket="tb-assets", prefix="themes"), "shiroko/bg1.webp"),
dict(bucket="tb-assets", prefix="themes"), "shiroko/bg1.webp", None),
("virtual-host 对象", "GET", "https://s3.us-west-004.backblazeb2.com",
dict(bucket="my-bucket", region="us-west-004", pathStyle=False), "themes/shiroko/bg1.webp"),
dict(bucket="my-bucket", region="us-west-004", pathStyle=False), "themes/shiroko/bg1.webp", None),
("R2 region=auto", "PUT", "https://acc.r2.cloudflarestorage.com",
dict(bucket="cdn", region="auto", prefix="themes"), "x/a.webp"),
dict(bucket="cdn", region="auto", prefix="themes"), "x/a.webp", None),
("R2 虚拟主机", "GET", "https://acc.r2.cloudflarestorage.com",
dict(bucket="cdn", region="auto", pathStyle=False), "themes/s/t.webp"),
dict(bucket="cdn", region="auto", pathStyle=False), "themes/s/t.webp", None),
("带端口 https", "DELETE", "https://minio.lan:9443",
dict(bucket="static", region="us-east-1"), "themes/s/t.webp"),
dict(bucket="static", region="us-east-1"), "themes/s/t.webp", None),
# 带 query:ListObjectsV2 的 canonical query 要按键排序、逐项编码(/ 编成 %2F)
("ListObjectsV2 三参数", "GET", "http://127.0.0.1:9000", dict(bucket="tb-assets"),
"", {"list-type": "2", "max-keys": "1000", "prefix": "themes/shiroko/"}),
("ListObjectsV2 分页(乱序传入)", "GET", "https://acc.r2.cloudflarestorage.com",
dict(bucket="cdn", region="auto", pathStyle=False), "",
{"prefix": "themes/", "continuation-token": "1/abc+d", "max-keys": "2", "list-type": "2"}),
]
@@ -68,20 +74,21 @@ def go_sign(payloads):
def main():
payloads = []
for _, method, endpoint, extra, key in CASES:
for _, method, endpoint, extra, key, query in CASES:
cfg, path = build(endpoint, extra, key)
body = BODY_HASH if method == "PUT" else EMPTY
payloads.append("\t".join([method, cfg.url(path), cfg.region, body, TS]))
url = cfg.url(path) + (("?" + serve.canonical_query(query)) if query else "")
payloads.append("\t".join([method, url, cfg.region, body, TS]))
go = go_sign(payloads)
if len(go) != len(CASES):
raise SystemExit("返回条数不对:go=%d cases=%d" % (len(go), len(CASES)))
bad = 0
for (desc, method, endpoint, extra, key), g in zip(CASES, go):
for (desc, method, endpoint, extra, key, query), g in zip(CASES, go):
cfg, path = build(endpoint, extra, key)
body = BODY_HASH if method == "PUT" else EMPTY
py = serve.sign_v4(cfg, method, path, body, TS, cfg.host)
py = serve.sign_v4(cfg, method, path, body, TS, cfg.host, serve.canonical_query(query))
if g.rsplit("Signature=", 1)[-1] == py.rsplit("Signature=", 1)[-1]:
print("OK %s" % desc)
else: