ed063ab1fe
Capture desktop-specific system, session, Noctalia, and Pi integration alongside shared laptop support. Retire obsolete X11 configuration and make package ownership explicit for repeatable Decman deployments.
98 lines
3.0 KiB
Python
98 lines
3.0 KiB
Python
#!/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())
|