生成器可检测与管理远端主题素材(列表 / 差异 / 覆盖 / 删除)
「传上去」之后还得知道「传得对不对」:同一 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:
@@ -91,7 +91,15 @@ class MockS3(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urllib.parse.urlsplit(self.path)
|
||||
qs = urllib.parse.parse_qs(parsed.query)
|
||||
bucket, key = self._split()
|
||||
if not key and qs.get("list-type") == ["2"]:
|
||||
if not self._check(bucket):
|
||||
return
|
||||
return self._list(qs.get("prefix", [""])[0],
|
||||
int(qs.get("max-keys", ["1000"])[0] or 1000),
|
||||
int(qs.get("continuation-token", ["0"])[0] or 0))
|
||||
MockS3.requests.append((self.command, self.path, dict(self.headers)))
|
||||
if key not in MockS3.store:
|
||||
self._xml(404, "NoSuchKey", "The specified key does not exist")
|
||||
@@ -103,6 +111,34 @@ class MockS3(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def _list(self, prefix, max_keys, offset):
|
||||
# 只实现 ListObjectsV2 里服务端用到的字段;分页用 offset 当 token,够测真实分页逻辑。
|
||||
keys = sorted(k for k in MockS3.store if k.startswith(prefix))
|
||||
page = keys[offset:offset + max_keys]
|
||||
truncated = offset + len(page) < len(keys)
|
||||
items = []
|
||||
for k in page:
|
||||
data = MockS3.store[k][0]
|
||||
items.append(
|
||||
"<Contents><Key>%s</Key><Size>%d</Size>"
|
||||
"<LastModified>2026-10-21T00:00:00.000Z</LastModified><ETag>"mock"</ETag></Contents>"
|
||||
% (k, len(data)))
|
||||
token = ("<NextContinuationToken>%d</NextContinuationToken>" % (offset + len(page))) if truncated else ""
|
||||
body = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>'
|
||||
'<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
|
||||
"<Name>%s</Name><Prefix>%s</Prefix><KeyCount>%d</KeyCount>"
|
||||
"<MaxKeys>%d</MaxKeys><IsTruncated>%s</IsTruncated>%s%s"
|
||||
"</ListBucketResult>"
|
||||
% (BUCKET, prefix, len(page), max_keys, "true" if truncated else "false",
|
||||
token, "".join(items))
|
||||
).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/xml")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_DELETE(self):
|
||||
bucket, key = self._split()
|
||||
if not self._check(bucket):
|
||||
|
||||
Reference in New Issue
Block a user