from __future__ import annotations import json from pathlib import Path import threading from typing import Iterable from app.models import IpEntry class EntryStorage: def __init__(self, file_path: str): self._path = Path(file_path) self._lock = threading.Lock() def list_entries(self) -> list[IpEntry]: with self._lock: return self._read_entries_unlocked() def save_entries(self, entries: Iterable[IpEntry]) -> None: serializable = [entry.to_dict() for entry in entries] with self._lock: self._path.parent.mkdir(parents=True, exist_ok=True) tmp_path = self._path.with_suffix(self._path.suffix + ".tmp") tmp_path.write_text(json.dumps(serializable, indent=2, sort_keys=True), encoding="utf-8") tmp_path.replace(self._path) def _read_entries_unlocked(self) -> list[IpEntry]: if not self._path.exists(): return [] raw = self._path.read_text(encoding="utf-8").strip() if not raw: return [] parsed = json.loads(raw) if not isinstance(parsed, list): raise ValueError(f"State file must be a list: {self._path}") return [IpEntry.from_dict(item) for item in parsed]