pages: a site can be org/repo#branch/subdir; a hook pulls every site on that repo
This commit is contained in:
1 parent
517e496061
commit
1f15b5b7be
1 file changed
+31
-15
+31
-15
@@ -1,7 +1,8 @@
|
||||
"""Teahub pages: serve each org's public `site` repo by hostname.
|
||||
|
||||
SITES maps hostnames to `org/repo` on GITEA (public repos only; cloned over https, no token). A clone lives under
|
||||
/data/<org>__<repo>; `POST /_hook/<org>/<repo>` (a Gitea webhook) or the periodic loop pulls it. Static files only:
|
||||
SITES maps hostnames to `org/repo`, or `org/repo#branch/subdir`, on GITEA (public repos only; cloned over https, no
|
||||
token). A clone lives under /data/<org>__<repo>__<branch>; `POST /_hook/<org>/<repo>` (a Gitea webhook) or the
|
||||
periodic loop pulls it. Static files only:
|
||||
a path maps to the file, a directory to its index.html, anything missing to 404.html or a plain 404. No listing, no
|
||||
execution, nothing writes except git. stdlib only.
|
||||
"""
|
||||
@@ -26,21 +27,35 @@ TYPES = {".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8",
|
||||
LOCKS: dict[str, threading.Lock] = {}
|
||||
|
||||
|
||||
def clone_dir(repo: str) -> Path:
|
||||
return DATA / repo.replace("/", "__")
|
||||
def parse(site: str) -> tuple[str, str, str]:
|
||||
"""'org/repo#branch/sub/dir' -> (repo, branch, subdir); branch '' means the default branch."""
|
||||
repo, _, rest = site.partition("#")
|
||||
branch, _, sub = rest.partition("/")
|
||||
return repo, branch, sub.strip("/")
|
||||
|
||||
|
||||
def sync(repo: str) -> str:
|
||||
def clone_dir(site: str) -> Path:
|
||||
repo, branch, _ = parse(site)
|
||||
return DATA / (repo.replace("/", "__") + (f"__{branch}" if branch else ""))
|
||||
|
||||
|
||||
def site_root(site: str) -> Path:
|
||||
return clone_dir(site) / parse(site)[2]
|
||||
|
||||
|
||||
def sync(site: str) -> str:
|
||||
"""Clone or fast-forward one site repo. Returns the short HEAD or an error line."""
|
||||
d = clone_dir(repo)
|
||||
lock = LOCKS.setdefault(repo, threading.Lock())
|
||||
repo, branch, _ = parse(site)
|
||||
d = clone_dir(site)
|
||||
lock = LOCKS.setdefault(site, threading.Lock())
|
||||
with lock:
|
||||
try:
|
||||
if (d / ".git").exists():
|
||||
subprocess.run(["git", "-C", str(d), "fetch", "-q", "--depth", "1", "origin"], check=True, capture_output=True, timeout=120)
|
||||
subprocess.run(["git", "-C", str(d), "reset", "-q", "--hard", "origin/HEAD"], check=True, capture_output=True, timeout=60)
|
||||
subprocess.run(["git", "-C", str(d), "reset", "-q", "--hard", f"origin/{branch}" if branch else "origin/HEAD"], check=True, capture_output=True, timeout=60)
|
||||
else:
|
||||
subprocess.run(["git", "clone", "-q", "--depth", "1", f"{GITEA}/{repo}.git", str(d)], check=True, capture_output=True, timeout=180)
|
||||
subprocess.run(["git", "clone", "-q", "--depth", "1", *(["--branch", branch] if branch else []), f"{GITEA}/{repo}.git", str(d)],
|
||||
check=True, capture_output=True, timeout=180)
|
||||
return subprocess.run(["git", "-C", str(d), "rev-parse", "--short", "HEAD"], capture_output=True, text=True).stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
return f"error: {e.stderr.decode(errors='replace')[-200:]}"
|
||||
@@ -67,11 +82,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
def do_POST(self):
|
||||
parts = urlsplit(self.path).path.strip("/").split("/")
|
||||
if len(parts) == 3 and parts[0] == "_hook" and f"{parts[1]}/{parts[2]}" in SITES.values():
|
||||
hit = [s for s in SITES.values() if len(parts) == 3 and parts[0] == "_hook" and parse(s)[0] == f"{parts[1]}/{parts[2]}"]
|
||||
if hit:
|
||||
self.rfile.read(int(self.headers.get("content-length") or 0))
|
||||
out = sync(f"{parts[1]}/{parts[2]}")
|
||||
body = out.encode()
|
||||
self.send_response(200 if not out.startswith("error") else 502)
|
||||
outs = [sync(s) for s in dict.fromkeys(hit)] # every site on that repo (any branch)
|
||||
body = "\n".join(outs).encode()
|
||||
self.send_response(200 if not any(o.startswith("error") for o in outs) else 502)
|
||||
self.send_header("content-type", "text/plain"); self.send_header("content-length", str(len(body))); self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return
|
||||
@@ -83,7 +99,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self, head: bool = False):
|
||||
path = urlsplit(self.path).path
|
||||
if path == "/healthz":
|
||||
body = json.dumps({"ok": True, "sites": {h: (clone_dir(r) / ".git").exists() for h, r in SITES.items()}}).encode()
|
||||
body = json.dumps({"ok": True, "sites": {h: site_root(r).is_dir() for h, r in SITES.items()}}).encode()
|
||||
self.send_response(200); self.send_header("content-type", "application/json"); self.send_header("content-length", str(len(body))); self.end_headers()
|
||||
if not head:
|
||||
self.wfile.write(body)
|
||||
@@ -92,7 +108,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if not repo:
|
||||
self.send_error(404, "no site for this host")
|
||||
return
|
||||
root = clone_dir(repo).resolve()
|
||||
root = site_root(repo).resolve()
|
||||
rel = unquote(path).lstrip("/")
|
||||
target = (root / rel).resolve() if rel else root
|
||||
if root not in target.parents and target != root:
|
||||
|
||||
Reference in new issue
Block a user