From 31c5688df96e365b27f373819e41a86cebe54ec9 Mon Sep 17 00:00:00 2001 From: Patrick Singletary Date: Thu, 20 Aug 2026 07:52:54 -0400 Subject: [PATCH] backup3: canonical X10 targets (X10//), canonical_ok guard, mac.lan hostname; PLAN-BACKUP3-X10-CANONICAL-MAP decisions 1a-5a,6c --- bin/backup3.zsh | 53 ++++++++++++++------ config/backup3/README.md | 8 ++- config/backup3/machines/mac-studio.conf.stub | 37 +++++++++----- config/backup3/machines/ole-blu.conf.stub | 24 +++++++++ docs/plans/PLAN-BACKUP3-X10-CANONICAL-MAP.md | 18 +++++-- 5 files changed, 107 insertions(+), 33 deletions(-) create mode 100644 config/backup3/machines/ole-blu.conf.stub diff --git a/bin/backup3.zsh b/bin/backup3.zsh index f5743aa..293854b 100755 --- a/bin/backup3.zsh +++ b/bin/backup3.zsh @@ -2,6 +2,9 @@ # backup3.zsh — three-location backup: ole-blu (local) -> OneDrive (active cloud) + Crucial X10 (archive). # Canonical copy: ~/dev/_shared/bin/backup3.zsh (Tangled-synced). # Full design: ~/dev/_shared/docs/BACKUP3-PLAN.md +# X10 targets follow the post-reorg canonical layout (PLAN-BACKUP3-X10-CANONICAL-MAP): +# X10//; guarded by canonical_ok (needs _ledger + a canonical +# top-level) so the script no-ops on a legacy-layout or mid-reorg drive. # # Modes: # backup3.zsh dry-run: report what WOULD move (default, no changes) @@ -41,7 +44,7 @@ done HOST="$(hostname | tr '[:upper:]' '[:lower:]')" case "$HOST" in ole-blu*|ole*blu*) MACHINE=ole-blu ;; - mac-studio*|mac*studio*) MACHINE=mac-studio ;; + mac-studio*|mac*studio*|mac.lan*) MACHINE=mac-studio ;; *) MACHINE="$HOST" ;; esac @@ -74,6 +77,19 @@ x10_mounted() { [[ "$uuid" == "$X10_UUID" ]] } +# canonical_ok: the X10 is attached, but only act on it once it is in the +# post-reorg canonical layout (PLAN-BACKUP3-X10-CANONICAL-MAP). _ledger is the +# machine-managed metadata root; at least one canonical top-level must exist. +# Backstop for attaching the drive mid-reorg or before GATE-4 freeze (DP-5a). +canonical_ok() { + [[ -d "$X10_ROOT/_ledger" ]] || return 1 + local top + for top in Downloads Media Pictures Documents Desktop Music; do + [[ -d "$X10_ROOT/$top" ]] && return 0 + done + return 1 +} + # move_entry category src dst — src is a FILE or a DIR; dst is the target dir for it move_entry() { local cat="$1" src="$2" dst="$3" @@ -152,9 +168,9 @@ run_categories() { local entry for entry in "$src"/*(N); do if [[ "$always" == "yes" ]]; then - move_entry "$cat" "$entry" "$xd/$cat/$MACHINE" + move_entry "$cat" "$entry" "$xd/$MACHINE" else - size_split "$cat" "$entry" "$od/$cat/$MACHINE" "$xd/$cat/$MACHINE" + size_split "$cat" "$entry" "$od/$cat/$MACHINE" "$xd/$MACHINE" fi done done @@ -165,7 +181,7 @@ INSTALLER_EXTS=(dmg pkg exe msi iso zip tar gz tgz bz2 xz 7z) AUDIO_EXTS=(mp3 m4a wav flac aac ogg opus) route_downloads() { - local src="$HOME/Downloads" od="$ONEDRIVE_ROOT" xd="$ARCHIVE_ROOT" + local src="$HOME/Downloads" od="$ONEDRIVE_ROOT" xd="$X10_ROOT" [[ -d "$src" ]] || return 0 log "== category: Downloads (bespoke routing)" local entry base ext @@ -173,21 +189,21 @@ route_downloads() { base="${entry:t}" if [[ -d "$entry" ]]; then case "$base" in - stacher) move_entry "Downloads-stacher" "$entry" "$xd/stacher/$MACHINE" ;; - Media) move_entry "Downloads-media" "$entry" "$xd/media/$MACHINE" ;; + stacher) move_entry "Downloads-stacher" "$entry" "$xd/Media/Music-Videos/$MACHINE" ;; + Media) move_entry "Downloads-media" "$entry" "$xd/Media/Other-Video/$MACHINE" ;; Music) move_entry "Downloads-music" "$entry" "$od/Music/$MACHINE" ;; - *) size_split "Downloads" "$entry" "$od/Downloads/$MACHINE" "$xd/downloads/$MACHINE" ;; + *) size_split "Downloads" "$entry" "$od/Downloads/$MACHINE" "$xd/Downloads/$MACHINE" ;; esac else ext="${base:e:l}" if (( INSTALLER_EXTS[(Ie)$ext] )); then - move_entry "Downloads-installers" "$entry" "$xd/installers/$MACHINE" + move_entry "Downloads-installers" "$entry" "$xd/Archive/installers/$MACHINE" elif (( AUDIO_EXTS[(Ie)$ext] )); then move_entry "Downloads-music" "$entry" "$od/Music/$MACHINE" elif [[ "$ext" == "pdf" ]]; then move_entry "Downloads-docs" "$entry" "$od/Documents/$MACHINE" else - size_split "Downloads" "$entry" "$od/Downloads/$MACHINE" "$xd/downloads/$MACHINE" + size_split "Downloads" "$entry" "$od/Downloads/$MACHINE" "$xd/Downloads/$MACHINE" fi fi done @@ -216,11 +232,11 @@ cleanup_pass() { else log "$verb $f" && echo "$verb $f" >> "$out" fi - done < <(find "$X10_ROOT" \( -path "$X10_ROOT/archive" -o -path "$X10_ROOT/.backup3-trash" \) -prune -o -type f \( "${findargs[@]}" \) -print 2>/dev/null) + done < <(find "$X10_ROOT" \( -path "$X10_ROOT/_ledger" -o -path "$X10_ROOT/_CONFLICTS" -o -path "$X10_ROOT/.backup3-trash" -o -path "$X10_ROOT/photos-restore.photoslibrary" \) -prune -o -type f \( "${findargs[@]}" \) -print 2>/dev/null) # 2) duplicate candidates: same basename + same size (report-only v1; ._ sidecars excluded — they are cruft) log "== duplicate candidates (same basename+size; report-only in v1) ==" - find "$X10_ROOT" \( -path "$X10_ROOT/archive" -o -path "$X10_ROOT/.backup3-trash" \) -prune \ + find "$X10_ROOT" \( -path "$X10_ROOT/_ledger" -o -path "$X10_ROOT/_CONFLICTS" -o -path "$X10_ROOT/.backup3-trash" -o -path "$X10_ROOT/photos-restore.photoslibrary" \) -prune \ -o -type f ! -name '._*' ! -name '.DS_Store' ! -name 'desktop.ini' -print0 2>/dev/null \ | xargs -0 stat -f '%N|%z' 2>/dev/null \ | awk -F'|' '{ n=split($1,a,"/"); b=a[n]; k=b "|" $2; c[k]++; lines[k]=lines[k] sprintf(" %s\n", $1) } @@ -235,6 +251,7 @@ status() { echo "machine: $MACHINE (host=$HOST)" echo "config: $CONFIG_DIR" echo "X10 mounted: $(x10_mounted && echo yes || echo no) ($X10_ROOT, uuid $X10_UUID)" + echo "X10 canonical: $(canonical_ok && echo yes || echo no) (post-reorg layout: _ledger + canonical top-levels)" echo "OneDrive root: $ONEDRIVE_ROOT $([[ -d "$ONEDRIVE_ROOT" ]] && echo ok || echo MISSING)" echo "size threshold: ${SIZE_THRESHOLD_MB}MB checksum: always ledger-hash-max: ${LEDGER_HASH_MAX_MB}MB" echo "categories:" @@ -256,6 +273,10 @@ if ! x10_mounted; then log "$(date +%F_%T) X10 not mounted at $X10_ROOT — nothing to do (launchd-safe exit 0)" exit 0 fi +if ! canonical_ok; then + log "$(date +%F_%T) X10 attached but NOT in canonical post-reorg layout (no _ledger/canonical top-level) — nothing to do (DP-5a guard)" + exit 0 +fi log "$(date +%F_%T) === backup3 start ($MACHINE, apply=$APPLY, cleanup=$CLEANUP) ===" [[ -d "$ONEDRIVE_ROOT" ]] || log "WARN: OneDrive root missing — OneDrive targets skipped" @@ -265,12 +286,12 @@ route_downloads (( CLEANUP )) && cleanup_pass log "=== done. run log: $RUNLOG ===" -# mirror ledger + run log to X10 archive metadata. +# mirror ledger + run log to X10 _ledger metadata. # Per-machine filenames: both ole-blu and mac-studio share the same X10, so a # shared ledger.csv would clobber (last writer wins). Run logs are timestamped # but scope them per-machine too to stay unambiguous. -if (( APPLY )) && [[ -d "$ARCHIVE_ROOT" ]]; then - mkdir -p "$ARCHIVE_ROOT/_ledger" - cp "$LEDGER" "$ARCHIVE_ROOT/_ledger/ledger-$MACHINE.csv" 2>/dev/null || true - cp "$RUNLOG" "$ARCHIVE_ROOT/_ledger/run-$MACHINE-$(basename "$RUNLOG")" 2>/dev/null || true +if (( APPLY )) && [[ -d "$X10_ROOT" ]]; then + mkdir -p "$X10_ROOT/_ledger" + cp "$LEDGER" "$X10_ROOT/_ledger/ledger-$MACHINE.csv" 2>/dev/null || true + cp "$RUNLOG" "$X10_ROOT/_ledger/run-$MACHINE-$(basename "$RUNLOG")" 2>/dev/null || true fi diff --git a/config/backup3/README.md b/config/backup3/README.md index 6bf345b..54bbe08 100644 --- a/config/backup3/README.md +++ b/config/backup3/README.md @@ -5,7 +5,13 @@ were moved out to the local dir `~/.config/backup3/` — sourced by `bin/backup3.zsh` and `bin/backup-drive.zsh` via `${XDG_CONFIG_HOME:-$HOME/.config}/backup3`. -What stays here: the sanitized `machines/mac-studio.conf.stub` template only. +What stays here: the sanitized `machines/mac-studio.conf.stub` + +`machines/ole-blu.conf.stub` templates only. + +X10 targets follow the post-reorg canonical layout (see +`docs/plans/PLAN-BACKUP3-X10-CANONICAL-MAP.md`): `X10//`, +ledger mirrors to `X10/_ledger/`. Config knob: `CANON_ROOT` in backup3.conf +(default `$X10_ROOT`). Deploying to a new machine (order matters — BEFORE resetting the repo clone after a force-push): diff --git a/config/backup3/machines/mac-studio.conf.stub b/config/backup3/machines/mac-studio.conf.stub index c233afd..08cae7e 100644 --- a/config/backup3/machines/mac-studio.conf.stub +++ b/config/backup3/machines/mac-studio.conf.stub @@ -1,13 +1,24 @@ -# machine: mac-studio — STUB (not wired up yet). -# Wire up after ole-Blu proves out the pipeline (see docs/BACKUP3-PLAN.md, Phase 5). -# Steps: rename to mac-studio.conf, set MACHINE_ID, fill CATEGORIES with the -# Studio's actual user-data dirs, install launchd plist on the Studio. -# -# MACHINE_ID="mac-studio" -# CATEGORIES=( -# "Music|$HOME/Music|$ONEDRIVE_ROOT|$ARCHIVE_ROOT|no" -# "Pictures|$HOME/Pictures|$ONEDRIVE_ROOT|$ARCHIVE_ROOT|no" -# "Documents|$HOME/Documents|$ONEDRIVE_ROOT|$ARCHIVE_ROOT|no" -# "Desktop|$HOME/Desktop|$ONEDRIVE_ROOT|$ARCHIVE_ROOT|no" -# "Movies|$HOME/Movies|$ONEDRIVE_ROOT|$ARCHIVE_ROOT|yes" -# ) +# machine: mac-studio (Mac Studio 2022) — hostname "mac.lan" (was Mac-Studio-2022.localdomain) +# SANITIZED STUB — fill in real values locally at ~/.config/backup3/machines/. +# NEVER commit real UUIDs or machine-specific policies (repo is PUBLIC). +MACHINE_ID="mac-studio" + +# Category rules: name|source|onedrive_base|x10_base|always_x10 +# - always_x10=yes : whole category goes to X10 regardless of size +# - always_x10=no : per-entry size split (>= SIZE_THRESHOLD_MB -> x10, else onedrive) +# x10_base is the EXACT canonical parent (post-reorg layout); the script appends +# the per-machine subdir: //... (CANON_ROOT defined in backup3.conf) +CATEGORIES=( + "Music|$HOME/Music|$ONEDRIVE_ROOT|$CANON_ROOT/Music|no" + "Pictures|$HOME/Pictures|$ONEDRIVE_ROOT|$CANON_ROOT/Pictures|no" + "Documents|$HOME/Documents|$ONEDRIVE_ROOT|$CANON_ROOT/Documents|no" + "Desktop|$HOME/Desktop|$ONEDRIVE_ROOT|$CANON_ROOT/Desktop|no" + "Movies|$HOME/Movies|$ONEDRIVE_ROOT|$CANON_ROOT/Media/Movies|yes" +) +# ~/Downloads has bespoke routing in backup3.zsh (route_downloads), canonical targets: +# stacher/ -> X10/Media/Music-Videos/ (stacher = music videos per reorg taxonomy) +# Media/ -> X10/Media/Other-Video/ +# Music/ -> OneDrive/Music/ +# *.dmg/pkg/exe/msi/iso/zip/tar/gz/tgz/bz2/xz/7z -> X10/Archive/installers/ +# *.pdf -> OneDrive/Documents/ +# everything else -> size split: OneDrive/Downloads/ vs X10/Downloads/ diff --git a/config/backup3/machines/ole-blu.conf.stub b/config/backup3/machines/ole-blu.conf.stub new file mode 100644 index 0000000..63b3f7d --- /dev/null +++ b/config/backup3/machines/ole-blu.conf.stub @@ -0,0 +1,24 @@ +# machine: ole-blu (MacBook Air) — hostname "ole-Blu" +# SANITIZED STUB — fill in real values locally at ~/.config/backup3/machines/. +# NEVER commit real UUIDs or machine-specific policies (repo is PUBLIC). +MACHINE_ID="ole-blu" + +# Category rules: name|source|onedrive_base|x10_base|always_x10 +# - always_x10=yes : whole category goes to X10 regardless of size +# - always_x10=no : per-entry size split (>= SIZE_THRESHOLD_MB -> x10, else onedrive) +# x10_base is the EXACT canonical parent (post-reorg layout); the script appends +# the per-machine subdir: //... (CANON_ROOT defined in backup3.conf) +CATEGORIES=( + "Music|$HOME/Music|$ONEDRIVE_ROOT|$CANON_ROOT/Music|no" + "Pictures|$HOME/Pictures|$ONEDRIVE_ROOT|$CANON_ROOT/Pictures|no" + "Documents|$HOME/Documents|$ONEDRIVE_ROOT|$CANON_ROOT/Documents|no" + "Desktop|$HOME/Desktop|$ONEDRIVE_ROOT|$CANON_ROOT/Desktop|no" + "Movies|$HOME/Movies|$ONEDRIVE_ROOT|$CANON_ROOT/Media/Movies|yes" +) +# ~/Downloads has bespoke routing in backup3.zsh (route_downloads), canonical targets: +# stacher/ -> X10/Media/Music-Videos/ (stacher = music videos per reorg taxonomy) +# Media/ -> X10/Media/Other-Video/ +# Music/ -> OneDrive/Music/ +# *.dmg/pkg/exe/msi/iso/zip/tar/gz/tgz/bz2/xz/7z -> X10/Archive/installers/ +# *.pdf -> OneDrive/Documents/ +# everything else -> size split: OneDrive/Downloads/ vs X10/Downloads/ diff --git a/docs/plans/PLAN-BACKUP3-X10-CANONICAL-MAP.md b/docs/plans/PLAN-BACKUP3-X10-CANONICAL-MAP.md index d332c1e..180114e 100644 --- a/docs/plans/PLAN-BACKUP3-X10-CANONICAL-MAP.md +++ b/docs/plans/PLAN-BACKUP3-X10-CANONICAL-MAP.md @@ -1,6 +1,9 @@ # PLAN-BACKUP3-X10-CANONICAL-MAP — Update ole-blu backup3 to the X10 canonical structure Status: DRAFT for operator review (no execution yet). 2026-08-20. +DECISIONS LOCKED (2026-08-20): DP-1a 2a 3a 4a 5a 6c — see Section 6. 6c = ONE-PASS +activation: implement now in _shared; activate on ole-blu AND mac-studio together +after the reorg passes GATE-4 (freeze). Canonical copy: `~/dev/_shared/docs/plans/` (Tangled-synced; repo is PUBLIC — paths only, no secrets). Related: `docs/BACKUP3-PLAN.md`, studio `~/msd512-audit/PLAN-3B-X10-REORG.md` + `PLAN-3B-EXECUTION.md` (STEP 5 = this task), `backup-move-scripts` skill. @@ -103,9 +106,18 @@ UUID mount check. X10/Media/Other-Video/, rest -> Movies; (c) split by extension into Clips/Other-Video. - DP-5 Structure assertion: (a) require _ledger + one canonical top-level [recommended]; (b) require _ledger only; (c) no assertion (trust UUID check). -- DP-6 Activation: (a) implement now, activate after reorg GATE-4 freeze [recommended]; - (b) implement + activate immediately (assertion is the backstop); (c) hold until the - Studio script is updated too, do both in one pass. +- DP-6 Activation: ONE-PASS (locked 6c) — implement now (code + shared configs live in + `_shared`); do NOT activate either machine until BOTH are updated and the reorg + passed GATE-4. Activation checklist: Air live configs copied; Studio live configs + created (`~/.config/backup3/` first install) + launchd re-enabled + (`launchctl load ~/Library/LaunchAgents/com.psingletary.backup3.plist`); both + machines' `_shared` pulled to the new script. (a) implement now + activate after + GATE-4 on Air only [rejected]; (b) implement + activate immediately [rejected]; + (c) ONE-PASS both machines [LOCKED]. +- DP-7 Studio hostname: studio `hostname` is now `mac.lan` (was mac-studio-2022). + backup3.zsh MACHINE detection must match it or the studio runs dry-run-only + (no machine conf). Locked: extend the hostname case `mac.lan* -> mac-studio` + (script change, included in P1). ## 7. Phases (gated, same posture as the reorg) -- 2.51.2