Files
Dockhand_Backup/app/store.py
2026-07-13 13:12:49 +02:00

93 lines
3.0 KiB
Python

"""JSON "DB" — the only persistence layer (no real database, by design).
All mutable, user-editable state lives in <DATA_DIR>/config.json. Writes are
atomic (temp file + os.replace) and guarded by a cross-process file lock so the
scheduler thread and the web UI never corrupt the file when writing together.
"""
import json
import os
import threading
from filelock import FileLock
from . import settings
_LOCK = threading.RLock()
_FILE_LOCK = FileLock(settings.CONFIG_PATH + ".lock")
DEFAULT_CONFIG = {
"schedule": "0 3 * * *", # cron: daily at 03:00 (in `timezone`)
"schedule_enabled": True,
"timezone": "", # IANA tz, e.g. "Europe/Berlin"; "" -> TZ env/UTC
"retention_n": 7, # keep last N .tar per stack
"auto_discover": True, # auto-add newly seen stacks before a run
"dockhand_stacks_base": "", # host dir where Dockhand stores compose/env
"stacks": [], # [{name, order, enabled, extra_paths:[]}]
"custom_paths": [], # [{label, path, enabled}]
"history": [], # [{stack, ts, status, file, size, error}]
}
MAX_HISTORY = 500
def _ensure_dir():
os.makedirs(settings.DATA_DIR, exist_ok=True)
def load() -> dict:
"""Load config, filling in any missing keys with defaults."""
with _LOCK:
_ensure_dir()
if not os.path.exists(settings.CONFIG_PATH):
save(DEFAULT_CONFIG)
return json.loads(json.dumps(DEFAULT_CONFIG))
with open(settings.CONFIG_PATH, "r", encoding="utf-8") as fh:
data = json.load(fh)
merged = json.loads(json.dumps(DEFAULT_CONFIG))
merged.update(data)
return merged
def save(config: dict) -> None:
"""Atomically persist the whole config."""
with _LOCK, _FILE_LOCK:
_ensure_dir()
tmp = settings.CONFIG_PATH + ".tmp"
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(config, fh, indent=2, ensure_ascii=False)
fh.flush()
os.fsync(fh.fileno())
os.replace(tmp, settings.CONFIG_PATH)
def update(mutator) -> dict:
"""Load, apply mutator(config) in place, save, return the new config.
Holds the lock across the whole read-modify-write so concurrent updates
from the UI and scheduler don't clobber each other.
"""
with _LOCK, _FILE_LOCK:
config = load()
mutator(config)
save(config)
return config
def stacks_by_order(config: dict):
"""Stacks sorted by their `order` field."""
return sorted(config.get("stacks", []), key=lambda s: s.get("order", 0))
def get_stack(config: dict, name: str):
for s in config.get("stacks", []):
if s["name"] == name:
return s
return None
def add_history(config: dict, entry: dict) -> None:
"""Prepend a run entry, trimming to MAX_HISTORY. Mutates config in place."""
hist = config.setdefault("history", [])
hist.insert(0, entry)
del hist[MAX_HISTORY:]