feat: v1.2.0 — Gitea 地址可更改(默认 http://127.0.0.1:3000)
- 设置页新增「Gitea 地址」输入:同步引擎的 API/克隆/拉取全部改用该地址, 适配 Gitea 换了端口或部署在别处的场景 - 地址规范化:自动补 http://、去尾部斜杠,校验 scheme/主机名,非法值明确拒绝 - 保存前探测连通性(/api/v1/version),连不上拒绝保存并提示 - 改地址后自动把已有镜像仓库的 origin 重指到新地址,无需重新克隆 - 「打开 Gitea」按钮随设置端口 + 访问面板的主机名自适应(不再写死 3000) - 改地址/用户名不再误清 Gitea 密码(密码仅在密码行保存时提交) - 启动日志记录当前 Gitea 地址,便于排障 - manifest 1.1.2 → 1.2.0
This commit is contained in:
@@ -23,7 +23,7 @@ import uuid
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote, urlsplit
|
||||||
|
|
||||||
APP_DIR = Path(__file__).resolve().parent.parent
|
APP_DIR = Path(__file__).resolve().parent.parent
|
||||||
DATA_DIR = Path(os.environ.get("GHS_DATA_DIR", "/vol1/@appdata/GiteaHubSync"))
|
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():
|
if (DATA_DIR / "id_ed25519").exists():
|
||||||
SSH_KEY = DATA_DIR / "id_ed25519"
|
SSH_KEY = DATA_DIR / "id_ed25519"
|
||||||
KNOWN_HOSTS = DATA_DIR / "known_hosts"
|
KNOWN_HOSTS = DATA_DIR / "known_hosts"
|
||||||
GITEA_API = "http://127.0.0.1:3000/api/v1"
|
GITEA_URL_ENV = os.environ.get("GITEA_URL") # “打开 Gitea”按钮地址覆盖,一般不设
|
||||||
GITEA_UI = os.environ.get("GITEA_URL", "http://192.168.31.10:3000")
|
DEFAULT_GITEA_HOST = "http://127.0.0.1:3000" # 同步引擎访问的 Gitea 地址(设置页可改)
|
||||||
DEFAULT_REPOS = ["SMB_Android", "DCU_APP", "GD32E50X_BOOT", "DCU_DesktopController",
|
DEFAULT_REPOS = ["SMB_Android", "DCU_APP", "GD32E50X_BOOT", "DCU_DesktopController",
|
||||||
"Motor_v2", "DCU_ECAT_Master", "GD32_BOOT"]
|
"Motor_v2", "DCU_ECAT_Master", "GD32_BOOT"]
|
||||||
LEGACY_GH_USER = "cbb2625274797" # 旧版硬编码账号,仅用于首次运行的迁移
|
LEGACY_GH_USER = "cbb2625274797" # 旧版硬编码账号,仅用于首次运行的迁移
|
||||||
@@ -62,7 +62,46 @@ ENV["GIT_TERMINAL_PROMPT"] = "0"
|
|||||||
|
|
||||||
# ---------------- 应用设置(settings.json) ----------------
|
# ---------------- 应用设置(settings.json) ----------------
|
||||||
cfg = {"gitea_user": LEGACY_GITEA_USER, "gitea_pass": "", "auto_create": True,
|
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():
|
def sync_interval():
|
||||||
@@ -90,6 +129,9 @@ def load_settings():
|
|||||||
cfg["auto_create"] = bool(d.get("auto_create", True))
|
cfg["auto_create"] = bool(d.get("auto_create", True))
|
||||||
if valid_interval(d.get("interval")):
|
if valid_interval(d.get("interval")):
|
||||||
cfg["interval"] = int(d["interval"])
|
cfg["interval"] = int(d["interval"])
|
||||||
|
h = norm_gitea_host(d.get("gitea_host"))
|
||||||
|
if h:
|
||||||
|
cfg["gitea_host"] = h
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
write_gitcreds()
|
write_gitcreds()
|
||||||
@@ -107,8 +149,8 @@ def save_settings():
|
|||||||
|
|
||||||
|
|
||||||
def gitea_base():
|
def gitea_base():
|
||||||
"""Gitea 上当前用户的仓库前缀"""
|
"""Gitea 上当前用户的仓库前缀(地址可在设置页修改)"""
|
||||||
return f"http://127.0.0.1:3000/{cfg['gitea_user']}"
|
return f"{gitea_host()}/{cfg['gitea_user']}"
|
||||||
|
|
||||||
|
|
||||||
# ---------------- GitHub 多账号(accounts.json) ----------------
|
# ---------------- GitHub 多账号(accounts.json) ----------------
|
||||||
@@ -217,9 +259,11 @@ def write_gitcreds():
|
|||||||
这样镜像仓库的 remote 地址里不保存密码。清除密码时删除该文件。"""
|
这样镜像仓库的 remote 地址里不保存密码。清除密码时删除该文件。"""
|
||||||
try:
|
try:
|
||||||
if cfg.get("gitea_pass"):
|
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="")
|
user = quote(str(cfg["gitea_user"]), safe="")
|
||||||
pw = quote(str(cfg["gitea_pass"]), 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")
|
encoding="utf-8")
|
||||||
os.chmod(GITCREDS_FILE, 0o600)
|
os.chmod(GITCREDS_FILE, 0o600)
|
||||||
elif GITCREDS_FILE.exists():
|
elif GITCREDS_FILE.exists():
|
||||||
@@ -228,6 +272,20 @@ def write_gitcreds():
|
|||||||
pass
|
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():
|
def git_creds_args():
|
||||||
if cfg.get("gitea_pass") and GITCREDS_FILE.exists():
|
if cfg.get("gitea_pass") and GITCREDS_FILE.exists():
|
||||||
return ["-c", f"credential.helper=store --file={GITCREDS_FILE}"]
|
return ["-c", f"credential.helper=store --file={GITCREDS_FILE}"]
|
||||||
@@ -286,13 +344,33 @@ def gitea_api(path):
|
|||||||
cmd = ["curl", "-sS", "-m", "10"]
|
cmd = ["curl", "-sS", "-m", "10"]
|
||||||
if cfg.get("gitea_pass"):
|
if cfg.get("gitea_pass"):
|
||||||
cmd += ["-u", f"{cfg['gitea_user']}:{cfg['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)
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||||
return r.returncode == 0, json.loads(r.stdout or "{}")
|
return r.returncode == 0, json.loads(r.stdout or "{}")
|
||||||
except Exception:
|
except Exception:
|
||||||
return False, {}
|
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):
|
def gitea_repo_exists(name):
|
||||||
ok, d = gitea_api(f"/repos/{cfg['gitea_user']}/{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}"
|
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 私钥"}
|
"method": "Token(HTTPS)" if acc.get("token") else "SSH 私钥"}
|
||||||
|
|
||||||
|
|
||||||
def global_status():
|
def global_status(ui_url=None):
|
||||||
repos = load_repos()
|
repos = load_repos()
|
||||||
details = [repo_detail(r) for r in repos]
|
details = [repo_detail(r) for r in repos]
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -582,7 +660,7 @@ def global_status():
|
|||||||
"synced_count": sum(1 for x in details if x["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),
|
"total_size_mb": round(sum(x["size_mb"] for x in details), 1),
|
||||||
"repos": details,
|
"repos": details,
|
||||||
"gitea_url": GITEA_UI,
|
"gitea_url": ui_url or gitea_ui(),
|
||||||
"gitea_user": cfg["gitea_user"],
|
"gitea_user": cfg["gitea_user"],
|
||||||
"gitea_online": bool(gitea_all),
|
"gitea_online": bool(gitea_all),
|
||||||
"available": [r for r in gitea_all if r not in repos],
|
"available": [r for r in gitea_all if r not in repos],
|
||||||
@@ -759,7 +837,7 @@ PAGE_HOME = """
|
|||||||
</div>
|
</div>
|
||||||
<div class="btns">
|
<div class="btns">
|
||||||
<button class="b-sync" id="btnSync" onclick="syncNow()">⚡ 立即同步一次</button>
|
<button class="b-sync" id="btnSync" onclick="syncNow()">⚡ 立即同步一次</button>
|
||||||
<a class="btn b-gitea" href="__GITEA__" target="_blank">🗂️ 打开 Gitea(3000端口)</a>
|
<a class="btn b-gitea" href="__GITEA__" target="_blank">🗂️ 打开 Gitea</a>
|
||||||
<a class="btn b-gh" id="btnGh" href="__GH__" target="_blank">🐙 GitHub 仓库列表</a>
|
<a class="btn b-gh" id="btnGh" href="__GH__" target="_blank">🐙 GitHub 仓库列表</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -961,17 +1039,22 @@ PAGE_SETTINGS = """
|
|||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Gitea 与同步设置</h2>
|
<h2>Gitea 与同步设置</h2>
|
||||||
|
<label class="flabel">Gitea 地址<span class="sub">同步引擎访问的地址,默认 http://127.0.0.1:3000</span></label>
|
||||||
|
<div class="addrow">
|
||||||
|
<input id="giteaHost" placeholder="http://127.0.0.1:3000">
|
||||||
|
<button class="b-mini" id="btnGitea" onclick="saveGitea(false)">保存</button>
|
||||||
|
</div>
|
||||||
<label class="flabel">Gitea 用户名<span class="sub">面板按此用户名列出你可同步的仓库</span></label>
|
<label class="flabel">Gitea 用户名<span class="sub">面板按此用户名列出你可同步的仓库</span></label>
|
||||||
<div class="addrow">
|
<div class="addrow">
|
||||||
<input id="giteaUser" placeholder="cbb">
|
<input id="giteaUser" placeholder="cbb">
|
||||||
<button class="b-mini" id="btnGitea" onclick="saveGitea()">保存</button>
|
<button class="b-mini" onclick="saveGitea(false)">保存</button>
|
||||||
</div>
|
</div>
|
||||||
<label class="flabel">Gitea 密码<span class="sub">仅仓库为私有时需要;留空保存可清除</span></label>
|
<label class="flabel">Gitea 密码<span class="sub">仅仓库为私有时需要;留空保存可清除</span></label>
|
||||||
<div class="addrow">
|
<div class="addrow">
|
||||||
<input id="giteaPass" type="password" placeholder="未设置">
|
<input id="giteaPass" type="password" placeholder="未设置">
|
||||||
<button class="b-mini" onclick="saveGitea()">保存</button>
|
<button class="b-mini" onclick="saveGitea(true)">保存</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tip">本地 Gitea 地址 __GITEAURL__;密码只保存在 NAS 本地,仅用于读取私有仓库。</div>
|
<div class="tip" id="giteaUrlTip"></div>
|
||||||
<div class="optbox">
|
<div class="optbox">
|
||||||
<div class="optrow">
|
<div class="optrow">
|
||||||
<label class="switch"><input type="checkbox" id="swAuto" onchange="saveAuto()"><span class="slider"></span></label>
|
<label class="switch"><input type="checkbox" id="swAuto" onchange="saveAuto()"><span class="slider"></span></label>
|
||||||
@@ -998,9 +1081,11 @@ function showMsg(id,t,ok){const m=document.getElementById(id);m.textContent=t;m.
|
|||||||
async function refresh(){
|
async function refresh(){
|
||||||
try{
|
try{
|
||||||
const s=await jget('api/settings');
|
const s=await jget('api/settings');
|
||||||
|
document.getElementById('giteaHost').value=s.gitea_host||'http://127.0.0.1:3000';
|
||||||
document.getElementById('giteaUser').value=s.gitea_user||'';
|
document.getElementById('giteaUser').value=s.gitea_user||'';
|
||||||
document.getElementById('giteaPass').placeholder=s.gitea_pass_set
|
document.getElementById('giteaPass').placeholder=s.gitea_pass_set
|
||||||
?'Gitea 密码(已设置;留空保存可清除)':'Gitea 密码(私有仓库必填)';
|
?'Gitea 密码(已设置;留空保存可清除)':'Gitea 密码(私有仓库必填)';
|
||||||
|
document.getElementById('giteaUrlTip').textContent='「打开 Gitea」按钮地址:'+(s.gitea_url||'—')+'(随访问地址自动适配);密码只保存在 NAS 本地,仅用于读取私有仓库。';
|
||||||
document.getElementById('swAuto').checked=!!s.auto_create;
|
document.getElementById('swAuto').checked=!!s.auto_create;
|
||||||
const iv=document.getElementById('syncInterval');
|
const iv=document.getElementById('syncInterval');
|
||||||
iv.value=s.interval||10;iv.min=s.min_interval||5;iv.max=s.max_interval||86400;
|
iv.value=s.interval||10;iv.min=s.min_interval||5;iv.max=s.max_interval||86400;
|
||||||
@@ -1073,12 +1158,14 @@ async function delAcc(id){
|
|||||||
const r=await (await fetch('api/accounts/remove',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})})).json();
|
const r=await (await fetch('api/accounts/remove',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})})).json();
|
||||||
showMsg('msgAcc',r.msg||JSON.stringify(r),!!r.ok);refresh();
|
showMsg('msgAcc',r.msg||JSON.stringify(r),!!r.ok);refresh();
|
||||||
}
|
}
|
||||||
async function saveGitea(){
|
async function saveGitea(withPass){
|
||||||
const gitea_user=document.getElementById('giteaUser').value.trim();
|
// withPass=false 时不发送密码字段,避免改地址/用户名时误清密码
|
||||||
const gitea_pass=document.getElementById('giteaPass').value;
|
const body={gitea_host:document.getElementById('giteaHost').value.trim(),
|
||||||
const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({gitea_user,gitea_pass})})).json();
|
gitea_user:document.getElementById('giteaUser').value.trim()};
|
||||||
|
if(withPass)body.gitea_pass=document.getElementById('giteaPass').value;
|
||||||
|
const r=await (await fetch('api/settings/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
|
||||||
showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok);
|
showMsg('msgCfg',r.msg||JSON.stringify(r),!!r.ok);
|
||||||
document.getElementById('giteaPass').value='';
|
if(withPass)document.getElementById('giteaPass').value='';
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
async function saveAuto(){
|
async function saveAuto(){
|
||||||
@@ -1138,7 +1225,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
path = self.path.split("?")[0].rstrip("/") or "/"
|
path = self.path.split("?")[0].rstrip("/") or "/"
|
||||||
if path == "/":
|
if path == "/":
|
||||||
acc = active_account()
|
acc = active_account()
|
||||||
body = (PAGE_HOME.replace("__GITEA__", GITEA_UI)
|
body = (PAGE_HOME.replace("__GITEA__", gitea_ui(self.headers.get("Host")))
|
||||||
.replace("__GH__", f"https://github.com/{acc['gh_user']}?tab=repositories" if acc else "")
|
.replace("__GH__", f"https://github.com/{acc['gh_user']}?tab=repositories" if acc else "")
|
||||||
.replace("__LOGPATH__", str(LOG_FILE)))
|
.replace("__LOGPATH__", str(LOG_FILE)))
|
||||||
self._html(page("控制面板", body, "home"))
|
self._html(page("控制面板", body, "home"))
|
||||||
@@ -1147,8 +1234,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
elif path == "/manage":
|
elif path == "/manage":
|
||||||
self._html(page("项目管理", PAGE_MANAGE, "manage"))
|
self._html(page("项目管理", PAGE_MANAGE, "manage"))
|
||||||
elif path == "/settings":
|
elif path == "/settings":
|
||||||
body = PAGE_SETTINGS.replace("__GITEAURL__", GITEA_UI)
|
self._html(page("设置", PAGE_SETTINGS, "settings"))
|
||||||
self._html(page("设置", body, "settings"))
|
|
||||||
elif path == "/api/state":
|
elif path == "/api/state":
|
||||||
repos = load_repos()
|
repos = load_repos()
|
||||||
acc = active_account()
|
acc = active_account()
|
||||||
@@ -1171,7 +1257,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
"interval": sync_interval(),
|
"interval": sync_interval(),
|
||||||
"min_interval": MIN_INTERVAL,
|
"min_interval": MIN_INTERVAL,
|
||||||
"max_interval": MAX_INTERVAL,
|
"max_interval": MAX_INTERVAL,
|
||||||
"gitea_url": GITEA_UI,
|
"gitea_host": gitea_host(),
|
||||||
|
"gitea_url": gitea_ui(self.headers.get("Host")),
|
||||||
"active": active_id,
|
"active": active_id,
|
||||||
"accounts": [account_public(a) for a in accounts]})
|
"accounts": [account_public(a) for a in accounts]})
|
||||||
elif path == "/api/log":
|
elif path == "/api/log":
|
||||||
@@ -1191,7 +1278,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
ok, msg = run_sync()
|
ok, msg = run_sync()
|
||||||
self._json({"ok": ok, "msg": msg})
|
self._json({"ok": ok, "msg": msg})
|
||||||
elif path == "/api/settings/save":
|
elif path == "/api/settings/save":
|
||||||
new_user = new_pass = None
|
new_user = new_pass = new_host = None
|
||||||
if "gitea_user" in data:
|
if "gitea_user" in data:
|
||||||
u = (data.get("gitea_user") or "").strip()
|
u = (data.get("gitea_user") or "").strip()
|
||||||
if not valid_name(u):
|
if not valid_name(u):
|
||||||
@@ -1200,22 +1287,35 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
new_user = u
|
new_user = u
|
||||||
if "gitea_pass" in data:
|
if "gitea_pass" in data:
|
||||||
new_pass = str(data.get("gitea_pass") or "") # 空串 = 清除密码
|
new_pass = str(data.get("gitea_pass") or "") # 空串 = 清除密码
|
||||||
|
if "gitea_host" in data:
|
||||||
|
h = norm_gitea_host(data.get("gitea_host"))
|
||||||
|
if not h:
|
||||||
|
self._json({"ok": False, "msg": "Gitea 地址不合法,应类似 http://127.0.0.1:3000"})
|
||||||
|
return
|
||||||
|
new_host = h
|
||||||
if "interval" in data:
|
if "interval" in data:
|
||||||
if not valid_interval(data.get("interval")):
|
if not valid_interval(data.get("interval")):
|
||||||
self._json({"ok": False, "msg": f"同步间隔须为 {MIN_INTERVAL} ~ {MAX_INTERVAL} 之间的整数(秒)"})
|
self._json({"ok": False, "msg": f"同步间隔须为 {MIN_INTERVAL} ~ {MAX_INTERVAL} 之间的整数(秒)"})
|
||||||
return
|
return
|
||||||
# 预校验:涉及 Gitea 账号的改动,先用新凭据调一次 /user 验证
|
# 预校验:新地址先探连通性;涉及账号的改动再用新凭据调一次 /user 验证
|
||||||
trial = dict(cfg)
|
trial = dict(cfg)
|
||||||
if new_user is not None:
|
if new_user is not None:
|
||||||
trial["gitea_user"] = new_user
|
trial["gitea_user"] = new_user
|
||||||
if new_pass is not None:
|
if new_pass is not None:
|
||||||
trial["gitea_pass"] = new_pass
|
trial["gitea_pass"] = new_pass
|
||||||
|
if new_host is not None:
|
||||||
|
trial["gitea_host"] = new_host
|
||||||
|
ok, msg = gitea_probe(new_host)
|
||||||
|
if not ok:
|
||||||
|
self._json({"ok": False, "msg": msg})
|
||||||
|
return
|
||||||
if (new_user is not None or new_pass is not None) and trial["gitea_pass"]:
|
if (new_user is not None or new_pass is not None) and trial["gitea_pass"]:
|
||||||
try:
|
try:
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
["curl", "-sS", "-m", "10", "-w", "\n%{http_code}",
|
["curl", "-sS", "-m", "10", "-w", "\n%{http_code}",
|
||||||
"-u", f"{trial['gitea_user']}:{trial['gitea_pass']}",
|
"-u", f"{trial['gitea_user']}:{trial['gitea_pass']}",
|
||||||
f"{GITEA_API}/user"], capture_output=True, text=True, timeout=15)
|
f"{trial['gitea_host']}/api/v1/user"],
|
||||||
|
capture_output=True, text=True, timeout=15)
|
||||||
out, _, code = r.stdout.rpartition("\n")
|
out, _, code = r.stdout.rpartition("\n")
|
||||||
st = int(code) if code.strip().isdigit() else 0
|
st = int(code) if code.strip().isdigit() else 0
|
||||||
if not (200 <= st < 300):
|
if not (200 <= st < 300):
|
||||||
@@ -1228,11 +1328,14 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._json({"ok": False, "msg": f"Gitea 账号验证出错: {e}"})
|
self._json({"ok": False, "msg": f"Gitea 账号验证出错: {e}"})
|
||||||
return
|
return
|
||||||
|
host_changed = new_host is not None and new_host != gitea_host()
|
||||||
if new_user is not None:
|
if new_user is not None:
|
||||||
cfg["gitea_user"] = new_user
|
cfg["gitea_user"] = new_user
|
||||||
if new_pass is not None:
|
if new_pass is not None:
|
||||||
cfg["gitea_pass"] = new_pass
|
cfg["gitea_pass"] = new_pass
|
||||||
log("Gitea 密码已" + ("更新" if new_pass else "清除"))
|
log("Gitea 密码已" + ("更新" if new_pass else "清除"))
|
||||||
|
if new_host is not None:
|
||||||
|
cfg["gitea_host"] = new_host
|
||||||
if "auto_create" in data:
|
if "auto_create" in data:
|
||||||
cfg["auto_create"] = bool(data.get("auto_create"))
|
cfg["auto_create"] = bool(data.get("auto_create"))
|
||||||
if "interval" in data:
|
if "interval" in data:
|
||||||
@@ -1241,15 +1344,23 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
if cfg["interval"] != old_iv:
|
if cfg["interval"] != old_iv:
|
||||||
log(f"同步间隔已调整: {old_iv}s -> {cfg['interval']}s")
|
log(f"同步间隔已调整: {old_iv}s -> {cfg['interval']}s")
|
||||||
save_settings()
|
save_settings()
|
||||||
|
notes = []
|
||||||
|
if new_host is not None:
|
||||||
|
ok, msg = gitea_probe()
|
||||||
|
notes.append(f"Gitea 地址已设为 {new_host}({msg})")
|
||||||
|
if host_changed:
|
||||||
|
n = repoint_mirror_remotes()
|
||||||
|
log(f"Gitea 地址变更: {n} 个镜像仓库的 origin 已更新")
|
||||||
|
notes.append(f"{n} 个已有镜像的拉取地址已更新")
|
||||||
if cfg.get("gitea_pass"):
|
if cfg.get("gitea_pass"):
|
||||||
msg = "已保存(Gitea 密码验证通过 ✓,可同步私有仓库)"
|
notes.append("Gitea 密码验证通过 ✓,可同步私有仓库")
|
||||||
else:
|
else:
|
||||||
ok, d = gitea_api(f"/users/{cfg['gitea_user']}")
|
ok, d = gitea_api(f"/users/{cfg['gitea_user']}")
|
||||||
if ok and isinstance(d, dict) and d.get("login"):
|
if ok and isinstance(d, dict) and d.get("login"):
|
||||||
msg = "已保存(未配置 Gitea 密码,只能同步公开仓库;仓库为私有时请填写密码)"
|
notes.append("未配置 Gitea 密码,只能同步公开仓库;仓库为私有时请填写密码")
|
||||||
else:
|
else:
|
||||||
msg = f"已保存(Gitea 上找不到用户 {cfg['gitea_user']},请检查)"
|
notes.append(f"注意:在新地址上找不到用户 {cfg['gitea_user']},请检查")
|
||||||
self._json({"ok": True, "msg": msg})
|
self._json({"ok": True, "msg": "已保存(" + ";".join(notes) + ")"})
|
||||||
elif path == "/api/accounts/add":
|
elif path == "/api/accounts/add":
|
||||||
token = (data.get("token") or "").strip()
|
token = (data.get("token") or "").strip()
|
||||||
label = (data.get("label") or "").strip()
|
label = (data.get("label") or "").strip()
|
||||||
@@ -1440,7 +1551,7 @@ def main():
|
|||||||
log(f"GiteaHubSync 守护进程启动 (pid {os.getpid()}, 自动同步={'开' if state['enabled'] else '关'}, "
|
log(f"GiteaHubSync 守护进程启动 (pid {os.getpid()}, 自动同步={'开' if state['enabled'] else '关'}, "
|
||||||
f"项目 {len(load_repos())} 个, GitHub 账号={acc['label'] if acc else '无'}"
|
f"项目 {len(load_repos())} 个, GitHub 账号={acc['label'] if acc else '无'}"
|
||||||
f"({acc['gh_user'] if acc else '-'}/{'Token' if acc and acc.get('token') else 'SSH'}), "
|
f"({acc['gh_user'] if acc else '-'}/{'Token' if acc and acc.get('token') else 'SSH'}), "
|
||||||
f"Gitea 用户={cfg['gitea_user']}, 自动创建={'开' if cfg.get('auto_create') else '关'})")
|
f"Gitea 用户={cfg['gitea_user']}, 地址={gitea_host()}, 自动创建={'开' if cfg.get('auto_create') else '关'})")
|
||||||
threading.Thread(target=loop, daemon=True).start()
|
threading.Thread(target=loop, daemon=True).start()
|
||||||
server = ThreadingHTTPServer(("0.0.0.0", PANEL_PORT), Handler)
|
server = ThreadingHTTPServer(("0.0.0.0", PANEL_PORT), Handler)
|
||||||
log(f"控制面板监听 :{PANEL_PORT}")
|
log(f"控制面板监听 :{PANEL_PORT}")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
appname = GiteaHubSync
|
appname = GiteaHubSync
|
||||||
version = 1.1.2
|
version = 1.2.0
|
||||||
display_name = Gitea→GitHub 同步助手
|
display_name = Gitea→GitHub 同步助手
|
||||||
desc = 每次推送到飞牛 Gitea 的代码,10 秒内自动镜像到 GitHub 的 7 个私有仓库。内置控制面板:自动同步开关、立即同步、仓库状态、同步日志,一键打开 Gitea。数据全部保存在 NAS 本地。
|
desc = 每次推送到飞牛 Gitea 的代码,10 秒内自动镜像到 GitHub 的 7 个私有仓库。内置控制面板:自动同步开关、立即同步、仓库状态、同步日志,一键打开 Gitea。数据全部保存在 NAS 本地。
|
||||||
platform = all
|
platform = all
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ NAS 重启自动恢复。控制面板带自动同步开关、立即同步、仓
|
|||||||
- **多 GitHub 账号**:设置页可添加多个 GitHub 账号(用户名 + Personal Access Token),
|
- **多 GitHub 账号**:设置页可添加多个 GitHub 账号(用户名 + Personal Access Token),
|
||||||
一键切换推送目标;每个账号独立记录同步进度,切回原账号不会重复推送
|
一键切换推送目标;每个账号独立记录同步进度,切回原账号不会重复推送
|
||||||
- **同步间隔可调**:设置页可改检查间隔(5 秒 ~ 1 天,默认 10 秒),改完立即生效
|
- **同步间隔可调**:设置页可改检查间隔(5 秒 ~ 1 天,默认 10 秒),改完立即生效
|
||||||
|
- **Gitea 地址可改**:设置页可改同步引擎访问的 Gitea 地址(默认 `http://127.0.0.1:3000`),
|
||||||
|
保存前自动探测连通性;改地址后已有镜像的拉取地址自动更新,「打开 Gitea」按钮随访问地址自适应
|
||||||
- **自动创建仓库**:Gitea 上的新仓库加入同步时,若 GitHub 上不存在同名仓库,
|
- **自动创建仓库**:Gitea 上的新仓库加入同步时,若 GitHub 上不存在同名仓库,
|
||||||
自动按账号设置为私有/公开创建(可开关)
|
自动按账号设置为私有/公开创建(可开关)
|
||||||
- **两种推送方式**:配置了 Token 的账号走 HTTPS 推送(通用,任何人都能用自己的
|
- **两种推送方式**:配置了 Token 的账号走 HTTPS 推送(通用,任何人都能用自己的
|
||||||
@@ -63,6 +65,9 @@ FNOS_HOST=192.168.31.10 FNOS_USER=<管理员账号> FNOS_PASS=<密码> \
|
|||||||
> Token 只保存在 NAS 本地 `accounts.json`(权限 600),不上传任何地方;
|
> Token 只保存在 NAS 本地 `accounts.json`(权限 600),不上传任何地方;
|
||||||
> 日志中的 Token 会自动打码。卸载应用不会动 GitHub 上的任何仓库。
|
> 日志中的 Token 会自动打码。卸载应用不会动 GitHub 上的任何仓库。
|
||||||
|
|
||||||
|
如果你的 Gitea 不在默认的 `127.0.0.1:3000`(比如换了端口),到设置页「Gitea 地址」一栏改掉即可,
|
||||||
|
保存时会自动探测连通性,并同步更新所有已有镜像的拉取地址。
|
||||||
|
|
||||||
## 重新构建
|
## 重新构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -76,5 +81,5 @@ cd /vol1/@appshare/fn-deepseek-harness/fnos-gitea-sync
|
|||||||
端口直连,私钥随应用打包安装在 NAS 本地
|
端口直连,私钥随应用打包安装在 NAS 本地
|
||||||
- 从 v1.0.0 升级会自动迁移:旧的同步进度归入「默认(SSH 私钥)」账号,行为不变
|
- 从 v1.0.0 升级会自动迁移:旧的同步进度归入「默认(SSH 私钥)」账号,行为不变
|
||||||
- 数据文件:`accounts.json`(GitHub 账号,含 Token)、`settings.json`
|
- 数据文件:`accounts.json`(GitHub 账号,含 Token)、`settings.json`
|
||||||
(Gitea 用户名 / 自动创建开关)、`repos.json`(同步列表)、`.last-pushed/<账号>/<仓库>`
|
(Gitea 地址/用户名/密码、自动创建开关、同步间隔)、`repos.json`(同步列表)、
|
||||||
(每个账号的同步进度)
|
`.last-pushed/<账号>/<仓库>`(每个账号的同步进度)
|
||||||
|
|||||||
Reference in New Issue
Block a user