MCPcopy Create free account
hub / github.com/VectifyAI/OpenKB / _restore_hardlinked_dir

Function _restore_hardlinked_dir

openkb/mutation.py:260–305  ·  view source on GitHub ↗

O(touched) restore for a hardlinked directory backup. The backup was built with ``os.link``, so live files the mutation never touched still share the backup's inode — leave them. Only files the mutation changed need work: new files (no backup counterpart) are removed, modified files

(backup: Path, target: Path)

Source from the content-addressed store, hash-verified

258
259
260def _restore_hardlinked_dir(backup: Path, target: Path) -> None:
261 """O(touched) restore for a hardlinked directory backup.
262
263 The backup was built with ``os.link``, so live files the mutation never
264 touched still share the backup's inode — leave them. Only files the
265 mutation changed need work: new files (no backup counterpart) are removed,
266 modified files (atomic temp+replace → new inode) and deleted files are
267 restored from the backup's pre-mutation bytes. This avoids recopying the
268 whole tree on rollback — the cost that bit ``.openkb/files`` (the blob
269 store) and large concept/entity trees on every failed add.
270
271 Degrades gracefully to a full copy if the backup isn't actually hardlinked
272 (e.g. the EXDEV/EACCES fallback fired at snapshot time): every file then has
273 a different inode, so every file is treated as modified and recopied.
274 """
275
276 def _file_key(path: Path) -> tuple[int, int]:
277 st = path.stat() # follows symlinks; these trees hold regular files only
278 return (st.st_dev, st.st_ino)
279
280 backup_files = {p.relative_to(backup): p for p in backup.rglob("*") if p.is_file()}
281
282 # Pass 1: remove new + modified live regular files; leave untouched ones
283 # (they share the backup inode) in place.
284 if target.exists():
285 for live in list(target.rglob("*")):
286 if not live.is_file():
287 continue
288 counterpart = backup_files.get(live.relative_to(target))
289 if counterpart is None or _file_key(live) != _file_key(counterpart):
290 live.unlink()
291
292 # Pass 2: restore modified + deleted files from backup.
293 for rel, src in backup_files.items():
294 dest = target / rel
295 if not dest.exists() or _file_key(dest) != _file_key(src):
296 dest.parent.mkdir(parents=True, exist_ok=True)
297 shutil.copy2(src, dest)
298
299 # Pass 3: prune directories the mutation created that are now empty.
300 if target.exists():
301 for d in sorted(
302 (p for p in target.rglob("*") if p.is_dir()), key=lambda p: len(p.parts), reverse=True
303 ):
304 if not (backup / d.relative_to(target)).exists() and not any(d.iterdir()):
305 d.rmdir()
306
307
308def snapshot_paths(

Callers 1

rollbackMethod · 0.85

Calls 2

_file_keyFunction · 0.85
getMethod · 0.45

Tested by

no test coverage detected