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())