dotfiles/aurora/restore-from-portable

352 lines
13 KiB
Bash
Executable file

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