134 lines
4.4 KiB
Python
134 lines
4.4 KiB
Python
"""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
|