master-zapier-plan-draft/scripts/rewrite-docs-host.py
George Lambert 20155de96b
Some checks are pending
offline / test (push) Waiting to run
Make catalog PDF links host-relative and add a hostname rewrite script.
New builds write path-relative PDF annotations so the tree can be served
from docs.verae-time.net (or any host at the site root) without a rebuild.
scripts/rewrite-docs-host.py retargets an already-built site when absolute
URLs are preferred. Live service doors stay on their own hostnames.
2026-09-11 23:00:12 -04:00

150 lines
5.5 KiB
Python
Executable file

#!/usr/bin/env python3
"""Rewrite catalog hostnames in a built ./site tree (HTML + PDF annotations).
Catalog HTML links are already relative. PDF annotations used to be baked as
https://zapier.georgelambert.org/… — this script retargets those (and any HTML
that still names a docs origin) when the tree is hosted on another name, e.g.
docs.verae-time.net.
It does **not** rewrite live service doors (portal/api/iam/…) or Forgejo.
Examples:
# Lab catalog → future docs host (keep absolute URLs)
python3 scripts/rewrite-docs-host.py \\
--site ./site \\
--from https://zapier.georgelambert.org \\
--to https://docs.verae-time.net
# Drop the host entirely (path-relative PDF links; any hostname at site root)
python3 scripts/rewrite-docs-host.py --site ./site --relative
python3 scripts/rewrite-docs-host.py --site ./site --from … --to … --dry-run
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _norm_origin(url: str) -> str:
url = url.strip()
if not url:
return ""
if "://" not in url:
url = "https://" + url
return url.rstrip("/") + "/"
def rewrite_html(site: Path, src: str, dst: str, dry: bool) -> int:
n = 0
src_bare = src.rstrip("/")
dst_bare = dst.rstrip("/") or "/"
for path in site.rglob("*"):
if not path.is_file() or path.suffix.lower() not in {".html", ".htm", ".xml", ".svg", ".js", ".css", ".md"}:
continue
text = path.read_text(encoding="utf-8", errors="replace")
if src not in text and src_bare not in text:
continue
new = text.replace(src, dst).replace(src_bare, dst_bare)
if new == text:
continue
n += 1
if dry:
print(f" html {path.relative_to(site)}")
else:
path.write_text(new, encoding="utf-8")
return n
def rewrite_pdfs(site: Path, src: str, dst: str | None, dry: bool) -> int:
try:
from pypdf import PdfReader, PdfWriter
from pypdf.generic import NameObject, create_string_object
except ImportError:
print("pypdf missing; pip install pypdf", file=sys.stderr)
return 0
n_pdf = 0
n_uri = 0
src_http = src.replace("https://", "http://", 1) if src.startswith("https://") else src
for pdf in site.rglob("*.pdf"):
try:
reader = PdfReader(str(pdf))
except Exception:
continue
changed = False
for page in reader.pages:
annots = page.get("/Annots")
if not annots:
continue
for annot in annots:
obj = annot.get_object()
action = obj.get("/A")
if not action:
continue
uri = action.get("/URI")
if not uri:
continue
raw = str(uri)
new = raw
if raw.startswith(src) or raw.startswith(src_http):
rest = raw[len(src) :] if raw.startswith(src) else raw[len(src_http) :]
if dst:
new = dst + rest
else:
dest = site / rest.split("#", 1)[0]
frag = "#" + rest.split("#", 1)[1] if "#" in rest else ""
if not dest.exists():
dest = site / "index.html"
rel = os.path.relpath(dest.resolve(), pdf.parent.resolve())
new = Path(rel).as_posix() + frag
if new != raw:
action[NameObject("/URI")] = create_string_object(new)
changed = True
n_uri += 1
if changed:
n_pdf += 1
if dry:
print(f" pdf {pdf.relative_to(site)}")
else:
writer = PdfWriter(clone_from=reader)
tmp = pdf.with_suffix(".pdf.tmp")
with tmp.open("wb") as fh:
writer.write(fh)
tmp.replace(pdf)
print(f"pdf annotations {n_uri} in {n_pdf} files")
return n_pdf
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--site", type=Path, default=ROOT / "site", help="built catalog directory")
ap.add_argument("--from", dest="src", default="https://zapier.georgelambert.org", help="current docs origin")
g = ap.add_mutually_exclusive_group(required=True)
g.add_argument("--to", help="new docs origin, e.g. https://docs.verae-time.net")
g.add_argument("--relative", action="store_true", help="convert docs-origin URLs to path-relative links")
ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args()
site = args.site.resolve()
if not site.is_dir():
print(f"no site at {site}", file=sys.stderr)
return 1
src = _norm_origin(args.src)
dst = None if args.relative else _norm_origin(args.to)
print(f"{src}{dst or 'relative paths'} ({site})")
# HTML: root-relative `/…` so nested pages still hit the site root on any host.
html_dst = "/" if dst is None else dst
n_html = rewrite_html(site, src, html_dst, args.dry_run)
print(f"html/text files {n_html}")
rewrite_pdfs(site, src, dst, args.dry_run)
if args.dry_run:
print("dry-run; nothing written")
return 0
if __name__ == "__main__":
sys.exit(main())