#!/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()