"""Configuration — JSON backed, atomic writes.""" import json, os, re, tempfile, threading from pathlib import Path DEFAULTS = { "LIBRARY_DIR": "/opt/serverup/stacks", "DATA_DIR": "/opt/serverup/appdata", "BACKUP_DIR": "/opt/serverup/backups", "APP_REPOS": [ {"id":"server-up","name":"server-up", "url":"https://github.com/bes-r/server-up.git","branch":"main","subdir":"apps"}, # ChristianLempa Boilerplates — automatisch herkend via template.json {"id":"boilerplates","name":"Boilerplates (ChristianLempa)", "url":"https://github.com/ChristianLempa/boilerplates-library.git", "branch":"main","subdir":"compose"}, ], "MODULE_REPOS": [ {"id":"server-up","name":"server-up", "url":"https://github.com/bes-r/server-up.git","branch":"main","subdir":"modules"}, ], "ACTIVE_MODULES": None, "MODULE_SETTINGS": {}, "LANGUAGE": "nl", "THEME": "dark", "WIZARD_DONE": False, } _path: Path = Path(os.environ.get("SU_CONFIG", "/data/config.json")) _lock = threading.RLock() def _ensure(): try: _path.parent.mkdir(parents=True, exist_ok=True) except Exception: pass def _fix_repo_url(url: str) -> str: """Fix GitHub URLs met pad erin: https://github.com/user/repo/modules → https://github.com/user/repo.git""" if not url: return url m = re.match(r'^(https?://[^/]+/[^/]+/[^/]+?)(?:/.*)?$', url.rstrip('/')) if m: base = m.group(1) if not base.endswith('.git'): base += '.git' return base return url def _sanitize_repos(repos: list) -> list: """Fix repo URLs in a repo list.""" for r in repos: if isinstance(r, dict) and r.get("url"): r["url"] = _fix_repo_url(r["url"]) return repos def load() -> dict: _ensure() cfg = dict(DEFAULTS) if _path.exists(): try: saved = json.loads(_path.read_text()) cfg.update(saved) except Exception: pass # Herstel standaard repos als opgeslagen waarde leeg is for key in ("APP_REPOS", "MODULE_REPOS"): if not cfg.get(key) and DEFAULTS.get(key): cfg[key] = list(DEFAULTS[key]) # Fix foute URLs for key in ("APP_REPOS", "MODULE_REPOS"): if cfg.get(key): _sanitize_repos(cfg[key]) # Bescherm tegen onveilige paden _unsafe = {"", "/", "/app", "/app/", "/etc", "/bin", "/usr", "/var", "/tmp", "/root"} for key in ("LIBRARY_DIR", "DATA_DIR", "BACKUP_DIR"): val = cfg.get(key, "").rstrip("/") if val in _unsafe or not val.startswith("/"): cfg[key] = DEFAULTS[key] # Auto-reset wizard als LIBRARY_DIR niet bestaat (verse installatie met oud volume) if cfg.get("WIZARD_DONE") and not Path(cfg["LIBRARY_DIR"]).exists(): cfg["WIZARD_DONE"] = False for k in DEFAULTS: if k in os.environ: cfg[k] = os.environ[k] # Herhaal veiligheidscheck na env-overrides for key in ("LIBRARY_DIR", "DATA_DIR", "BACKUP_DIR"): val = cfg.get(key, "").rstrip("/") if val in _unsafe or not val.startswith("/"): cfg[key] = DEFAULTS[key] # Injecteer SU_BOOT_REPOS in geheugen (worden niet automatisch opgeslagen) boot_raw = os.environ.get("SU_BOOT_REPOS", "").strip() if boot_raw: try: extra = json.loads(boot_raw) existing_ids = {r.get("id") for r in cfg.get("APP_REPOS", [])} for r in extra: if isinstance(r, dict) and r.get("id") and r["id"] not in existing_ids: cfg["APP_REPOS"] = list(cfg.get("APP_REPOS", [])) + [r] existing_ids.add(r["id"]) except Exception: pass return cfg def save(cfg: dict): _ensure() # Sanitize voor opslaan for key in ("APP_REPOS", "MODULE_REPOS"): if cfg.get(key): _sanitize_repos(cfg[key]) with _lock: fd, tmp = tempfile.mkstemp(dir=str(_path.parent), suffix=".json") try: with os.fdopen(fd, "w") as f: json.dump(cfg, f, indent=2) Path(tmp).replace(_path) except Exception: try: Path(tmp).unlink(missing_ok=True) except Exception: pass raise def patch(updates: dict) -> dict: with _lock: cfg = load() cfg.update(updates) save(cfg) return cfg