# -*- coding: utf-8 -*- r'''补上 add_rescue_section.py 漏掉/插错的三处。 前一个补丁的问题: 1. _rescue_rows() 只在补丁脚本里定义,**没插进 admin.py** —— 后台一渲染就 NameError(页面直接 500) 2. /rescue/ 路由被插进了 **do_HEAD**,而且用了 `self.wfile.write(body)` —— HEAD 本来就不该有 body。而且 do_GET 那边一条都没加, 等于浏览器点下载根本走不到。 (锚点 "/download/" 出现两次:do_HEAD 和 do_GET 各一份, replace(...,1) 只命中了前面那个。这就是那个 "! 锚点 2 处" 警告。) 这个脚本按 do_HEAD 自己的 ok()/no() 风格把两处都写对。 ''' from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent RELAY = ROOT / "relay" / "relay.py" ADMIN = ROOT / "relay" / "admin.py" def load(p: Path): raw = p.read_bytes() return raw, raw.startswith(b"\xef\xbb\xbf"), raw.decode("utf-8-sig") def save(p: Path, text: str, bom: bool): p.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8")) # =========================================================================== # 1. do_HEAD:换成 ok()/no() 风格(不能有 body) # =========================================================================== BAD_HEAD = ''' if path.startswith("/rescue/"): # 抢救报告下载。单独一条路由,**不发缓存头** —— # 内容会变,而且不该让 Cloudflare 边缘留着。 wanted = urllib.parse.unquote(path[len("/rescue/"):]) safe = _safe_rescue_name(wanted) if not safe: self._json(400, {"ok": False, "error": "文件名不合法"}) return target = RESCUE_DIR / safe if not target.is_file(): self._json(404, {"ok": False, "error": "没有这份报告"}) return try: body = target.read_bytes() except OSError as exc: self._json(500, {"ok": False, "error": f"读不到: {exc}"}) return self.send_response(200) self.send_header("Content-Type", "text/plain; charset=utf-8") self.send_header("Content-Length", str(len(body))) quoted = urllib.parse.quote(safe) self.send_header( "Content-Disposition", f"attachment; filename*=UTF-8''{quoted}") self.send_header("Cache-Control", "no-store") self.end_headers() self.wfile.write(body) return ''' GOOD_HEAD = ''' if path.startswith("/rescue/"): # 抢救报告。HEAD 只回长度和类型,**不写 body** —— # 这条路由同时存在于 do_GET(真正下载)和 do_HEAD(探测)。 wanted = urllib.parse.unquote(path[len("/rescue/"):]) safe = _safe_rescue_name(wanted) if not safe: return no() target = RESCUE_DIR / safe if not target.is_file(): return no() quoted = urllib.parse.quote(safe) headers = {"Content-Disposition": "attachment; filename*=UTF-8''%s" % quoted, "Cache-Control": "no-store"} return ok("text/plain; charset=utf-8", target.stat().st_size, headers) ''' # =========================================================================== # 2. do_GET:加同一条路由(这里才真正回 body) # =========================================================================== GET_ANCHOR = ''' if path.startswith("/download/"): wanted = urllib.parse.unquote(path[len("/download/"):]) safe = _safe_download_name(wanted) if not safe: self._json(400, {"ok": False, "error": "文件名不合法"}) return''' GET_NEW = ''' if path.startswith("/rescue/"): # 抢救报告下载。**单独一条路由,不发缓存头** —— # 内容会变,而且不该让 Cloudflare 边缘留着。 # (和 /download/ 分成两套是有意的:那个是给用户的安装包, # 文件名带版本号、内容永不改变,所以敢发 604800 长缓存。) wanted = urllib.parse.unquote(path[len("/rescue/"):]) safe = _safe_rescue_name(wanted) if not safe: self._json(400, {"ok": False, "error": "文件名不合法"}) return target = RESCUE_DIR / safe if not target.is_file(): self._json(404, {"ok": False, "error": "没有这份报告"}) return try: body = target.read_bytes() except OSError as exc: self._json(500, {"ok": False, "error": f"读不到: {exc}"}) return self.send_response(200) self.send_header("Content-Type", "text/plain; charset=utf-8") self.send_header("Content-Length", str(len(body))) quoted = urllib.parse.quote(safe) self.send_header( "Content-Disposition", f"attachment; filename*=UTF-8''{quoted}") self.send_header("Cache-Control", "no-store") self.end_headers() self.wfile.write(body) return ''' + GET_ANCHOR # =========================================================================== # 3. admin.py:把 _rescue_rows 真正定义进去 # =========================================================================== RESCUE_FN = '''def _rescue_rows(items: list) -> str: """抢救报告列表的 HTML。 照 dl_rows 的风格:在服务端直接拼好塞进模板,不用 JS 去拉 —— 这台机器只有 460MB 内存,多个接口就多份开销,而且后台页 本来就只是一次性的展示。 没有报告时给一句正面的话("一直没出过问题"), 而不是干巴巴的"暂无数据"—— 没报告是好事。 """ if not items: return ('
还没有报告 —— 说明网站一直没出过' '「自动救不回来」的问题。
') import time as _t import urllib.parse as _up rows = [] for it in items: when = _t.strftime("%m-%d %H:%M", _t.localtime(it["mtime"])) size = it["size"] / 1024 size_s = ("%.1f KB" % size) if size >= 1 else ("%d B" % it["size"]) summary = _esc(it.get("summary") or "(没有结论行)") href = "/rescue/" + _up.quote(it["name"]) rows.append( '
' '%s' '' '%s' '' '%s
' % (href, when, size_s, summary)) return "".join(rows) def render_admin(''' def main() -> int: ok = True # ---- relay.py ---- raw, bom, text = load(RELAY) if BAD_HEAD in text: text = text.replace(BAD_HEAD, GOOD_HEAD, 1) print(" ✓ do_HEAD 里的 /rescue/ 换成 ok()/no() 风格(不再写 body)") elif GOOD_HEAD in text: print(" · do_HEAD 已经是对的") else: print(" ✗ do_HEAD 里找不到那段 /rescue/") ok = False if 'if path.startswith("/rescue/")' in text.split("def do_GET")[1]: print(" · do_GET 里已经有了") else: if GET_ANCHOR in text: # 只替换 do_GET 里那一处(do_HEAD 那处已经在上面被换掉了, # 所以现在全文只剩一处) n = text.count(GET_ANCHOR) if n != 1: print(" ! do_GET 锚点还有 %d 处" % n) text = text.replace(GET_ANCHOR, GET_NEW, 1) print(" ✓ do_GET 加了 /rescue/ 路由(真正回 body 的那条)") else: print(" ✗ do_GET 里找不到 /download/ 锚点") ok = False save(RELAY, text, bom) # ---- admin.py ---- raw, bom, text = load(ADMIN) if "def _rescue_rows(" in text: print(" · admin.py 里已经定义了 _rescue_rows") else: if "def render_admin(" not in text: print(" ✗ admin.py 里找不到 render_admin") ok = False else: text = text.replace("def render_admin(", RESCUE_FN, 1) save(ADMIN, text, bom) print(" ✓ admin.py 里补上了 _rescue_rows 的定义") ok = ok and True return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())