"""Tiny JSON store (one file, atomic writes, one process-wide lock).""" from __future__ import annotations import json import os import contextlib import fcntl import threading LOCK = threading.RLock() class Store: def __init__(self, path: str): self.path = path os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) self.data = {"digests": {}, "submissions": [], "rate": {}, "cursor": 0, "seen_urls": []} if os.path.exists(path): with open(path, encoding="utf-8") as f: self.data.update(json.load(f)) def save(self) -> None: with LOCK: tmp = self.path + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(self.data, f, ensure_ascii=False, indent=1) os.chmod(tmp, 0o600) os.replace(tmp, self.path) def reload(self) -> None: with LOCK: if os.path.exists(self.path): with open(self.path, encoding="utf-8") as f: self.data.update(json.load(f)) @contextlib.contextmanager def tx(self): """Cross-process transaction: lock file, reload from disk, mutate self.data, save. The web server, mailbox worker and daily job are separate processes sharing one file.""" with LOCK: with open(self.path + ".lock", "a") as lk: fcntl.flock(lk, fcntl.LOCK_EX) try: self.reload() yield self.data self.save() finally: fcntl.flock(lk, fcntl.LOCK_UN)