""" Storage configuration loader. Reads from /etc/fester/storage.json by default, or from FESTER_STORAGE_CONFIG env var. Falls back to sensible defaults if the file doesn't exist or isn't readable. """ import json import os from pathlib import Path DEFAULT_CONFIG_PATH = Path( os.environ.get("FESTER_STORAGE_CONFIG", "/etc/fester/storage.json") ) DEFAULT_CONFIG = { "tmpfs": True, "btrfs": False, "qcow2_freeze": False, "btrfs_path": "/var/lib/fester/cas", "tmpfs_path": "/dev/shm/fester-build", "qcow2_path": "/var/lib/fester/snapshots", } def _writable_path(p: Path) -> bool: """Check if a path is writable (or can be created).""" try: p.mkdir(parents=True, exist_ok=True) # touch a test file test = p / ".fester_write_test" test.write_text("") test.unlink() return True except (OSError, PermissionError): return False def _resolve_paths(cfg: dict) -> dict: """If a configured path isn't writable, fall back to user/tmp paths.""" fallbacks = { "btrfs_path": [ Path.home() / ".fester" / "cas", Path("/tmp/fester/cas"), ], "tmpfs_path": [ Path.home() / ".fester" / "tmpfs", Path("/tmp/fester/tmpfs"), ], "qcow2_path": [ Path.home() / ".fester" / "snapshots", Path("/tmp/fester/snapshots"), ], } for key, alts in fallbacks.items(): p = Path(cfg[key]) if not _writable_path(p): for alt in alts: if _writable_path(alt): cfg[key] = str(alt) break return cfg def load_storage_config() -> dict: """Load storage config with graceful fallbacks.""" cfg = DEFAULT_CONFIG.copy() if DEFAULT_CONFIG_PATH.exists(): try: with open(DEFAULT_CONFIG_PATH) as f: user_cfg = json.load(f) cfg.update(user_cfg) except (json.JSONDecodeError, OSError): pass cfg = _resolve_paths(cfg) return cfg def save_storage_config(cfg: dict) -> bool: """Persist config to disk. Returns True on success.""" try: DEFAULT_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) with open(DEFAULT_CONFIG_PATH, "w") as f: json.dump(cfg, f, indent=2) return True except (OSError, PermissionError): return False