smith.infrastructure.filesystem
Atomic file-system adapter — write files transactionally or remove them.
1"""Atomic file-system adapter — write files transactionally or remove them.""" 2 3import shutil 4import tempfile 5from pathlib import Path 6 7from smith.domain.value_objects import FileSpec 8 9 10class FileSystemError(Exception): 11 """Raised when an atomic file operation fails.""" 12 13 14class AtomicFileSystem: 15 """File-system adapter that writes files atomically via a staging directory.""" 16 17 def __init__(self, project_dir: Path) -> None: 18 """Initialise with the project root directory.""" 19 self._project_dir = project_dir 20 21 def check_conflicts(self, paths: list[Path]) -> list[Path]: 22 """Return the subset of *paths* that already exist on disk.""" 23 return [p for p in paths if (self._project_dir / p).exists()] 24 25 def write_atomic(self, specs: list[FileSpec]) -> None: 26 """Write all specs atomically; roll back on failure.""" 27 if not specs: 28 return 29 staging = Path(tempfile.mkdtemp(dir=self._project_dir)) 30 try: 31 for spec in specs: 32 dest = staging / spec.relative_path 33 dest.parent.mkdir(parents=True, exist_ok=True) 34 dest.write_bytes(spec.content) 35 for spec in specs: 36 final = self._project_dir / spec.relative_path 37 final.parent.mkdir(parents=True, exist_ok=True) 38 Path(staging / spec.relative_path).replace(final) 39 except Exception as err: 40 shutil.rmtree(staging, ignore_errors=True) 41 raise FileSystemError("Atomic write failed") from err 42 else: 43 shutil.rmtree(staging, ignore_errors=True) 44 45 def remove(self, paths: list[Path]) -> None: 46 """Remove the given paths from the project directory.""" 47 for p in paths: 48 full = self._project_dir / p 49 if full.exists(): 50 full.unlink() 51 52 def exists(self, paths: list[Path]) -> dict[Path, bool]: 53 """Return a mapping of each path to whether it exists on disk.""" 54 return {p: (self._project_dir / p).exists() for p in paths}
class
FileSystemError(builtins.Exception):
Raised when an atomic file operation fails.
class
AtomicFileSystem:
15class AtomicFileSystem: 16 """File-system adapter that writes files atomically via a staging directory.""" 17 18 def __init__(self, project_dir: Path) -> None: 19 """Initialise with the project root directory.""" 20 self._project_dir = project_dir 21 22 def check_conflicts(self, paths: list[Path]) -> list[Path]: 23 """Return the subset of *paths* that already exist on disk.""" 24 return [p for p in paths if (self._project_dir / p).exists()] 25 26 def write_atomic(self, specs: list[FileSpec]) -> None: 27 """Write all specs atomically; roll back on failure.""" 28 if not specs: 29 return 30 staging = Path(tempfile.mkdtemp(dir=self._project_dir)) 31 try: 32 for spec in specs: 33 dest = staging / spec.relative_path 34 dest.parent.mkdir(parents=True, exist_ok=True) 35 dest.write_bytes(spec.content) 36 for spec in specs: 37 final = self._project_dir / spec.relative_path 38 final.parent.mkdir(parents=True, exist_ok=True) 39 Path(staging / spec.relative_path).replace(final) 40 except Exception as err: 41 shutil.rmtree(staging, ignore_errors=True) 42 raise FileSystemError("Atomic write failed") from err 43 else: 44 shutil.rmtree(staging, ignore_errors=True) 45 46 def remove(self, paths: list[Path]) -> None: 47 """Remove the given paths from the project directory.""" 48 for p in paths: 49 full = self._project_dir / p 50 if full.exists(): 51 full.unlink() 52 53 def exists(self, paths: list[Path]) -> dict[Path, bool]: 54 """Return a mapping of each path to whether it exists on disk.""" 55 return {p: (self._project_dir / p).exists() for p in paths}
File-system adapter that writes files atomically via a staging directory.
AtomicFileSystem(project_dir: pathlib._local.Path)
18 def __init__(self, project_dir: Path) -> None: 19 """Initialise with the project root directory.""" 20 self._project_dir = project_dir
Initialise with the project root directory.
def
check_conflicts(self, paths: list[pathlib._local.Path]) -> list[pathlib._local.Path]:
22 def check_conflicts(self, paths: list[Path]) -> list[Path]: 23 """Return the subset of *paths* that already exist on disk.""" 24 return [p for p in paths if (self._project_dir / p).exists()]
Return the subset of paths that already exist on disk.
26 def write_atomic(self, specs: list[FileSpec]) -> None: 27 """Write all specs atomically; roll back on failure.""" 28 if not specs: 29 return 30 staging = Path(tempfile.mkdtemp(dir=self._project_dir)) 31 try: 32 for spec in specs: 33 dest = staging / spec.relative_path 34 dest.parent.mkdir(parents=True, exist_ok=True) 35 dest.write_bytes(spec.content) 36 for spec in specs: 37 final = self._project_dir / spec.relative_path 38 final.parent.mkdir(parents=True, exist_ok=True) 39 Path(staging / spec.relative_path).replace(final) 40 except Exception as err: 41 shutil.rmtree(staging, ignore_errors=True) 42 raise FileSystemError("Atomic write failed") from err 43 else: 44 shutil.rmtree(staging, ignore_errors=True)
Write all specs atomically; roll back on failure.
def
remove(self, paths: list[pathlib._local.Path]) -> None:
46 def remove(self, paths: list[Path]) -> None: 47 """Remove the given paths from the project directory.""" 48 for p in paths: 49 full = self._project_dir / p 50 if full.exists(): 51 full.unlink()
Remove the given paths from the project directory.
def
exists( self, paths: list[pathlib._local.Path]) -> dict[pathlib._local.Path, bool]:
53 def exists(self, paths: list[Path]) -> dict[Path, bool]: 54 """Return a mapping of each path to whether it exists on disk.""" 55 return {p: (self._project_dir / p).exists() for p in paths}
Return a mapping of each path to whether it exists on disk.