← Files ClaraARCHIVED FILE
scripts/case_store.py
8.47 KB · Oct 3, 2026 · 06:30 UTC
"""Serialized, recoverable writes for case-owned state.
A disk undo journal is committed before mutation, so process death can be
recovered on the next operation. The lock serializes cooperating local writers;
this does not claim transactions for arbitrary external files or manual edits.
"""
from __future__ import annotations
import json
import os
import shutil
import stat
import tempfile
import threading
from contextlib import contextmanager
from functools import wraps
from pathlib import Path
from typing import Any, BinaryIO, Callable, Iterator, TypeVar
__all__ = [
"atomic_bytes",
"atomic_stream",
"atomic_text",
"case_operation",
"case_reader",
"track_write",
"transaction",
]
_STATE = threading.local()
F = TypeVar("F", bound=Callable[..., Any])
def _sync_directory(path: Path) -> None:
if os.name != "nt":
fd = os.open(path, os.O_RDONLY)
try:
os.fsync(fd)
finally:
os.close(fd)
def _raw_write(path: Path, value: bytes) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if path.is_symlink():
raise ValueError(f"Case write target is a symbolic link: {path}")
temporary: Path | None = None
try:
with tempfile.NamedTemporaryFile(
dir=path.parent, prefix=f".{path.name}.", delete=False
) as handle:
temporary = Path(handle.name)
handle.write(value)
handle.flush()
os.fsync(handle.fileno())
temporary.replace(path)
_sync_directory(path.parent)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
def _safe_path(root: Path, relative: str) -> Path:
candidate = Path(relative)
if candidate.is_absolute() or ".." in candidate.parts:
raise ValueError("Unsafe case transaction path")
path = root / candidate
for part in (path, *path.parents):
if part == root:
break
if part.is_symlink():
raise ValueError("Case transaction path is a symbolic link")
if not path.resolve().is_relative_to(root):
raise ValueError("Case transaction path escapes case")
return path
def _raw_stream(path: Path, source: BinaryIO) -> None:
"""Replace one file from a bounded stream without buffering its full content."""
path.parent.mkdir(parents=True, exist_ok=True)
if path.is_symlink():
raise ValueError(f"Case write target is a symbolic link: {path}")
temporary: Path | None = None
try:
with tempfile.NamedTemporaryFile(
dir=path.parent, prefix=f".{path.name}.", delete=False
) as restored:
temporary = Path(restored.name)
shutil.copyfileobj(source, restored)
restored.flush()
os.fsync(restored.fileno())
temporary.replace(path)
_sync_directory(path.parent)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
def _restore_file(path: Path, source: Path) -> None:
"""Restore a preimage without enlisting recovery in another transaction."""
with source.open("rb") as original:
_raw_stream(path, original)
def atomic_stream(path: Path, source: BinaryIO) -> None:
"""Enlist and atomically write a stream, retaining no complete in-memory copy."""
track_write(path)
_raw_stream(path, source)
def _recover(root: Path, journal: Path) -> None:
if not journal.exists():
return
if journal.is_symlink():
raise ValueError("Case transaction journal is a symbolic link")
manifest = journal / "journal.json"
if manifest.is_symlink() or (journal / "committed").is_symlink():
raise ValueError("Case transaction marker is a symbolic link")
if manifest.exists() and not (journal / "committed").exists():
records = json.loads(manifest.read_text())
for relative, backup in records.items():
path = _safe_path(root, relative)
if backup is None:
path.unlink(missing_ok=True)
if path.parent.exists():
_sync_directory(path.parent)
else:
source = _safe_path(journal, backup)
_restore_file(path, source)
shutil.rmtree(journal)
_sync_directory(root)
def track_write(path: Path) -> None:
"""Persist an undo record before changing one case-owned file."""
active = getattr(_STATE, "active", {})
for root, (journal, records) in active.items():
absolute = path.absolute()
owner = next(
(parent for parent in absolute.parents if parent.resolve() == root),
None,
)
if owner is None:
if absolute.resolve().is_relative_to(root):
raise ValueError("Case write must use the case root or its root alias")
continue
# Preserve relative components so links *inside* the case still fail
# _safe_path; resolve only the root alias (for example macOS /tmp).
relative = absolute.relative_to(owner).as_posix()
target = _safe_path(root, relative)
if relative in records:
return
backup = None
if target.exists():
if not target.is_file():
raise ValueError("Case transaction target must be a file")
backup = f"{len(records)}.original"
# Stream large prior artifacts to disk rather than snapshot the tree in RAM.
with (
target.open("rb") as source,
(journal / backup).open("xb") as destination,
):
shutil.copyfileobj(source, destination)
destination.flush()
os.fsync(destination.fileno())
records[relative] = backup
_raw_write(journal / "journal.json", json.dumps(records).encode())
return
def atomic_bytes(path: Path, value: bytes) -> int:
"""Write exact bytes atomically and enlist case-owned files in the journal."""
track_write(path)
_raw_write(path, value)
return len(value)
def atomic_text(path: Path, value: str, *, encoding: str = "utf-8") -> int:
atomic_bytes(path, value.encode(encoding))
return len(value)
@contextmanager
def transaction(case_dir: Path) -> Iterator[None]:
"""Serialize writers and recover an interrupted operation before resuming."""
root = case_dir.resolve()
active = getattr(_STATE, "active", {})
if root in active:
yield
return
root.mkdir(parents=True, exist_ok=True)
lock_path = root / ".clara.lock"
fd = os.open(
lock_path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600
)
try:
info = os.fstat(fd)
if (
not stat.S_ISREG(info.st_mode)
or info.st_nlink != 1
or lock_path.is_symlink()
):
raise ValueError("Case lock must be an ordinary single-link file")
if os.name == "nt":
import msvcrt
if info.st_size == 0:
os.write(fd, b"0")
os.lseek(fd, 0, os.SEEK_SET)
msvcrt.locking(fd, msvcrt.LK_LOCK, 1)
else:
import fcntl
fcntl.flock(fd, fcntl.LOCK_EX)
journal = root / ".clara-transaction"
_recover(root, journal)
journal.mkdir(mode=0o700)
_sync_directory(root)
_STATE.active = {**active, root: (journal, {})}
complete = False
try:
yield
_raw_write(journal / "committed", b"committed\n")
complete = True
finally:
_STATE.active = active
# Recovery rolls back incomplete operations; committed journals only clean up.
if complete or journal.exists():
_recover(root, journal)
finally:
os.close(fd)
def case_operation(function: F) -> F:
"""Wrap a public operation whose first argument is its case directory."""
@wraps(function)
def wrapped(case_dir: Path, *args: Any, **kwargs: Any) -> Any:
with transaction(case_dir):
return function(case_dir.resolve(), *args, **kwargs)
return wrapped # type: ignore[return-value]
def case_reader(function: F) -> F:
"""Read one consistent state, recovering a crashed writer before inspection."""
@wraps(function)
def wrapped(case_dir: Path, *args: Any, **kwargs: Any) -> Any:
if not case_dir.is_dir():
return function(case_dir, *args, **kwargs)
with transaction(case_dir):
return function(case_dir.resolve(), *args, **kwargs)
return wrapped # type: ignore[return-value]
SHA-256: 46f7c0295caadc811e36a341404ff6f1d7bee5b803dc0c18281a9be24bb64dc1