"""Per-instance sync state. Tracks the snapshot id this pack was last synced to and when. Lives at ``/.cloud-sync/state.json`` (mode 600). Purpose: divergence detection. On ``pull``, if the remote latest id differs from ``last_pulled_snapshot_id`` AND any in-scope local file has mtime > ``last_pulled_at``, the local and remote diverged from a common ancestor — surface the conflict dialog. """ from __future__ import annotations import json from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path SCHEMA_VERSION = 2 @dataclass(frozen=True) class State: instance_id: str last_pulled_snapshot_id: str last_pulled_at: datetime host_tag: str = "instance-sync" def state_path(pack_folder: Path) -> Path: return pack_folder / ".cloud-sync" / "state.json" def read(pack_folder: Path) -> State | None: """Return parsed state or None if file missing / unreadable / wrong schema.""" p = state_path(pack_folder) if not p.exists(): return None try: data = json.loads(p.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return None if data.get("schema") != SCHEMA_VERSION: return None try: return State( instance_id=data["instance_id"], last_pulled_snapshot_id=data["last_pulled_snapshot_id"], last_pulled_at=_parse_iso(data["last_pulled_at"]), host_tag=data.get("host_tag", "instance-sync"), ) except (KeyError, ValueError): return None def write(pack_folder: Path, state: State) -> None: """Persist state. Creates parent dir + sets mode 600.""" p = state_path(pack_folder) p.parent.mkdir(parents=True, exist_ok=True) payload = { "schema": SCHEMA_VERSION, "instance_id": state.instance_id, "last_pulled_snapshot_id": state.last_pulled_snapshot_id, "last_pulled_at": state.last_pulled_at.astimezone(timezone.utc) .isoformat() .replace("+00:00", "Z"), "host_tag": state.host_tag, } p.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") p.chmod(0o600) def clear(pack_folder: Path) -> None: """Remove state.json if present. Used when remote has zero snapshots.""" p = state_path(pack_folder) p.unlink(missing_ok=True) def _parse_iso(s: str) -> datetime: """Parse ISO-8601 with trailing Z or +HH:MM, return tz-aware UTC.""" if s.endswith("Z"): s = s[:-1] + "+00:00" dt = datetime.fromisoformat(s) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(timezone.utc)