Files
vault-cli/scripts/one-off/sort_unsorted.py
T

164 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""Sort /data/_unsorted (old pre-RJFI JD tree) into /data/R,J,F,I storage tree.
Dry-run by default; pass --execute to actually move files.
"""
import os, shutil, sys, csv
UNSORTED = "/data/_unsorted"
DATA = "/data"
# old_prefix -> new_prefix (folder-level rename/move rules)
# Order matters: longer/more specific prefixes first.
MAPPING = [
# --- WhatsApp exports: wholesale move, old->new folder rename ---
("1 Life admin/11 Me & other living things/11.54 Whatsapp exports",
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.54 Whatsapp exports"),
# --- 12 Home, house, and transport -> split F.11 (home) / F.12 (vehicles) ---
("1 Life admin/12 Home, house, and transport/12.12 Home insurance & claims",
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.12 Home insurance & claims"),
("1 Life admin/12 Home, house, and transport/12.21 Electricity, gas, & water",
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.21 Electricity, gas, & water"),
("1 Life admin/12 Home, house, and transport/12.23 All other utilities & services",
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.23 All other utilities & services"),
("1 Life admin/12 Home, house, and transport/12.24 Renovations & improvements",
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.24 Renovations & improvements"),
("1 Life admin/12 Home, house, and transport/12.25 House repairs, maintenance, & upkeep",
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.24 Renovations & improvements"),
("1 Life admin/12 Home, house, and transport/12.61 Move out of 6 Dorothy",
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.61 Move out of 6 Dorothy"),
("1 Life admin/12 Home, house, and transport/12.30 Vehicles",
"F Family/F.1 Home, household & finances/F.12 Vehicles/F.12.30 Vehicles"),
("1 Life admin/12 Home, house, and transport/12.32 Motor vehicle auto insurance & claims",
"F Family/F.1 Home, household & finances/F.12 Vehicles/F.12.32 Motor vehicle auto insurance & claims"),
("1 Life admin/12 Home, house, and transport/12.33 Car repairs, maintenance, & upkeep",
"F Family/F.1 Home, household & finances/F.12 Vehicles/F.12.33 Car repairs, maintenance, & upkeep"),
# --- 13 Money -> split R.13 (solo) / F.13 (joint); tax + financial insurance are joint per design doc ---
("1 Life admin/13 Money earned, spent, saved, & owed/13.54 Tax returns & accounting",
"F Family/F.1 Home, household & finances/F.13 Household finances/F.13.05 Tax returns & accounting"),
("1 Life admin/13 Money earned, spent, saved, & owed/13.12 Financial insurance & claims",
"F Family/F.1 Home, household & finances/F.13 Household finances/F.13.12 Financial insurance & claims"),
("1 Life admin/13 Money earned, spent, saved, & owed/13.44 Cryptocurrency",
"R Ryan/R.1 Life admin/R.13 Personal finance/R.13.44 Cryptocurrency"),
# --- 11 Me & other living things -> mostly R (Ryan personal) ---
("1 Life admin/11 Me & other living things/11.11 Birth certificate & proof of name",
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.11 Birth certificate & proof of name"),
("1 Life admin/11 Me & other living things/11.14 Licenses",
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.14 Licenses"),
("1 Life admin/11 Me & other living things/11.16 Legal documents & certificates",
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.16 Legal documents & certificates"),
("1 Life admin/11 Me & other living things/11.28 Reproductive health",
"R Ryan/R.1 Life admin/R.12 Health & body/R.12.28 Reproductive health"),
("1 Life admin/11 Me & other living things/11.83 My library",
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.83 Ryan's library"),
# --- 15 Travel -> R.15 Travel (solo trips; matches exactly) ---
("1 Life admin/15 Travel, events, & entertainment/15.41 All short trips",
"R Ryan/R.1 Life admin/R.15 Travel/R.15.41 All short trips"),
# --- 2 Career -> R.2 Career, ~1:1 ---
("2 Career/21 Perfect Day", "R Ryan/R.2 Career/R.21 Perfect Day"),
("2 Career/22 Sejour", "R Ryan/R.2 Career/R.22 Sejour"),
("2 Career/23 Consulting", "R Ryan/R.2 Career/R.23 Consulting"),
("2 Career/24 Stanford", "R Ryan/R.2 Career/R.24 Stanford"),
("2 Career/29 Old applications & archives", "R Ryan/R.2 Career/R.29 Old applications & archives"),
# --- 3 Hobbies -> R.3 Hobbies, ~1:1 ---
("3 Hobbies & interests/31 Language learning", "R Ryan/R.3 Hobbies & interests/R.31 Language learning"),
("3 Hobbies & interests/36 Projects", "R Ryan/R.3 Hobbies & interests/R.36 Projects"),
# --- 4 Flying -> R.4 Flying, ~1:1 ---
("4 Flying/41 Certification", "R Ryan/R.4 Flying/R.41 Certification, legal, & insurance"),
("4 Flying/43 Resources", "R Ryan/R.4 Flying/R.43 Resources"),
]
# Items needing human judgment - do NOT auto-move, just report.
FLAG_FOR_REVIEW = [
"1 Life admin/10 Management of area 10-19", # SMS backups - unclear whose/where
"1 Life admin/11 Me & other living things/11.43 My family", # Mom's book proposal
"1 Life admin/11 Me & other living things/11.09 Archive for category 11", # AI deck for Jen
"1 Life admin/11 Me & other living things/11.71 My career sales pitch",
"1 Life admin/13 Money earned, spent, saved, & owed/13.41 Bank accounts", # solo or joint?
"1 Life admin/13 Money earned, spent, saved, & owed/13.01 Inbox for category 13",
"1 Life admin/12 Home, house, and transport/12.01 Inbox for category 12",
"1 Life admin/14 My digital life/14.14 My data storage & backups",
"3 Hobbies & interests/30 Management for Hobbies",
]
def find_files(root):
for dirpath, _, filenames in os.walk(root):
for fn in filenames:
yield os.path.join(dirpath, fn)
def plan_moves():
moves = [] # (src, dst)
unmatched = []
for src in find_files(UNSORTED):
rel = os.path.relpath(src, UNSORTED)
matched = False
for old_prefix, new_prefix in MAPPING:
if rel == old_prefix or rel.startswith(old_prefix + os.sep):
suffix = rel[len(old_prefix):]
dst = os.path.join(DATA, new_prefix.replace("/", os.sep) + suffix)
moves.append((src, dst))
matched = True
break
if not matched:
is_flagged = any(rel.startswith(f) for f in FLAG_FOR_REVIEW)
unmatched.append((rel, is_flagged))
return moves, unmatched
def main():
execute = "--execute" in sys.argv
moves, unmatched = plan_moves()
print(f"=== PLANNED MOVES: {len(moves)} files ===")
by_target = {}
for src, dst in moves:
key = os.path.dirname(dst)
by_target[key] = by_target.get(key, 0) + 1
for k, v in sorted(by_target.items()):
print(f" {v:5d} -> {k}")
print(f"\n=== FLAGGED FOR REVIEW (not moved): {sum(1 for _, f in unmatched if f)} files ===")
flagged_dirs = {}
for rel, is_flagged in unmatched:
if is_flagged:
d = os.path.dirname(rel)
flagged_dirs[d] = flagged_dirs.get(d, 0) + 1
for k, v in sorted(flagged_dirs.items()):
print(f" {v:5d} :: {k}")
print(f"\n=== TRULY UNMATCHED (no mapping rule, not flagged): {sum(1 for _, f in unmatched if not f)} files ===")
other_dirs = {}
for rel, is_flagged in unmatched:
if not is_flagged:
d = os.path.dirname(rel)
other_dirs[d] = other_dirs.get(d, 0) + 1
for k, v in sorted(other_dirs.items()):
print(f" {v:5d} :: {k}")
if execute:
print("\n=== EXECUTING MOVES ===")
moved = 0
for src, dst in moves:
os.makedirs(os.path.dirname(dst), exist_ok=True)
if os.path.exists(dst):
print(f" SKIP (dst exists): {dst}")
continue
shutil.move(src, dst)
moved += 1
print(f"Moved {moved} files.")
else:
print("\n(dry run only -- pass --execute to actually move files)")
if __name__ == "__main__":
main()