Compare commits
3 commits
d7573a3cbd
...
75d4c9ab03
| Author | SHA1 | Date | |
|---|---|---|---|
| 75d4c9ab03 | |||
| 484b37c3a3 | |||
| 1a1e467feb |
8 changed files with 910 additions and 1 deletions
|
|
@ -404,6 +404,9 @@ vim.g.syntastic_enable_elixir_checker = 1
|
|||
vim.g.syntastic_elixir_checkers = { "elixir" }
|
||||
vim.g.LargeFile = 20
|
||||
vim.g.airline_powerline_fonts = 1
|
||||
local airline_symbols = vim.g.airline_symbols or {}
|
||||
airline_symbols.branch = "\u{e0a0}"
|
||||
vim.g.airline_symbols = airline_symbols
|
||||
vim.g.airline_section_x = ""
|
||||
vim.g.airline_section_y = ""
|
||||
vim.g.airline_section_warning = ""
|
||||
|
|
@ -1033,4 +1036,4 @@ vim.cmd("hi GitSignsChangeVirtLnInline ctermfg=blue ctermbg=none guibg=#0043AE g
|
|||
-- Colorscheme application
|
||||
pcall(vim.cmd, "colorscheme codedark")
|
||||
_G.SetTransparent()
|
||||
vim.cmd("hi CursorLine guibg=NONE ctermbg=NONE gui=bold,undercurl cterm=bold guisp=#777777")
|
||||
vim.cmd("hi CursorLine guibg=NONE ctermbg=NONE gui=bold cterm=bold guisp=#777777")
|
||||
|
|
|
|||
65
aurora/.stignore
Normal file
65
aurora/.stignore
Normal file
|
|
@ -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
|
||||
2
aurora/99-backup-portable.rules
Normal file
2
aurora/99-backup-portable.rules
Normal file
|
|
@ -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"
|
||||
18
aurora/backup-portable.service
Normal file
18
aurora/backup-portable.service
Normal file
|
|
@ -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
|
||||
199
aurora/backup-to-portable
Executable file
199
aurora/backup-to-portable
Executable file
|
|
@ -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
|
||||
352
aurora/restore-from-portable
Executable file
352
aurora/restore-from-portable
Executable file
|
|
@ -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
|
||||
205
aurora/setup.sh
Executable file
205
aurora/setup.sh
Executable file
|
|
@ -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 ""
|
||||
65
restore-backup.sh
Executable file
65
restore-backup.sh
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
#!/bin/bash
|
||||
# Restore from Portable backup drive on a fresh Aurora install
|
||||
# Finds the drive, opens LUKS, mounts, and runs the restore script
|
||||
set -euo pipefail
|
||||
|
||||
bold() { echo -e "\033[1m$*\033[0m"; }
|
||||
red() { echo -e "\033[31m$*\033[0m"; }
|
||||
green() { echo -e "\033[32m$*\033[0m"; }
|
||||
|
||||
die() { red "ERROR: $*"; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Run with sudo: sudo bash restore-backup.sh"
|
||||
|
||||
LUKS_NAME="backup-drive"
|
||||
MOUNT="/mnt/backup-drive"
|
||||
|
||||
# --- Find the drive ---
|
||||
# Try stable by-id path first (Seagate Portable), fall back to scanning for LUKS partitions
|
||||
DEVICE=""
|
||||
for dev in /dev/disk/by-id/usb-Seagate_Portable_NT3D9HDX-*-part1; do
|
||||
[ -e "$dev" ] && DEVICE="$dev" && break
|
||||
done
|
||||
|
||||
if [ -z "$DEVICE" ]; then
|
||||
bold "Seagate Portable not found by ID. Scanning for LUKS partitions..."
|
||||
for part in /dev/sd?1 /dev/nvme?n1p?; do
|
||||
[ -b "$part" ] || continue
|
||||
if cryptsetup isLuks "$part" 2>/dev/null; then
|
||||
echo " Found LUKS partition: $part"
|
||||
DEVICE="$part"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
[ -n "$DEVICE" ] || die "No backup drive found. Plug it in and try again."
|
||||
bold "Using device: $DEVICE"
|
||||
|
||||
# --- Open LUKS ---
|
||||
if [ ! -e "/dev/mapper/$LUKS_NAME" ]; then
|
||||
echo ""
|
||||
bold "Enter your backup drive passphrase:"
|
||||
cryptsetup luksOpen "$DEVICE" "$LUKS_NAME" || die "Failed to unlock drive"
|
||||
fi
|
||||
green "LUKS unlocked."
|
||||
|
||||
# --- Mount ---
|
||||
mkdir -p "$MOUNT"
|
||||
if ! mountpoint -q "$MOUNT"; then
|
||||
mount /dev/mapper/"$LUKS_NAME" "$MOUNT" || die "Failed to mount"
|
||||
fi
|
||||
green "Mounted at $MOUNT"
|
||||
|
||||
# --- Verify it's a backup drive ---
|
||||
if [ ! -d "$MOUNT/backups" ] || [ ! -f "$MOUNT/restore.sh" ]; then
|
||||
die "Drive doesn't look like a backup drive (missing backups/ or restore.sh)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
green "Backup drive ready."
|
||||
bold "Launching restore menu..."
|
||||
echo ""
|
||||
|
||||
# --- Run restore ---
|
||||
bash "$MOUNT/restore.sh"
|
||||
Loading…
Add table
Add a link
Reference in a new issue