#!/usr/bin/env python3 """Install the lightweight front door and switch only /code/ to the JD tunnel.""" from __future__ import annotations import datetime import pathlib import re import shutil import subprocess import sys import urllib.error import urllib.request SITE_CONFIG = pathlib.Path("/etc/nginx/sites-enabled/guanghulab") WEB_ROOT = pathlib.Path("/var/www/guanghulab-front-door") SOURCE_FILES = ("index.html", "styles.css", "robots.txt", "llms.txt", "sitemap.xml") OLD_CODE_BLOCK = re.compile( r""" # Forgejo 代码仓库 location /code/ \{ auth_request /auth/verify-signature; proxy_pass http://127\.0\.0\.1:3001/; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; client_max_body_size 100m; \} """, ) NEW_CODE_BLOCK = """ # 光湖代码频道 · 广州备案前门 → 京东个人子频道 location /code/ { proxy_pass http://127.0.0.1:18088/code/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 300s; proxy_send_timeout 300s; client_max_body_size 256m; } """ def run(*args: str) -> None: subprocess.run(args, check=True) def get_status(url: str) -> int: request = urllib.request.Request(url, method="GET") try: with urllib.request.urlopen(request, timeout=15) as response: return response.status except urllib.error.HTTPError as error: return error.code def main() -> None: if len(sys.argv) != 2: raise SystemExit("usage: install-lite-front-door.py SOURCE_DIRECTORY") source = pathlib.Path(sys.argv[1]).resolve() if not SITE_CONFIG.is_file(): raise RuntimeError("active guanghulab nginx config unavailable") for name in SOURCE_FILES: if not (source / name).is_file(): raise RuntimeError(f"front-door source missing: {name}") current = SITE_CONFIG.read_text(encoding="utf-8") updated, replacements = OLD_CODE_BLOCK.subn(NEW_CODE_BLOCK, current) if replacements == 0 and NEW_CODE_BLOCK.strip() not in current: raise RuntimeError("expected legacy /code/ block not found") if replacements > 1: raise RuntimeError("multiple legacy /code/ blocks found") stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") backup_root = pathlib.Path("/var/backups/guanghu/front-door") / stamp backup_root.mkdir(parents=True, mode=0o700) shutil.copy2(SITE_CONFIG, backup_root / "guanghulab.nginx") if WEB_ROOT.exists(): shutil.copytree(WEB_ROOT, backup_root / "web-root") temporary_config = SITE_CONFIG.with_name(f"guanghulab.{stamp}.new") temporary_config.write_text(updated, encoding="utf-8") temporary_config.chmod(0o644) temporary_config.replace(SITE_CONFIG) WEB_ROOT.mkdir(parents=True, exist_ok=True) for name in SOURCE_FILES: destination = WEB_ROOT / name shutil.copy2(source / name, destination) destination.chmod(0o644) try: run("/usr/sbin/nginx", "-t") run("/usr/bin/systemctl", "reload", "nginx") if get_status("https://guanghulab.com/") != 200: raise RuntimeError("front-door homepage verification failed") if get_status("https://guanghulab.com/code/") not in (200, 303): raise RuntimeError("code-channel public route verification failed") if get_status("https://guanghulab.com/fifth-domain/api/v1/version") != 200: raise RuntimeError("legacy Fifth Domain verification failed") except Exception: shutil.copy2(backup_root / "guanghulab.nginx", SITE_CONFIG) if (backup_root / "web-root").exists(): shutil.rmtree(WEB_ROOT) shutil.copytree(backup_root / "web-root", WEB_ROOT) run("/usr/sbin/nginx", "-t") run("/usr/bin/systemctl", "reload", "nginx") raise print(f"LIGHT_FRONT_DOOR_DEPLOYED backup={backup_root}") if __name__ == "__main__": main()