49 lines
1.8 KiB
Python
49 lines
1.8 KiB
Python
"""Same-filesystem staging for plugin skill writes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import filecmp
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
from uuid import uuid4
|
|
|
|
|
|
class PluginSkillTransaction:
|
|
def __init__(self, workspace: str | Path) -> None:
|
|
self.workspace = Path(workspace)
|
|
self.transaction_id = uuid4().hex
|
|
self.root = self.workspace / ".beaver" / "staging" / "plugin-skills" / self.transaction_id
|
|
self.root.mkdir(parents=True, exist_ok=True)
|
|
|
|
def stage_upstream_snapshot(self, skill_name: str, source_id: str, tree_hash: str) -> Path:
|
|
path = self.root / "upstreams" / skill_name / source_id / tree_hash
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
def stage_skill_version(self, skill_name: str, version: str) -> Path:
|
|
path = self.root / "versions" / skill_name / version
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
def promote_directory(self, staged: Path, final: Path) -> None:
|
|
if final.exists():
|
|
if _directories_identical(staged, final):
|
|
return
|
|
raise ValueError(f"Immutable directory already exists with different content: {final}")
|
|
final.parent.mkdir(parents=True, exist_ok=True)
|
|
os.replace(staged, final)
|
|
|
|
def cleanup(self) -> None:
|
|
shutil.rmtree(self.root, ignore_errors=True)
|
|
|
|
|
|
def _directories_identical(left: Path, right: Path) -> bool:
|
|
comparison = filecmp.dircmp(left, right)
|
|
if comparison.left_only or comparison.right_only or comparison.funny_files:
|
|
return False
|
|
for filename in comparison.common_files:
|
|
if not filecmp.cmp(left / filename, right / filename, shallow=False):
|
|
return False
|
|
return all(_directories_identical(left / name, right / name) for name in comparison.common_dirs)
|