From d7573a3cbdd402f8b042249e6ea3c83c8ec1ae73 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 24 Feb 2026 19:55:00 -0700 Subject: [PATCH] Add Aurora backup drive + Syncthing setup --- aurora/.stignore | 65 ++++++ aurora/99-backup-portable.rules | 2 + aurora/backup-portable.service | 18 ++ aurora/backup-to-portable | 199 ++++++++++++++++++ aurora/restore-from-portable | 352 ++++++++++++++++++++++++++++++++ aurora/setup.sh | 205 +++++++++++++++++++ 6 files changed, 841 insertions(+) create mode 100644 aurora/.stignore create mode 100644 aurora/99-backup-portable.rules create mode 100644 aurora/backup-portable.service create mode 100755 aurora/backup-to-portable create mode 100755 aurora/restore-from-portable create mode 100755 aurora/setup.sh diff --git a/aurora/.stignore b/aurora/.stignore new file mode 100644 index 0000000..7982c46 --- /dev/null +++ b/aurora/.stignore @@ -0,0 +1,65 @@ +// ============================================= +// Syncthing excludes for home directory sync +// Everything NOT listed here will be synced +// ============================================= + +// --- Caches (all regeneratable) --- +.cache +.bun +.npm +.nvm +go +.local/share/Trash + +// --- Package managers / build artifacts --- +**/node_modules +**/__pycache__ +**/.venv +**/target +**/.tox +**/.mypy_cache +**/.pytest_cache +**/dist +**/build +**/.next +**/.turbo + +// --- AI tool session logs (large, not portable) --- +.local/share/opencode +.local/share/claude +.opencode +.claude + +// --- Containers (rebuild per-machine) --- +.local/share/containers + +// --- Flatpak app data (reinstall regenerates) --- +.var/app/com.valvesoftware.Steam +.var/app/com.microsoft.Edge +.var/app/com.google.Chrome +.var/app/com.stremio.Stremio +.var/app/org.mozilla.firefox + +// --- Machine-specific / KDE desktop state --- +.local/share/baloo +.local/share/kactivitymanagerd +.local/share/klipper +.local/share/recently-used.xbel +auto-cpufreq + +// --- SQLite DBs (live sync corrupts) --- +.open-webui + +// --- Transient --- +Downloads + +// --- Syncthing own data --- +.config/syncthing +.local/state/syncthing + +// --- Common junk patterns --- +*.swp +*.swo +*~ +.DS_Store +Thumbs.db diff --git a/aurora/99-backup-portable.rules b/aurora/99-backup-portable.rules new file mode 100644 index 0000000..b3722b7 --- /dev/null +++ b/aurora/99-backup-portable.rules @@ -0,0 +1,2 @@ +# Trigger backup when Seagate Portable drive is connected +ACTION=="add", SUBSYSTEM=="block", ENV{ID_SERIAL_SHORT}=="NT3D9HDX", ENV{DEVTYPE}=="partition", TAG+="systemd", ENV{SYSTEMD_WANTS}="backup-portable.service" diff --git a/aurora/backup-portable.service b/aurora/backup-portable.service new file mode 100644 index 0000000..e42c375 --- /dev/null +++ b/aurora/backup-portable.service @@ -0,0 +1,18 @@ +[Unit] +Description=Backup home to Portable drive (BTRFS send/receive) +# Give the drive a moment to fully settle after plug-in +After=local-fs.target + +[Service] +Type=oneshot +ExecStartPre=/bin/sleep 5 +ExecStart=/usr/local/bin/backup-to-portable +TimeoutStartSec=7200 +Nice=10 +IOSchedulingClass=idle +IOSchedulingPriority=7 +# Don't kill backup if user logs out +KillMode=process + +[Install] +WantedBy=multi-user.target diff --git a/aurora/backup-to-portable b/aurora/backup-to-portable new file mode 100755 index 0000000..e7d8f7a --- /dev/null +++ b/aurora/backup-to-portable @@ -0,0 +1,199 @@ +#!/bin/bash +# ============================================================================= +# Backup to Portable Drive +# BTRFS snapshot + send/receive with LUKS encryption +# For Aurora (Universal Blue) on BTRFS +# ============================================================================= +set -euo pipefail + +# --- Configuration --- +LUKS_DEVICE="/dev/disk/by-id/usb-Seagate_Portable_NT3D9HDX-0:0-part1" +LUKS_KEYFILE="/etc/backup-drive.key" +LUKS_NAME="backup-drive" +BACKUP_MOUNT="/mnt/backup-drive" +BTRFS_DEVICE="/dev/nvme0n1p3" +BTRFS_TOP="/mnt/btrfs-root" +SNAP_DIR="snapshots" +KEEP_LOCAL=2 # local snapshots (need at least 1 for incremental parent) +KEEP_REMOTE=10 # remote snapshots on backup drive +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +LOGFILE="/var/log/backup-portable.log" +LOCK="/run/backup-portable.lock" + +# --- Helpers --- +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOGFILE"; } + +notify() { + local urgency="${3:-normal}" + local uid bus + uid=$(id -u joey 2>/dev/null) || return 0 + bus="unix:path=/run/user/${uid}/bus" + sudo -u joey DBUS_SESSION_BUS_ADDRESS="$bus" \ + notify-send -u "$urgency" -i drive-removable-media "$1" "$2" 2>/dev/null || true +} + +die() { log "ERROR: $*"; notify "Backup Failed" "$*" critical; exit 1; } + +cleanup() { + log "Cleaning up mounts..." + mountpoint -q "$BACKUP_MOUNT" 2>/dev/null && umount "$BACKUP_MOUNT" || true + mountpoint -q "$BTRFS_TOP" 2>/dev/null && umount "$BTRFS_TOP" || true + [ -e "/dev/mapper/$LUKS_NAME" ] && cryptsetup luksClose "$LUKS_NAME" 2>/dev/null || true + rm -f "$LOCK" +} +trap cleanup EXIT + +# --- Preflight --- +[ "$(id -u)" -eq 0 ] || die "Must run as root" + +# Single-instance lock +if [ -f "$LOCK" ]; then + pid=$(cat "$LOCK" 2>/dev/null || true) + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + die "Backup already running (PID $pid)" + fi + log "Removing stale lock" + rm -f "$LOCK" +fi +echo $$ > "$LOCK" + +# Check drive is present +[ -e "$LUKS_DEVICE" ] || die "Backup drive not connected" + +log "=========================================" +log "Starting backup: $TIMESTAMP" +log "=========================================" +notify "Backup Started — Do NOT unplug drive" "Backing up home to Portable drive..." critical + +# --- Open LUKS --- +if [ ! -e "/dev/mapper/$LUKS_NAME" ]; then + log "Opening LUKS volume..." + cryptsetup luksOpen --key-file "$LUKS_KEYFILE" "$LUKS_DEVICE" "$LUKS_NAME" \ + || die "Failed to open LUKS volume" +fi + +# --- Mount filesystems --- +mkdir -p "$BACKUP_MOUNT" "$BTRFS_TOP" + +if ! mountpoint -q "$BACKUP_MOUNT"; then + mount /dev/mapper/"$LUKS_NAME" "$BACKUP_MOUNT" \ + || die "Failed to mount backup drive" +fi + +if ! mountpoint -q "$BTRFS_TOP"; then + mount -o subvolid=5 "$BTRFS_DEVICE" "$BTRFS_TOP" \ + || die "Failed to mount source BTRFS top-level" +fi + +mkdir -p "$BTRFS_TOP/$SNAP_DIR" + +# --- Create read-only snapshot of home --- +SNAP_NAME="home-$TIMESTAMP" +log "Creating snapshot: $SNAP_NAME" +btrfs subvolume snapshot -r "$BTRFS_TOP/home" "$BTRFS_TOP/$SNAP_DIR/$SNAP_NAME" \ + || die "Failed to create snapshot" + +# --- Send snapshot to backup drive --- +# Find parent for incremental send: latest snapshot that exists on BOTH local and remote +PARENT_FLAG="" +LATEST_REMOTE=$(ls -1d "$BACKUP_MOUNT/backups"/home-* 2>/dev/null | sort | tail -1 || true) +if [ -n "$LATEST_REMOTE" ]; then + PARENT_NAME=$(basename "$LATEST_REMOTE") + if [ -d "$BTRFS_TOP/$SNAP_DIR/$PARENT_NAME" ]; then + PARENT_FLAG="-p $BTRFS_TOP/$SNAP_DIR/$PARENT_NAME" + log "Incremental send (parent: $PARENT_NAME)" + else + log "Parent snapshot not found locally, falling back to full send" + fi +else + log "No remote snapshots found, performing full send" +fi + +log "Sending snapshot to backup drive..." +# shellcheck disable=SC2086 +btrfs send $PARENT_FLAG "$BTRFS_TOP/$SNAP_DIR/$SNAP_NAME" \ + | btrfs receive "$BACKUP_MOUNT/backups/" \ + || die "btrfs send/receive failed" + +log "Snapshot sent successfully" + +# --- Save metadata --- +log "Saving metadata..." +META_DIR="$BACKUP_MOUNT/metadata/$TIMESTAMP" +mkdir -p "$META_DIR" + +# Flatpak apps +flatpak list --app --columns=application > "$META_DIR/flatpak-apps.txt" 2>/dev/null || true + +# rpm-ostree status +rpm-ostree status > "$META_DIR/rpm-ostree-status.txt" 2>/dev/null || true + +# Layered packages +rpm-ostree status --json 2>/dev/null | python3 -c " +import sys, json +try: + d = json.load(sys.stdin) + pkgs = d.get('deployments', [{}])[0].get('requested-packages', []) + print('\n'.join(pkgs)) +except: pass +" > "$META_DIR/layered-packages.txt" 2>/dev/null || true + +# /etc (small but important on ostree systems) +tar czf "$META_DIR/etc-backup.tar.gz" -C / etc/ 2>/dev/null || true + +# Hostname + date for identification +echo "$(hostname) - $(date)" > "$META_DIR/info.txt" + +# --- Cleanup old local snapshots --- +# Keep KEEP_LOCAL most recent, but ALWAYS keep the one matching latest remote +log "Cleaning up old local snapshots..." +mapfile -t LOCAL_SNAPS < <(ls -1d "$BTRFS_TOP/$SNAP_DIR"/home-* 2>/dev/null | sort) +LOCAL_COUNT=${#LOCAL_SNAPS[@]} + +if [ "$LOCAL_COUNT" -gt "$KEEP_LOCAL" ]; then + # The latest local snap is the one we just created, keep it + # Also keep the most recent remote's parent if different + for ((i=0; i < LOCAL_COUNT - KEEP_LOCAL; i++)); do + OLD_SNAP="${LOCAL_SNAPS[$i]}" + OLD_NAME=$(basename "$OLD_SNAP") + # Don't delete if it's still needed as a remote parent reference + if [ -d "$BACKUP_MOUNT/backups/$OLD_NAME" ]; then + log "Keeping local snapshot $OLD_NAME (exists on remote)" + continue + fi + log "Deleting old local snapshot: $OLD_NAME" + btrfs subvolume delete "$OLD_SNAP" 2>/dev/null || true + done +fi + +# --- Cleanup old remote snapshots --- +log "Cleaning up old remote snapshots..." +mapfile -t REMOTE_SNAPS < <(ls -1d "$BACKUP_MOUNT/backups"/home-* 2>/dev/null | sort) +REMOTE_COUNT=${#REMOTE_SNAPS[@]} + +if [ "$REMOTE_COUNT" -gt "$KEEP_REMOTE" ]; then + for ((i=0; i < REMOTE_COUNT - KEEP_REMOTE; i++)); do + OLD_REMOTE="${REMOTE_SNAPS[$i]}" + log "Deleting old remote snapshot: $(basename "$OLD_REMOTE")" + btrfs subvolume delete "$OLD_REMOTE" 2>/dev/null || true + done +fi + +# Cleanup old metadata dirs +mapfile -t META_DIRS < <(ls -1d "$BACKUP_MOUNT/metadata"/20* 2>/dev/null | sort) +META_COUNT=${#META_DIRS[@]} +if [ "$META_COUNT" -gt "$KEEP_REMOTE" ]; then + for ((i=0; i < META_COUNT - KEEP_REMOTE; i++)); do + rm -rf "${META_DIRS[$i]}" + done +fi + +# --- Final sync --- +log "Syncing drive..." +sync +USAGE=$(df -h /dev/mapper/"$LUKS_NAME" | tail -1 | awk '{print $3 " used / " $2 " total (" $5 ")"}') +DURATION=$(( $(date +%s) - $(date -d "${TIMESTAMP:0:8} ${TIMESTAMP:9:2}:${TIMESTAMP:11:2}:${TIMESTAMP:13:2}" +%s) )) +DURATION_MIN=$(( DURATION / 60 )) +log "Backup complete: $SNAP_NAME (${DURATION_MIN}m)" +log "Drive usage: $USAGE" +notify "Backup Complete — Safe to unplug" "Snapshot: $SNAP_NAME\nDrive: $USAGE\nDuration: ${DURATION_MIN} min" critical diff --git a/aurora/restore-from-portable b/aurora/restore-from-portable new file mode 100755 index 0000000..574bbde --- /dev/null +++ b/aurora/restore-from-portable @@ -0,0 +1,352 @@ +#!/bin/bash +# ============================================================================= +# Restore from Portable Backup Drive +# Browse snapshots, mount for file recovery, or full home restore +# ============================================================================= +set -euo pipefail + +# --- Configuration --- +LUKS_DEVICE="/dev/disk/by-id/usb-Seagate_Portable_NT3D9HDX-0:0-part1" +LUKS_KEYFILE="/etc/backup-drive.key" +LUKS_NAME="backup-drive" +BACKUP_MOUNT="/mnt/backup-drive" +BTRFS_DEVICE="/dev/nvme0n1p3" +BTRFS_TOP="/mnt/btrfs-root" +BROWSE_MOUNT="/mnt/backup-browse" + +# --- Helpers --- +bold() { echo -e "\033[1m$*\033[0m"; } +green() { echo -e "\033[32m$*\033[0m"; } +red() { echo -e "\033[31m$*\033[0m"; } +yellow(){ echo -e "\033[33m$*\033[0m"; } + +die() { red "ERROR: $*"; exit 1; } + +cleanup() { + mountpoint -q "$BROWSE_MOUNT" 2>/dev/null && umount "$BROWSE_MOUNT" 2>/dev/null || true + # Don't auto-close LUKS/unmount backup in restore mode — user may still need it +} +trap cleanup EXIT + +# --- Preflight --- +[ "$(id -u)" -eq 0 ] || die "Must run as root (sudo)" +[ -e "$LUKS_DEVICE" ] || die "Backup drive not connected" + +# --- Open LUKS + Mount --- +if [ ! -e "/dev/mapper/$LUKS_NAME" ]; then + echo "Opening LUKS volume..." + if [ -f "$LUKS_KEYFILE" ]; then + cryptsetup luksOpen --key-file "$LUKS_KEYFILE" "$LUKS_DEVICE" "$LUKS_NAME" + else + echo "Keyfile not found. Enter passphrase:" + cryptsetup luksOpen "$LUKS_DEVICE" "$LUKS_NAME" + fi +fi + +mkdir -p "$BACKUP_MOUNT" +if ! mountpoint -q "$BACKUP_MOUNT"; then + mount /dev/mapper/"$LUKS_NAME" "$BACKUP_MOUNT" +fi + +# --- List snapshots --- +list_snapshots() { + bold "\nAvailable backup snapshots:" + echo "─────────────────────────────────────────" + local i=1 + mapfile -t SNAPSHOTS < <(ls -1d "$BACKUP_MOUNT/backups"/home-* 2>/dev/null | sort -r) + + if [ ${#SNAPSHOTS[@]} -eq 0 ]; then + die "No snapshots found on backup drive" + fi + + for snap in "${SNAPSHOTS[@]}"; do + local name date_str + name=$(basename "$snap") + # Parse timestamp from name: home-YYYYMMDD-HHMMSS + date_str=$(echo "$name" | sed 's/home-//' | sed 's/\([0-9]\{8\}\)-\([0-9]\{2\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)/\1 \2:\3:\4/') + printf " %2d) %s (%s)\n" "$i" "$name" "$date_str" + ((i++)) + done + echo "" +} + +# --- Show metadata for a snapshot --- +show_metadata() { + local snap_name="$1" + local ts="${snap_name#home-}" + local meta_dir="$BACKUP_MOUNT/metadata/$ts" + + if [ -d "$meta_dir" ]; then + bold "\nMetadata for $snap_name:" + echo "─────────────────────────────────────────" + [ -f "$meta_dir/info.txt" ] && echo " Host: $(cat "$meta_dir/info.txt")" + [ -f "$meta_dir/flatpak-apps.txt" ] && echo " Flatpaks: $(wc -l < "$meta_dir/flatpak-apps.txt") apps" + [ -f "$meta_dir/layered-packages.txt" ] && echo " Layered pkgs: $(cat "$meta_dir/layered-packages.txt" | grep -c . || echo 0)" + [ -f "$meta_dir/etc-backup.tar.gz" ] && echo " /etc backup: $(du -h "$meta_dir/etc-backup.tar.gz" | cut -f1)" + echo "" + else + yellow " (no metadata for this snapshot)" + fi +} + +# --- Browse a snapshot (mount read-only) --- +browse_snapshot() { + local snap_path="$1" + local snap_name + snap_name=$(basename "$snap_path") + + mkdir -p "$BROWSE_MOUNT" + if mountpoint -q "$BROWSE_MOUNT"; then + umount "$BROWSE_MOUNT" + fi + + # Bind-mount the snapshot for easy browsing + mount --bind "$snap_path" "$BROWSE_MOUNT" + + green "\nSnapshot mounted read-only at: $BROWSE_MOUNT" + echo "" + echo "You can now browse and copy files:" + echo " ls $BROWSE_MOUNT/" + echo " cp $BROWSE_MOUNT/joey/Documents/file.txt ~/Documents/" + echo "" + yellow "When done, run: sudo umount $BROWSE_MOUNT" + echo "Or press Enter here to unmount and return to menu." + read -r + umount "$BROWSE_MOUNT" 2>/dev/null || true +} + +# --- Restore specific files/directories --- +restore_files() { + local snap_path="$1" + local snap_name + snap_name=$(basename "$snap_path") + + echo "" + bold "Restore files from $snap_name" + echo "Enter path relative to home (e.g., joey/Documents, joey/.config/Code):" + read -r rel_path + + [ -z "$rel_path" ] && { yellow "No path entered."; return; } + + local src="$snap_path/$rel_path" + if [ ! -e "$src" ]; then + red "Path not found in snapshot: $rel_path" + return + fi + + local dest_base="/var/home" + local dest="$dest_base/$rel_path" + + echo "" + echo "Source: $src" + echo "Destination: $dest" + echo "" + + if [ -e "$dest" ]; then + yellow "WARNING: Destination exists and will be overwritten." + echo -n "Create backup of current version first? [Y/n] " + read -r yn + if [ "$yn" != "n" ] && [ "$yn" != "N" ]; then + local backup="${dest}.pre-restore-$(date +%Y%m%d-%H%M%S)" + cp -a "$dest" "$backup" + green "Current version backed up to: $backup" + fi + fi + + echo -n "Proceed with restore? [y/N] " + read -r confirm + if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then + if [ -d "$src" ]; then + mkdir -p "$(dirname "$dest")" + rsync -a --delete "$src/" "$dest/" + else + mkdir -p "$(dirname "$dest")" + cp -a "$src" "$dest" + fi + # Restore ownership to joey + chown -R joey:joey "$dest" + green "Restored: $rel_path" + else + yellow "Cancelled." + fi +} + +# --- Full home restore via btrfs send/receive --- +full_restore() { + local snap_path="$1" + local snap_name + snap_name=$(basename "$snap_path") + + echo "" + red "╔══════════════════════════════════════════════════════╗" + red "║ FULL HOME RESTORE — THIS IS DESTRUCTIVE ║" + red "║ Your current /var/home will be replaced entirely. ║" + red "║ A snapshot of the current state will be saved. ║" + red "╚══════════════════════════════════════════════════════╝" + echo "" + echo "Restoring from: $snap_name" + echo "" + echo -n "Type 'RESTORE' to confirm: " + read -r confirm + [ "$confirm" = "RESTORE" ] || { yellow "Cancelled."; return; } + + # Mount top-level BTRFS + mkdir -p "$BTRFS_TOP" + if ! mountpoint -q "$BTRFS_TOP"; then + mount -o subvolid=5 "$BTRFS_DEVICE" "$BTRFS_TOP" + fi + + # Snapshot current home as safety net + local safety_name="home-pre-restore-$(date +%Y%m%d-%H%M%S)" + echo "Creating safety snapshot of current home: $safety_name" + btrfs subvolume snapshot -r "$BTRFS_TOP/home" "$BTRFS_TOP/snapshots/$safety_name" + green "Safety snapshot created: $safety_name" + + # Strategy: receive the backup snapshot, then swap subvolumes + echo "Receiving snapshot from backup drive..." + local restore_name="home-restoring-$(date +%Y%m%d-%H%M%S)" + + btrfs send "$snap_path" | btrfs receive "$BTRFS_TOP/snapshots/" \ + || die "btrfs receive failed" + + # The received snapshot is read-only. Create a writable snapshot from it. + echo "Creating writable home from snapshot..." + + # Rename current home subvolume out of the way + mv "$BTRFS_TOP/home" "$BTRFS_TOP/home-old-$(date +%Y%m%d-%H%M%S)" + + # Create writable snapshot as new home + btrfs subvolume snapshot "$BTRFS_TOP/snapshots/$snap_name" "$BTRFS_TOP/home" + + green "" + green "═══════════════════════════════════════════" + green " Full restore complete!" + green " Old home saved as: home-old-*" + green " REBOOT to use restored home." + green "═══════════════════════════════════════════" + echo "" + yellow "After reboot, once verified, clean up old home with:" + echo " sudo mount -o subvolid=5 /dev/nvme0n1p3 /mnt/btrfs-root" + echo " sudo btrfs subvolume delete /mnt/btrfs-root/home-old-*" + echo "" + + umount "$BTRFS_TOP" 2>/dev/null || true +} + +# --- Restore system metadata --- +restore_metadata() { + local snap_name="$1" + local ts="${snap_name#home-}" + local meta_dir="$BACKUP_MOUNT/metadata/$ts" + + [ -d "$meta_dir" ] || { red "No metadata found for $snap_name"; return; } + + bold "\nRestore system metadata from $snap_name" + echo "─────────────────────────────────────────" + echo " 1) Reinstall flatpak apps" + echo " 2) Reinstall layered rpm-ostree packages" + echo " 3) Restore /etc from backup" + echo " 4) Show all metadata (view only)" + echo " b) Back" + echo "" + echo -n "Choice: " + read -r choice + + case "$choice" in + 1) + if [ -f "$meta_dir/flatpak-apps.txt" ]; then + echo "Installing flatpak apps..." + while IFS= read -r app; do + [ -z "$app" ] && continue + echo " Installing: $app" + flatpak install -y flathub "$app" 2>/dev/null || yellow " Failed: $app" + done < "$meta_dir/flatpak-apps.txt" + green "Done." + fi + ;; + 2) + if [ -f "$meta_dir/layered-packages.txt" ] && [ -s "$meta_dir/layered-packages.txt" ]; then + echo "Layered packages to install:" + cat "$meta_dir/layered-packages.txt" + echo "" + echo -n "Install all? [y/N] " + read -r yn + if [ "$yn" = "y" ] || [ "$yn" = "Y" ]; then + # shellcheck disable=SC2046 + rpm-ostree install $(cat "$meta_dir/layered-packages.txt" | tr '\n' ' ') + yellow "Reboot required to apply layered packages." + fi + else + yellow "No layered packages recorded." + fi + ;; + 3) + if [ -f "$meta_dir/etc-backup.tar.gz" ]; then + red "WARNING: This will overwrite files in /etc" + echo -n "Proceed? [y/N] " + read -r yn + if [ "$yn" = "y" ] || [ "$yn" = "Y" ]; then + tar xzf "$meta_dir/etc-backup.tar.gz" -C / + green "/etc restored." + fi + fi + ;; + 4) + echo "" + [ -f "$meta_dir/info.txt" ] && { bold "System info:"; cat "$meta_dir/info.txt"; echo ""; } + [ -f "$meta_dir/flatpak-apps.txt" ] && { bold "Flatpak apps:"; cat "$meta_dir/flatpak-apps.txt"; echo ""; } + [ -f "$meta_dir/layered-packages.txt" ] && { bold "Layered packages:"; cat "$meta_dir/layered-packages.txt"; echo ""; } + [ -f "$meta_dir/rpm-ostree-status.txt" ] && { bold "rpm-ostree status:"; cat "$meta_dir/rpm-ostree-status.txt"; echo ""; } + ;; + b|B) return ;; + esac +} + +# ═══════════════════════════════════════════ +# Main Menu +# ═══════════════════════════════════════════ +while true; do + list_snapshots + + bold "Actions:" + echo " 1) Browse snapshot (mount read-only, copy files manually)" + echo " 2) Restore specific files/directories" + echo " 3) Full home restore (replace entire /var/home)" + echo " 4) Restore system metadata (flatpaks, packages, /etc)" + echo " q) Quit" + echo "" + echo -n "Choice: " + read -r action + + case "$action" in + q|Q) + echo "Closing backup drive..." + umount "$BACKUP_MOUNT" 2>/dev/null || true + cryptsetup luksClose "$LUKS_NAME" 2>/dev/null || true + green "Done." + exit 0 + ;; + 1|2|3|4) + echo -n "Snapshot number: " + read -r num + if [[ ! "$num" =~ ^[0-9]+$ ]] || [ "$num" -lt 1 ] || [ "$num" -gt "${#SNAPSHOTS[@]}" ]; then + red "Invalid selection." + continue + fi + SELECTED="${SNAPSHOTS[$((num-1))]}" + SELECTED_NAME=$(basename "$SELECTED") + + show_metadata "$SELECTED_NAME" + + case "$action" in + 1) browse_snapshot "$SELECTED" ;; + 2) restore_files "$SELECTED" ;; + 3) full_restore "$SELECTED" ;; + 4) restore_metadata "$SELECTED_NAME" ;; + esac + ;; + *) + red "Invalid choice." + ;; + esac +done diff --git a/aurora/setup.sh b/aurora/setup.sh new file mode 100755 index 0000000..438ea41 --- /dev/null +++ b/aurora/setup.sh @@ -0,0 +1,205 @@ +#!/bin/bash +# ============================================================================= +# Aurora Machine Setup — Backup Drive + Syncthing +# Run after a fresh Aurora install or home restore +# Usage: sudo bash setup.sh +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +bold() { echo -e "\033[1m$*\033[0m"; } +green() { echo -e "\033[32m$*\033[0m"; } +yellow(){ echo -e "\033[33m$*\033[0m"; } +red() { echo -e "\033[31m$*\033[0m"; } + +die() { red "ERROR: $*"; exit 1; } + +[ "$(id -u)" -eq 0 ] || die "Run with sudo: sudo bash $0" + +REAL_USER="${SUDO_USER:-joey}" +REAL_HOME=$(eval echo "~$REAL_USER") + +echo "" +bold "=========================================" +bold " Aurora Machine Setup" +bold "=========================================" +echo "" + +# ============================================================================= +# 1. BACKUP DRIVE SETUP +# ============================================================================= +bold "[1/2] Backup Drive Setup" +echo "─────────────────────────────────────────" + +# Install backup + restore scripts +cp "$SCRIPT_DIR/backup-to-portable" /usr/local/bin/backup-to-portable +cp "$SCRIPT_DIR/restore-from-portable" /usr/local/bin/restore-from-portable +chmod 755 /usr/local/bin/backup-to-portable /usr/local/bin/restore-from-portable +green " ✓ Installed backup/restore scripts to /usr/local/bin/" + +# Install systemd service +cp "$SCRIPT_DIR/backup-portable.service" /etc/systemd/system/backup-portable.service +systemctl daemon-reload +systemctl enable backup-portable.service +green " ✓ Installed and enabled backup-portable.service" + +# Install udev rule +cp "$SCRIPT_DIR/99-backup-portable.rules" /etc/udev/rules.d/99-backup-portable.rules +udevadm control --reload-rules +green " ✓ Installed udev auto-backup rule" + +# Generate LUKS keyfile if not present +if [ ! -f /etc/backup-drive.key ]; then + dd if=/dev/urandom of=/etc/backup-drive.key bs=4096 count=1 2>/dev/null + chmod 400 /etc/backup-drive.key + green " ✓ Generated new LUKS keyfile at /etc/backup-drive.key" + echo "" + yellow " ⚠ New keyfile generated — you need to add it to the backup drive:" + echo " 1. Plug in the backup drive" + echo " 2. sudo cryptsetup luksAddKey /dev/sdX1 /etc/backup-drive.key" + echo " (Enter your existing passphrase when prompted)" + echo "" +else + green " ✓ LUKS keyfile already exists at /etc/backup-drive.key" +fi + +# Create snapshots directory on source BTRFS +BTRFS_DEVICE=$(findmnt -n -o SOURCE /var/home | sed 's/\[.*//') +if [ -n "$BTRFS_DEVICE" ]; then + mkdir -p /mnt/btrfs-root + mount -o subvolid=5 "$BTRFS_DEVICE" /mnt/btrfs-root 2>/dev/null || true + if mountpoint -q /mnt/btrfs-root; then + mkdir -p /mnt/btrfs-root/snapshots + green " ✓ Snapshot directory ready on source BTRFS" + umount /mnt/btrfs-root + fi +fi + +# Create BTRFS subvolumes for excluded paths (if not already subvolumes) +create_exclude_subvol() { + local path="$1" + local full_path="$REAL_HOME/$path" + + # Skip if path doesn't exist or is already a subvolume + [ -d "$full_path" ] || return 0 + if btrfs subvolume show "$full_path" &>/dev/null; then + green " ✓ $path already a BTRFS subvolume (excluded from snapshots)" + return 0 + fi + + yellow " Converting $path to BTRFS subvolume (excludes from backup snapshots)..." + mv "$full_path" "${full_path}.setup-bak" + btrfs subvolume create "$full_path" + cp -a "${full_path}.setup-bak/." "$full_path/" 2>/dev/null || true + chown "$REAL_USER:$REAL_USER" "$full_path" + rm -rf "${full_path}.setup-bak" + green " ✓ $path → BTRFS subvolume" +} + +create_exclude_subvol ".cache" +create_exclude_subvol ".local/share/containers" + +echo "" + +# ============================================================================= +# 2. SYNCTHING SETUP +# ============================================================================= +bold "[2/2] Syncthing Setup" +echo "─────────────────────────────────────────" + +HPSERVER_ID="5JLI4YY-6TKV3VM-ZJ6J3HT-J3P5UBX-ZCO7OUN-REF4O2P-XHZPI6B-4SNFAAU" + +# Install .stignore +cp "$SCRIPT_DIR/.stignore" "$REAL_HOME/.stignore" +chown "$REAL_USER:$REAL_USER" "$REAL_HOME/.stignore" +green " ✓ Installed .stignore" + +# Check if syncthing is available +SYNCTHING_BIN="" +if command -v syncthing &>/dev/null; then + SYNCTHING_BIN="syncthing" +elif [ -x /home/linuxbrew/.linuxbrew/bin/syncthing ]; then + SYNCTHING_BIN="/home/linuxbrew/.linuxbrew/bin/syncthing" +fi + +if [ -z "$SYNCTHING_BIN" ]; then + yellow " Syncthing not installed. Installing via brew..." + if command -v brew &>/dev/null; then + sudo -u "$REAL_USER" brew install syncthing + SYNCTHING_BIN="/home/linuxbrew/.linuxbrew/bin/syncthing" + elif sudo -u "$REAL_USER" bash -c 'command -v brew' &>/dev/null; then + sudo -u "$REAL_USER" brew install syncthing + SYNCTHING_BIN="/home/linuxbrew/.linuxbrew/bin/syncthing" + else + yellow " ⚠ Homebrew not found. Install syncthing manually:" + echo " brew install syncthing" + echo " Then re-run this script, or manually:" + echo " brew services start syncthing" + echo " syncthing cli config devices add --device-id $HPSERVER_ID --name hpserver" + echo " syncthing cli config devices $HPSERVER_ID addresses add tcp://vgm.joeypayne.com:22000" + echo " syncthing cli config folders add --id home-sync --label Home --path ~/" + echo " syncthing cli config folders home-sync devices add --device-id $HPSERVER_ID" + echo "" + bold "Backup setup complete. Syncthing needs manual install." + exit 0 + fi +fi + +green " ✓ Syncthing found: $($SYNCTHING_BIN --version | head -1)" + +# Start syncthing as user if not running +if ! sudo -u "$REAL_USER" systemctl --user is-active syncthing.service &>/dev/null && \ + ! sudo -u "$REAL_USER" systemctl --user is-active homebrew.syncthing.service &>/dev/null; then + yellow " Starting syncthing..." + sudo -u "$REAL_USER" brew services start syncthing 2>/dev/null || \ + sudo -u "$REAL_USER" systemctl --user enable --now syncthing.service 2>/dev/null || \ + yellow " ⚠ Could not auto-start syncthing. Run: brew services start syncthing" + sleep 3 +fi +green " ✓ Syncthing running" + +# Configure: add hpserver device + home folder share +sudo -u "$REAL_USER" "$SYNCTHING_BIN" cli config devices add \ + --device-id "$HPSERVER_ID" --name "hpserver" 2>/dev/null || true + +# Add explicit address so hpserver is reachable from anywhere (not just LAN) +sudo -u "$REAL_USER" "$SYNCTHING_BIN" cli config devices "$HPSERVER_ID" addresses add \ + "tcp://vgm.joeypayne.com:22000" 2>/dev/null || true + +sudo -u "$REAL_USER" "$SYNCTHING_BIN" cli config folders add \ + --id "home-sync" --label "Home" --path "$REAL_HOME" 2>/dev/null || true + +sudo -u "$REAL_USER" "$SYNCTHING_BIN" cli config folders "home-sync" devices add \ + --device-id "$HPSERVER_ID" 2>/dev/null || true + +green " ✓ Syncthing configured: home-sync → hpserver" + +# Get this device's ID for pairing on hpserver +LOCAL_ID=$(sudo -u "$REAL_USER" "$SYNCTHING_BIN" cli config devices list 2>/dev/null | head -1) +echo "" +yellow " ⚠ If this is a NEW machine, add it to hpserver:" +echo " ssh hpserver \"syncthing cli config devices add --device-id '$LOCAL_ID' --name 'NEW-MACHINE-NAME'\"" +echo " ssh hpserver \"syncthing cli config folders home-sync devices add --device-id '$LOCAL_ID'\"" + +# ============================================================================= +# DONE +# ============================================================================= +echo "" +bold "=========================================" +green " Setup complete!" +bold "=========================================" +echo "" +echo " Backup:" +echo " • Plug in Seagate Portable → backup runs automatically" +echo " • Notification when safe to unplug" +echo " • Restore: sudo restore-from-portable" +if [ ! -f /etc/backup-drive.key ] || [ "$(stat -c%s /etc/backup-drive.key 2>/dev/null)" = "4096" ]; then + echo " • Remember to add keyfile to drive if this is a new machine" +fi +echo "" +echo " Sync:" +echo " • Syncthing syncs home → hpserver whenever online" +echo " • Excludes: caches, containers, downloads, browser data, AI logs" +echo " • Web UI: http://127.0.0.1:8384" +echo ""