-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvault-splitter.py
156 lines (130 loc) · 4.46 KB
/
vault-splitter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# Requires Python 3.12
from argparse import ArgumentParser
from pathlib import Path
import os
from typing import Dict, Optional, TypeVar
import re
import shutil
parser: ArgumentParser = ArgumentParser(
description=
"Vault-splitting utility for Obsidian. Allows you to select a root note and then recursively list, move, or copy all notes that it links to. Does not follow backlinks."
)
parser.add_argument(
"root-note",
help="Path to file that should be considered as root when building tree")
parser.add_argument('-cp',
'--copy',
metavar='PATH',
help="Copy isolated tree to given directory")
parser.add_argument('-mv',
'--move',
metavar='PATH',
help="Move isolated tree to given directory")
parser.add_argument(
'-ls',
'--list',
action='store_true',
help="List files without moving or copying them. Default behavior.")
parser.add_argument(
'--find-orphans',
action='store_true',
help="Invert behavior to affect all files that aren't in the tree")
def resolve_path(path: Optional[Path]) -> Optional[Path]:
"""Leaves path as-is if it's absolute, and makes it relative to the working directory if it isn't"""
if path is None:
return None
if not os.path.isabs(path):
path = Path(os.getcwd()).joinpath(path)
else:
path = Path(path)
if not path.exists():
raise ValueError(f"Path not found: {path.as_posix()}")
return path
T = TypeVar('T')
def unpack_optional(opt: Optional[T]) -> T:
if opt is None:
raise ValueError("Optional value is None")
return opt
args = vars(parser.parse_args())
root_note = unpack_optional(resolve_path(args["root-note"]))
copy_dir = resolve_path(args["copy"])
move_dir = resolve_path(args["move"])
list_mode: bool = args["list"]
orphan_mode: bool = args["find_orphans"]
if root_note.suffix != ".md":
raise ValueError("Root note is not an .md file!")
selected_modes = 0
if copy_dir:
selected_modes += 1
if move_dir:
selected_modes += 1
if list_mode:
selected_modes += 1
if selected_modes > 1:
raise ValueError("Can only select one of --copy, --move, --list")
if selected_modes == 0:
list_mode = True
vault_dir = root_note.parent
while True:
if not vault_dir.joinpath(".obsidian").exists():
if vault_dir == Path(vault_dir.anchor):
raise ValueError("Given root note is not part of vault!")
vault_dir = vault_dir.parent
if vault_dir is None:
raise ValueError("Given root note is not part of vault!")
else:
break
unprocessed_notes: Dict[str, Path] = {}
in_tree: Dict[str, Path] = {}
for note in vault_dir.glob("**/*"):
if re.match(f"^{vault_dir.joinpath('.obsidian').as_uri()}.*",
note.as_uri()) or re.match(
f"^{vault_dir.joinpath('.trash').as_uri()}.*",
note.as_uri()):
continue
if note.suffix == ".md":
unprocessed_notes[note.stem] = note
else:
unprocessed_notes[note.name] = note
def add_to_tree(note_name: str):
if note_name in unprocessed_notes.keys():
name = note_name
path = unprocessed_notes[name]
in_tree[name] = path
unprocessed_notes.pop(name)
if path.suffix == ".md":
with open(path, "r") as note_file:
matches = re.findall(
r"(?<!\\)\[(?<!\\)\[(.*?)(?:(?<!\\)\|.*?)?(?<!\\)\](?<!\\)\]",
note_file.read())
for match in matches:
add_to_tree(match)
add_to_tree(root_note.stem)
active_notes: Dict[str, Path] = {}
if not orphan_mode:
active_notes = in_tree
else:
active_notes = unprocessed_notes
if list_mode:
names = [
x.relative_to(vault_dir).as_posix() for x in active_notes.values()
]
names.sort(key=lambda x: x.lower())
for name in names:
print(name)
if len(names) == 0:
print("No notes match the given criteria.")
elif copy_dir or move_dir:
target_path = Path()
if copy_dir:
target_path = copy_dir
elif move_dir:
target_path = move_dir
for file in active_notes.values():
print(f"{file} -> {target_path}")
new_path = target_path.joinpath(file.relative_to(vault_dir))
os.makedirs(new_path.parent, exist_ok=True)
if copy_dir:
shutil.copy2(file, new_path)
elif move_dir:
shutil.move(file, new_path)