#!/usr/bin/env python3 """Safely move or delete entries inside the knowledge vault.""" from __future__ import annotations import json import shutil import sys from pathlib import Path VAULT: Path def fail(message: str) -> int: print(message, file=sys.stderr) return 1 def vault_entry(relative: str) -> Path | None: if not relative or relative.startswith("/") or "\0" in relative: return None candidate = VAULT / relative parent = candidate.parent.resolve() if parent != VAULT and VAULT not in parent.parents: return None return parent / candidate.name def move_entry(source_relative: str, destination_relative: str) -> int: source = vault_entry(source_relative) destination_directory = (VAULT / destination_relative).resolve() if source is None or not (source.exists() or source.is_symlink()): return fail("selected entry no longer exists") if destination_directory != VAULT and VAULT not in destination_directory.parents: return fail("destination escaped the vault") if not destination_directory.is_dir(): return fail("destination directory no longer exists") if source.is_dir() and ( destination_directory == source or source in destination_directory.parents ): return fail("cannot move a directory into itself") destination = destination_directory / source.name if destination.exists() or destination.is_symlink(): return fail(f"already exists: {destination.name}") try: source.rename(destination) except OSError as error: return fail(f"cannot move {source.name}: {error}") json.dump( { "action": "move", "relative": destination.relative_to(VAULT).as_posix(), "kind": "directory" if destination.is_dir() else "note", }, sys.stdout, ) return 0 def delete_entry(source_relative: str) -> int: source = vault_entry(source_relative) if source is None or not (source.exists() or source.is_symlink()): return fail("selected entry no longer exists") try: if source.is_symlink(): source.unlink() elif source.is_dir(): shutil.rmtree(source) else: source.unlink() except OSError as error: return fail(f"cannot delete {source.name}: {error}") json.dump({"action": "delete", "relative": source_relative}, sys.stdout) return 0 def main() -> int: global VAULT if len(sys.argv) < 4: return fail( "usage: manage-entry.py VAULT ACTION SOURCE [DESTINATION_DIRECTORY]" ) VAULT = Path(sys.argv[1]).expanduser().resolve() if not VAULT.is_dir(): return fail(f"vault does not exist: {VAULT}") action, source = sys.argv[2:4] if action == "move" and len(sys.argv) == 5: return move_entry(source, sys.argv[4]) if action == "delete" and len(sys.argv) == 4: return delete_entry(source) return fail("invalid file operation") if __name__ == "__main__": raise SystemExit(main())