diff --git a/GiteaHubSync/app/backend/panel.py b/GiteaHubSync/app/backend/panel.py index 5acf757..888b0d8 100644 --- a/GiteaHubSync/app/backend/panel.py +++ b/GiteaHubSync/app/backend/panel.py @@ -23,7 +23,7 @@ import uuid from datetime import datetime from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path -from urllib.parse import quote +from urllib.parse import quote, urlsplit APP_DIR = Path(__file__).resolve().parent.parent DATA_DIR = Path(os.environ.get("GHS_DATA_DIR", "/vol1/@appdata/GiteaHubSync")) @@ -41,8 +41,8 @@ SSH_KEY = APP_DIR / "ssh" / "id_ed25519" if (DATA_DIR / "id_ed25519").exists(): SSH_KEY = DATA_DIR / "id_ed25519" KNOWN_HOSTS = DATA_DIR / "known_hosts" -GITEA_API = "http://127.0.0.1:3000/api/v1" -GITEA_UI = os.environ.get("GITEA_URL", "http://192.168.31.10:3000") +GITEA_URL_ENV = os.environ.get("GITEA_URL") # “打开 Gitea”按钮地址覆盖,一般不设 +DEFAULT_GITEA_HOST = "http://127.0.0.1:3000" # 同步引擎访问的 Gitea 地址(设置页可改) DEFAULT_REPOS = ["SMB_Android", "DCU_APP", "GD32E50X_BOOT", "DCU_DesktopController", "Motor_v2", "DCU_ECAT_Master", "GD32_BOOT"] LEGACY_GH_USER = "cbb2625274797" # 旧版硬编码账号,仅用于首次运行的迁移 @@ -62,7 +62,46 @@ ENV["GIT_TERMINAL_PROMPT"] = "0" # ---------------- 应用设置(settings.json) ---------------- cfg = {"gitea_user": LEGACY_GITEA_USER, "gitea_pass": "", "auto_create": True, - "interval": DEFAULT_INTERVAL} + "interval": DEFAULT_INTERVAL, "gitea_host": DEFAULT_GITEA_HOST} + + +def norm_gitea_host(h): + """规范化 Gitea 地址:补 http://、去尾部斜杠;不合法返回 None""" + h = str(h or "").strip().rstrip("/") + if not h: + return None + if "://" not in h: + h = "http://" + h + try: + p = urlsplit(h) + host = p.hostname or "" + if p.scheme in ("http", "https") and host \ + and not p.path.strip("/") and not p.query and not p.fragment \ + and re.fullmatch(r"[A-Za-z0-9.\-:\[\]]+", host): + return h + except Exception: + pass + return None + + +def gitea_host(): + return cfg.get("gitea_host") or DEFAULT_GITEA_HOST + + +def gitea_ui(host_header=""): + """「打开 Gitea」按钮地址:优先环境变量;否则用设置里的地址, + 但把 127.0.0.1/localhost 替换成访问面板的主机名(浏览器打不开本机回环)""" + if GITEA_URL_ENV: + return GITEA_URL_ENV + base = gitea_host() + try: + p = urlsplit(base) + host = (host_header or "").split(":")[0].strip() + if host and p.hostname in ("127.0.0.1", "localhost", "0.0.0.0", "::1", ""): + return f"{p.scheme}://{host}" + (f":{p.port}" if p.port else "") + except Exception: + pass + return base def sync_interval(): @@ -90,6 +129,9 @@ def load_settings(): cfg["auto_create"] = bool(d.get("auto_create", True)) if valid_interval(d.get("interval")): cfg["interval"] = int(d["interval"]) + h = norm_gitea_host(d.get("gitea_host")) + if h: + cfg["gitea_host"] = h except Exception: pass write_gitcreds() @@ -107,8 +149,8 @@ def save_settings(): def gitea_base(): - """Gitea 上当前用户的仓库前缀""" - return f"http://127.0.0.1:3000/{cfg['gitea_user']}" + """Gitea 上当前用户的仓库前缀(地址可在设置页修改)""" + return f"{gitea_host()}/{cfg['gitea_user']}" # ---------------- GitHub 多账号(accounts.json) ---------------- @@ -217,9 +259,11 @@ def write_gitcreds(): 这样镜像仓库的 remote 地址里不保存密码。清除密码时删除该文件。""" try: if cfg.get("gitea_pass"): + p = urlsplit(gitea_host()) + netloc = p.hostname + (f":{p.port}" if p.port else "") user = quote(str(cfg["gitea_user"]), safe="") pw = quote(str(cfg["gitea_pass"]), safe="") - GITCREDS_FILE.write_text(f"http://{user}:{pw}@127.0.0.1:3000\n", + GITCREDS_FILE.write_text(f"{p.scheme}://{user}:{pw}@{netloc}\n", encoding="utf-8") os.chmod(GITCREDS_FILE, 0o600) elif GITCREDS_FILE.exists(): @@ -228,6 +272,20 @@ def write_gitcreds(): pass +def repoint_mirror_remotes(): + """Gitea 地址变更后,把已有镜像仓库的 origin 指向新地址""" + n = 0 + try: + for repo in REPO_DIR.glob("*.git"): + r = git(["-C", str(repo), "remote", "set-url", "origin", + f"{gitea_base()}/{repo.name[:-4]}.git"], timeout=20) + if r.returncode == 0: + n += 1 + except Exception as e: + log(f"更新镜像 remote 失败: {e}") + return n + + def git_creds_args(): if cfg.get("gitea_pass") and GITCREDS_FILE.exists(): return ["-c", f"credential.helper=store --file={GITCREDS_FILE}"] @@ -286,13 +344,33 @@ def gitea_api(path): cmd = ["curl", "-sS", "-m", "10"] if cfg.get("gitea_pass"): cmd += ["-u", f"{cfg['gitea_user']}:{cfg['gitea_pass']}"] - cmd.append(f"{GITEA_API}{path}") + cmd.append(f"{gitea_host()}/api/v1{path}") r = subprocess.run(cmd, capture_output=True, text=True, timeout=15) return r.returncode == 0, json.loads(r.stdout or "{}") except Exception: return False, {} +def gitea_probe(host=None): + """探测 Gitea 地址连通性,返回 (ok, msg)""" + host = host or gitea_host() + try: + r = subprocess.run(["curl", "-sS", "-m", "8", "-w", "\n%{http_code}", + f"{host}/api/v1/version"], + capture_output=True, text=True, timeout=12) + out, _, code = r.stdout.rpartition("\n") + st = int(code) if code.strip().isdigit() else 0 + if 200 <= st < 300: + try: + ver = json.loads(out or "{}").get("version", "?") + except Exception: + ver = "?" + return True, f"连接成功,Gitea 版本 {ver}" + return False, f"地址连不上(HTTP {st or '超时'}),请检查后重试" + except Exception as e: + return False, f"地址连不上:{e}" + + def gitea_repo_exists(name): ok, d = gitea_api(f"/repos/{cfg['gitea_user']}/{name}") return ok and isinstance(d, dict) and d.get("full_name") == f"{cfg['gitea_user']}/{name}" @@ -559,7 +637,7 @@ def account_public(acc): "method": "Token(HTTPS)" if acc.get("token") else "SSH 私钥"} -def global_status(): +def global_status(ui_url=None): repos = load_repos() details = [repo_detail(r) for r in repos] now = time.time() @@ -582,7 +660,7 @@ def global_status(): "synced_count": sum(1 for x in details if x["status"] == "已同步"), "total_size_mb": round(sum(x["size_mb"] for x in details), 1), "repos": details, - "gitea_url": GITEA_UI, + "gitea_url": ui_url or gitea_ui(), "gitea_user": cfg["gitea_user"], "gitea_online": bool(gitea_all), "available": [r for r in gitea_all if r not in repos], @@ -759,7 +837,7 @@ PAGE_HOME = """
@@ -961,17 +1039,22 @@ PAGE_SETTINGS = """