diff --git a/_build/clean_dup_assets.py b/_build/clean_dup_assets.py new file mode 100644 index 0000000..ba9440e --- /dev/null +++ b/_build/clean_dup_assets.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +r'''统一清理两个平台发行版里的重复附件。 + +背景: + 重发时如果只"复用已有发行版",附件会一次次堆积。 + Gitee 尤其绕:`GET /releases/tags/{tag}` 返回的 `assets` + **只有 name 和 download_url,没有 id**,所以没法删。 + 但另一个接口有 id: + GET /repos/{o}/{r}/releases/{id}/attach_files -> 带 id + DELETE /repos/{o}/{r}/releases/{id}/attach_files/{attach_id} + + Gitea/EzHub 则直接有 assets[].id。 + +策略:同名附件只保留最新的一份,其余删掉。 +支持 --dry 先看要删什么。 +''' +from __future__ import annotations + +import json +import sys +import time +import urllib.error +import urllib.request +from collections import defaultdict +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +GT = (ROOT / "_build" / ".gitee_token").read_text(encoding="utf-8").strip() +ET = (Path.home() / ".ykxmz" / "ezhub_token").read_text( + encoding="utf-8").strip() +TAG = "v1.8.0" + +# 这两个是 Gitee 自动生成的源码包,不是我们传的,不要动 +KEEP_ALWAYS = {"v1.8.0.zip", "v1.8.0.tar.gz"} + + +def http(url: str, method: str = "GET", kind: str = "gitea", + token: str = "", tries: int = 3): + """带重试的请求。网络会抖,不重试会误判。""" + headers = {"User-Agent": "ykxmz-clean"} + if kind == "gitee": + sep = "&" if "?" in url else "?" + url = url + sep + "access_token=" + token + else: + headers["Authorization"] = "token " + token + last = None + for attempt in range(1, tries + 1): + try: + req = urllib.request.Request(url, method=method, headers=headers) + with urllib.request.urlopen(req, timeout=90) as resp: + text = resp.read().decode("utf-8", "replace") + return resp.status, (json.loads(text) if text.strip() else {}) + except urllib.error.HTTPError as exc: + return exc.code, exc.read().decode("utf-8", "replace")[:200] + except Exception as exc: # noqa: BLE001 + last = exc + if attempt < tries: + time.sleep(4) + return 0, str(last) + + +def list_gitee_assets(rid: int) -> list: + """Gitee 要单独调 attach_files 才拿得到 id。""" + status, data = http( + "https://gitee.com/api/v5/repos/xmz7891/ykxmz/releases/%s" + "/attach_files" % rid, kind="gitee", token=GT) + if status != 200 or not isinstance(data, list): + print(" [失败] 取 Gitee 附件列表 HTTP %s: %s" % (status, data)) + return [] + return data + + +def list_gitea_assets(rid: int) -> list: + status, data = http( + "https://git.261826.xyz/api/v1/repos/xmz/ykxmz/releases/%s" + % rid, kind="gitea", token=ET) + if status != 200: + print(" [失败] 取 EzHub 发行版 HTTP %s" % status) + return [] + return data.get("assets") or [] + + +def clean(platform: str, rid: int, assets: list, dry: bool) -> int: + groups = defaultdict(list) + for a in assets: + name = a.get("name") or "?" + if name in KEEP_ALWAYS: + continue + groups[name].append(a) + + removed = 0 + for name, items in sorted(groups.items()): + if len(items) <= 1: + continue + # 新的排前面 + items.sort(key=lambda x: str(x.get("created_at") or ""), reverse=True) + keep = items[0] + print(" %-32s %d 份 -> 保留 id=%s" % (name, len(items), + keep.get("id"))) + for extra in items[1:]: + aid = extra.get("id") + if dry: + print(" (演练)会删 id=%s" % aid) + removed += 1 + continue + if platform == "gitee": + url = ("https://gitee.com/api/v5/repos/xmz7891/ykxmz" + "/releases/%s/attach_files/%s" % (rid, aid)) + else: + url = ("https://git.261826.xyz/api/v1/repos/xmz/ykxmz" + "/releases/%s/assets/%s" % (rid, aid)) + status, body = http(url, method="DELETE", + kind=("gitee" if platform == "gitee" + else "gitea"), + token=(GT if platform == "gitee" else ET)) + if status in (200, 204): + print(" ✓ 删了 id=%s" % aid) + removed += 1 + else: + print(" ✗ id=%s HTTP %s %s" + % (aid, status, str(body)[:80])) + return removed + + +def main() -> int: + dry = "--dry" in sys.argv + total = 0 + + print(" ═══ Gitee ═══") + status, rel = http("https://gitee.com/api/v5/repos/xmz7891/ykxmz" + "/releases/tags/" + TAG, kind="gitee", token=GT) + if status == 200: + rid = rel.get("id") + assets = list_gitee_assets(rid) + print(" 发行版 id=%s,可删附件 %d 个" % (rid, len(assets))) + total += clean("gitee", rid, assets, dry) + else: + print(" [失败] HTTP %s" % status) + print() + + print(" ═══ EzHub ═══") + status, rel = http("https://git.261826.xyz/api/v1/repos/xmz/ykxmz" + "/releases/tags/" + TAG, kind="gitea", token=ET) + if status == 200: + rid = rel.get("id") + assets = list_gitea_assets(rid) + print(" 发行版 id=%s,附件 %d 个" % (rid, len(assets))) + total += clean("gitea", rid, assets, dry) + else: + print(" [失败] HTTP %s" % status) + + print() + print(" %s %d 个" % ("演练:会删" if dry else "共删", total)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/dump_assets.py b/_build/dump_assets.py new file mode 100644 index 0000000..0885f90 --- /dev/null +++ b/_build/dump_assets.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +r'''列出两个平台发行版的全部附件(含 ID),并试删一个看接口通不通。''' +from __future__ import annotations + +import json +import sys +import urllib.error +import urllib.request +from collections import defaultdict +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") +GT = (ROOT / "_build" / ".gitee_token").read_text(encoding="utf-8").strip() +ET = (Path.home() / ".ykxmz" / "ezhub_token").read_text( + encoding="utf-8").strip() +TAG = "v1.8.0" + + +def get(url: str, token: str, kind: str): + headers = {"User-Agent": "ykxmz"} + if kind == "gitee": + sep = "&" if "?" in url else "?" + url = url + sep + "access_token=" + token + else: + headers["Authorization"] = "token " + token + with urllib.request.urlopen( + urllib.request.Request(url, headers=headers), timeout=60) as r: + return json.loads(r.read().decode("utf-8", "replace")) + + +def dump(name: str, url: str, token: str, kind: str) -> None: + print(" ═══ %s ═══" % name) + try: + rel = get(url, token, kind) + except Exception as exc: # noqa: BLE001 + print(" 查询失败: %s" % exc) + return + rid = rel.get("id") + print(" release id = %s" % rid) + assets = (rel.get("assets") or rel.get("attach_files") or []) + print(" 附件 %d 个:" % len(assets)) + groups = defaultdict(list) + for a in assets: + aname = a.get("name") or a.get("filename") or "?" + aid = a.get("id") + when = a.get("created_at") or a.get("created_at") or "" + size = a.get("size") or 0 + groups[aname].append((aid, when, size)) + print(" id=%-10s %-32s %8.2f MB %s" + % (aid, aname, size / 1024 / 1024, str(when)[:19])) + print() + print(" 重复情况:") + for aname, items in sorted(groups.items()): + if len(items) > 1: + print(" %-32s %d 份" % (aname, len(items))) + print() + + +def main() -> int: + dump("Gitee", + "https://gitee.com/api/v5/repos/xmz7891/ykxmz/releases/tags/" + TAG, + GT, "gitee") + dump("EzHub", + "https://git.261826.xyz/api/v1/repos/xmz/ykxmz/releases/tags/" + TAG, + ET, "gitea") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/release_all.py b/_build/release_all.py index a3bf2b9..6983dcb 100644 --- a/_build/release_all.py +++ b/_build/release_all.py @@ -133,27 +133,41 @@ def gitee_release(token: str, version: str, body: str, return False # 复用已有发行版时先把同名旧附件删掉,否则每跑一次多一份。 + # + # ⚠ Gitee 的坑:`GET /releases/{id}` 返回的 `assets` **只有 name 和 + # download_url,没有 id**,照着它删是删不掉的。 + # 必须换成单独这个接口才带 id: + # GET /releases/{id}/attach_files -> [{id, name, size, ...}] + # 删也是:DELETE /releases/{id}/attach_files/{attach_id} try: with urllib.request.urlopen( - "%s/releases/%s?access_token=%s" % (api, rid, token), - timeout=60) as resp: - existing = json.loads(resp.read().decode("utf-8", "replace")) + "%s/releases/%s/attach_files?access_token=%s" + % (api, rid, token), timeout=90) as resp: + old_assets = json.loads(resp.read().decode("utf-8", "replace")) + if not isinstance(old_assets, list): + old_assets = [] wanted = {p.name for p in files} - # Gitee 叫 attach_files,Gitea 叫 assets - old_assets = (existing.get("assets") - or existing.get("attach_files") or []) for asset in old_assets: - name = asset.get("name") or asset.get("filename") + name = asset.get("name") aid = asset.get("id") if name in wanted and aid: del_url = ("%s/releases/%s/attach_files/%s" "?access_token=%s" % (api, rid, aid, token)) dreq = urllib.request.Request(del_url, method="DELETE") - try: - urllib.request.urlopen(dreq, timeout=60) - print(" • 删掉同名旧附件 %s" % name) - except urllib.error.HTTPError as exc: - print(" • 删 %s 失败 HTTP %s(继续)" % (name, exc.code)) + for attempt in range(1, 4): + try: + urllib.request.urlopen(dreq, timeout=90) + print(" • 删掉同名旧附件 %s" % name) + break + except urllib.error.HTTPError as exc: + print(" • 删 %s 失败 HTTP %s(继续)" + % (name, exc.code)) + break + except Exception: # noqa: BLE001 + if attempt == 3: + print(" • 删 %s 网络失败,跳过" % name) + else: + time.sleep(4) except Exception as exc: # noqa: BLE001 print(" • 清理旧附件时出错(继续): %s" % exc)