fd50b7deb9
Provide a portable, cache-ignored Obsidian vault lookup plugin and bind it to Super+N.
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())
|