"""Crash-safe JSON persistence shared across all data modules. - load_json: returns `default` if the file is missing, empty, or corrupt (e.g. a partial/interrupted write). A corrupt file is moved aside to `.corrupt` so it can be inspected rather than silently overwritten. - save_json: atomic write — serialize to a temp file in the same directory, fsync, then os.replace() (atomic on POSIX). An interrupted write can never leave a truncated or empty target file. This is the durability backbone for the app's JSON storage (issues #11/#12). """ import json import os import tempfile DATA_DIR = os.environ.get("DATA_DIR", os.path.join(os.path.dirname(__file__), "data")) def load_json(path, default): if not os.path.exists(path): return default try: with open(path) as f: return json.load(f) except (json.JSONDecodeError, ValueError): # Corrupt/partial file — preserve it for inspection, fall back to default # instead of crashing the request. try: os.replace(path, path + ".corrupt") except OSError: pass return default def save_json(path, data): directory = os.path.dirname(path) or "." os.makedirs(directory, exist_ok=True) fd, tmp = tempfile.mkstemp(dir=directory, prefix=".tmp-", suffix=".json") try: with os.fdopen(fd, "w") as f: json.dump(data, f, indent=2) f.flush() os.fsync(f.fileno()) os.replace(tmp, path) except BaseException: try: os.remove(tmp) except OSError: pass raise