runv-server

server tooling for runv.club
Log | Files | Refs | README

gen_config_toml.py (4616B)


      1 #!/usr/bin/env python3
      2 """
      3 Gera ``config.toml`` a partir de ``config.example.toml`` (sem editar o example no git).
      4 
      5 Uso típico no servidor após ``git pull`` (evita conflitos se ``config.toml`` não for versionado):
      6 
      7   sudo python3 /opt/runv/src/terminal/gen_config_toml.py --install-root /opt/runv/terminal
      8 
      9 No clone local (ficheiro em ``terminal/config.toml``, ignorado pelo git):
     10 
     11   python3 terminal/gen_config_toml.py
     12 
     13 O ``setup_entre.py`` chama a mesma função ao instalar o módulo.
     14 """
     15 
     16 from __future__ import annotations
     17 
     18 import argparse
     19 import re
     20 import shutil
     21 import sys
     22 from pathlib import Path
     23 from typing import Final, Literal
     24 
     25 SCRIPT_DIR: Final[Path] = Path(__file__).resolve().parent
     26 ADMIN_DIR: Final[Path] = SCRIPT_DIR.parent / "scripts" / "admin"
     27 if str(ADMIN_DIR) not in sys.path:
     28     sys.path.insert(0, str(ADMIN_DIR))
     29 
     30 from admin_guard import ensure_admin_cli
     31 
     32 ADMIN_EMAIL_LINE_RE: Final[re.Pattern[str]] = re.compile(
     33     r'^(?P<prefix>\s*admin_email\s*=\s*")(?P<value>.*?)(?P<suffix>"\s*)$',
     34     re.MULTILINE,
     35 )
     36 
     37 
     38 def preserve_admin_email(*, existing: Path, generated: Path) -> None:
     39     """Mantém admin_email do config.toml existente ao regenerar a partir do example."""
     40     if not existing.is_file() or not generated.is_file():
     41         return
     42     old_text = existing.read_text(encoding="utf-8")
     43     new_text = generated.read_text(encoding="utf-8")
     44     old_match = ADMIN_EMAIL_LINE_RE.search(old_text)
     45     new_match = ADMIN_EMAIL_LINE_RE.search(new_text)
     46     if old_match is None or new_match is None:
     47         return
     48     old_value = old_match.group("value")
     49     preserved_line = (
     50         f'{new_match.group("prefix")}{old_value}{new_match.group("suffix")}'
     51     )
     52     updated = ADMIN_EMAIL_LINE_RE.sub(preserved_line, new_text, count=1)
     53     if updated != new_text:
     54         generated.write_text(updated, encoding="utf-8")
     55 
     56 
     57 def write_terminal_config_toml(
     58     *,
     59     example: Path,
     60     out: Path,
     61     force: bool,
     62     dry_run: bool,
     63 ) -> Literal["wrote", "skipped", "dry_run"]:
     64     """
     65     Copia ``example`` para ``out`` se ``out`` não existir ou ``force``.
     66 
     67     Returns:
     68         ``wrote``, ``skipped`` (já existia e não force), ou ``dry_run``.
     69     """
     70     if not example.is_file():
     71         raise FileNotFoundError(f"modelo em falta: {example}")
     72     if dry_run:
     73         return "dry_run"
     74     if out.is_file() and not force:
     75         return "skipped"
     76     previous = out.read_text(encoding="utf-8") if out.is_file() else None
     77     out.parent.mkdir(parents=True, exist_ok=True)
     78     shutil.copy2(example, out)
     79     if previous is not None:
     80         tmp_previous = out.with_suffix(out.suffix + ".previous")
     81         tmp_previous.write_text(previous, encoding="utf-8")
     82         try:
     83             preserve_admin_email(existing=tmp_previous, generated=out)
     84         finally:
     85             try:
     86                 tmp_previous.unlink()
     87             except OSError:
     88                 pass
     89     try:
     90         out.chmod(0o640)
     91     except OSError:
     92         pass
     93     return "wrote"
     94 
     95 
     96 def main() -> int:
     97     parser = argparse.ArgumentParser(
     98         description="Gera config.toml do módulo entre a partir de config.example.toml.",
     99     )
    100     parser.add_argument(
    101         "--install-root",
    102         type=Path,
    103         default=SCRIPT_DIR,
    104         help="directório do módulo (default: pasta deste script)",
    105     )
    106     parser.add_argument(
    107         "--example",
    108         type=Path,
    109         default=None,
    110         help="caminho explícito do config.example.toml (default: <install-root>/config.example.toml)",
    111     )
    112     parser.add_argument(
    113         "--force",
    114         action="store_true",
    115         help="sobrescrever config.toml existente",
    116     )
    117     parser.add_argument("--dry-run", action="store_true")
    118     args = parser.parse_args()
    119     ensure_admin_cli(
    120         script_name=Path(__file__).name,
    121         dry_run=bool(args.dry_run),
    122     )
    123 
    124     root = args.install_root.resolve()
    125     example = args.example.resolve() if args.example else root / "config.example.toml"
    126     out = root / "config.toml"
    127 
    128     try:
    129         result = write_terminal_config_toml(
    130             example=example,
    131             out=out,
    132             force=bool(args.force),
    133             dry_run=bool(args.dry_run),
    134         )
    135     except FileNotFoundError as e:
    136         print(e, file=sys.stderr)
    137         return 1
    138 
    139     if result == "dry_run":
    140         print(f"[dry-run] escreveria {out} a partir de {example}")
    141         return 0
    142     if result == "skipped":
    143         print(f"Mantido {out} (use --force para substituir pelo example).")
    144         return 0
    145     print(f"Escrito {out} a partir de {example}.")
    146     return 0
    147 
    148 
    149 if __name__ == "__main__":
    150     raise SystemExit(main())