#!/usr/bin/env bash # ── Sovran Hub External Backup Script ──────────────────────────── # Backs up Sovran_SystemsOS data to an external USB hard drive. # Designed for the Hub web UI (no GUI dependencies). # # Your Sovran Pro already backs up your data automatically to its # internal second drive (BTCEcoandBackup at /run/media/Second_Drive). # This script creates an additional copy on an external USB drive — # storing your data in a third location for maximum protection. # # Usage: # BACKUP_TARGET=/run/media// bash sovran-hub-backup.sh # (or run with no env var to auto-detect the first external USB drive) set -euo pipefail BACKUP_LOG="/var/log/sovran-hub-backup.log" BACKUP_STATUS="/var/log/sovran-hub-backup.status" MEDIA_ROOT="/run/media" MIN_FREE_GB=10 # ── Internal drive labels/paths to NEVER use as backup targets ─── INTERNAL_LABELS=("BTCEcoandBackup" "sovran_systemsos") INTERNAL_MOUNTS=("/run/media/Second_Drive" "/boot/efi" "/") # ── Logging helpers ────────────────────────────────────────────── log() { local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*" echo "$msg" | tee -a "$BACKUP_LOG" } set_status() { echo "$1" > "$BACKUP_STATUS" } fail() { log "ERROR: $*" set_status "FAILED" exit 1 } # ── Check whether a mount point is an internal drive ──────────── is_internal() { local mnt="$1" # Reject known internal mount points and their subdirectories for internal in "${INTERNAL_MOUNTS[@]}"; do if [[ "$mnt" == "$internal" || "$mnt" == "${internal}/"* ]]; then return 0 fi done return 1 } # ── Use lsblk to find the first genuine external USB drive ─────── find_external_drive() { local target="" # lsblk JSON output: NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE if command -v lsblk &>/dev/null; then while IFS=$'\t' read -r dev_type hotplug removable label mountpoint; do # Must be a partition or disk, and be removable/hotplug [[ "$dev_type" == "part" || "$dev_type" == "disk" ]] || continue [[ "$hotplug" == "1" || "$removable" == "1" ]] || continue [[ -n "$mountpoint" ]] || continue # Filter out internal labels local skip=0 for lbl in "${INTERNAL_LABELS[@]}"; do [[ "$label" == "$lbl" ]] && skip=1 && break done [[ "$skip" -eq 1 ]] && continue # Filter out internal mount points is_internal "$mountpoint" && continue if mountpoint -q "$mountpoint" 2>/dev/null; then target="$mountpoint" break fi done < <(lsblk -J -o NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE 2>/dev/null | \ python3 -c " import sys, json data = json.load(sys.stdin) def flatten(devs): for d in devs: yield d for c in d.get('children', []): yield from flatten([c]) for d in flatten(data.get('blockdevices', [])): print('\t'.join([ d.get('type') or '', str(d.get('hotplug') or '0'), str(d.get('rm') or '0'), d.get('label') or '', d.get('mountpoint') or '', ])) " 2>/dev/null || true) fi # Fallback: walk /run/media/ if lsblk produced nothing if [[ -z "$target" && -d "$MEDIA_ROOT" ]]; then while IFS= read -r -d '' mnt; do is_internal "$mnt" && continue # Check label via lsblk on the device backing this mount local dev dev=$(findmnt -n -o SOURCE "$mnt" 2>/dev/null || true) if [[ -n "$dev" ]]; then local lbl lbl=$(lsblk -n -o LABEL "$dev" 2>/dev/null || true) local skip=0 for internal_lbl in "${INTERNAL_LABELS[@]}"; do [[ "$lbl" == "$internal_lbl" ]] && skip=1 && break done [[ "$skip" -eq 1 ]] && continue fi if mountpoint -q "$mnt" 2>/dev/null; then target="$mnt" break fi done < <(find "$MEDIA_ROOT" -mindepth 2 -maxdepth 2 -type d -print0 2>/dev/null) fi echo "$target" } # ── Initialise log file ────────────────────────────────────────── : > "$BACKUP_LOG" set_status "RUNNING" log "=== Sovran_SystemsOS External Hub Backup ===" log "Starting backup process…" # ── Detect target drive ────────────────────────────────────────── if [[ -n "${BACKUP_TARGET:-}" ]]; then TARGET="$BACKUP_TARGET" # Safety: never allow internal drives even if explicitly passed if is_internal "$TARGET"; then fail "Target '$TARGET' is an internal system drive and cannot be used for external backup." fi log "Using specified backup target: $TARGET" else log "Auto-detecting external USB drives…" TARGET="$(find_external_drive)" if [[ -z "$TARGET" ]]; then fail "No external USB drive detected. " \ "Please plug in an exFAT-formatted USB drive (≥500 GB) and try again." fi log "Detected external drive: $TARGET" fi # ── Verify mount point ─────────────────────────────────────────── [[ -d "$TARGET" ]] || fail "Target path '$TARGET' does not exist." mountpoint -q "$TARGET" || fail "Target path '$TARGET' is not a mount point." # ── Check free disk space (require ≥ 10 GB) ────────────────────── FREE_KB=$(df -k --output=avail "$TARGET" | tail -1) FREE_GB=$(( FREE_KB / 1024 / 1024 )) log "Free space on drive: ${FREE_GB} GB" (( FREE_GB >= MIN_FREE_GB )) || \ fail "Not enough free space on drive (${FREE_GB} GB available, ${MIN_FREE_GB} GB required)." # ── Create timestamped backup directory ───────────────────────── TIMESTAMP="$(date '+%Y%m%d_%H%M%S')" BACKUP_DIR="${TARGET}/Sovran_SystemsOS_Backup/${TIMESTAMP}" mkdir -p "$BACKUP_DIR" log "Backup destination: $BACKUP_DIR" # ── Stage 1/4: NixOS configuration ────────────────────────────── log "" log "── Stage 1/4: NixOS configuration (/etc/nixos) ──────────────" if [[ -d /etc/nixos ]]; then rsync -a --info=progress2 /etc/nixos/ "$BACKUP_DIR/nixos/" 2>&1 | tee -a "$BACKUP_LOG" || \ fail "Stage 1 failed while copying /etc/nixos" log "Stage 1 complete." else log "WARNING: /etc/nixos not found — skipping." fi # ── Stage 2/4: Secrets ────────────────────────────────────────── log "" log "── Stage 2/4: Secrets ───────────────────────────────────────" mkdir -p "$BACKUP_DIR/secrets" for SRC in /etc/nix-bitcoin-secrets /var/lib/domains; do if [[ -e "$SRC" ]]; then rsync -a --info=progress2 "$SRC" "$BACKUP_DIR/secrets/" 2>&1 | tee -a "$BACKUP_LOG" || \ log "WARNING: Could not copy $SRC — continuing." else log " (not found: $SRC — skipping)" fi done # Hub state files from /var/lib/secrets/ if [[ -d /var/lib/secrets ]]; then mkdir -p "$BACKUP_DIR/secrets/hub-state" rsync -a --info=progress2 /var/lib/secrets/ "$BACKUP_DIR/secrets/hub-state/" 2>&1 | tee -a "$BACKUP_LOG" || \ log "WARNING: Could not copy /var/lib/secrets — continuing." else log " (not found: /var/lib/secrets — skipping)" fi log "Stage 2 complete." # ── Stage 3/4: Home directory ──────────────────────────────────── log "" log "── Stage 3/4: Home directory (/home) ───────────────────────" if [[ -d /home ]]; then rsync -a --info=progress2 \ --exclude='.cache/' \ --exclude='.local/share/Trash/' \ --exclude='*/Trash/' \ /home/ "$BACKUP_DIR/home/" 2>&1 | tee -a "$BACKUP_LOG" || \ fail "Stage 3 failed while copying /home" log "Stage 3 complete." else log "WARNING: /home not found — skipping." fi # ── Stage 4/4: Wallet and node data ───────────────────────────── log "" log "── Stage 4/4: Wallet and node data (/var/lib/lnd) ──────────" if [[ -d /var/lib/lnd ]]; then rsync -a --info=progress2 \ --exclude='logs/' \ /var/lib/lnd/ "$BACKUP_DIR/lnd/" 2>&1 | tee -a "$BACKUP_LOG" || \ fail "Stage 4 failed while copying /var/lib/lnd" log "Stage 4 complete." else log "WARNING: /var/lib/lnd not found — skipping." fi # ── Generate manifest ──────────────────────────────────────────── log "" log "Generating BACKUP_MANIFEST.txt …" { echo "Sovran_SystemsOS Backup Manifest" echo "Generated: $(date)" echo "Hostname: $(hostname)" echo "Target: $TARGET" echo "" echo "Contents:" find "$BACKUP_DIR" -mindepth 1 -maxdepth 2 | sort } > "$BACKUP_DIR/BACKUP_MANIFEST.txt" log "Manifest written to $BACKUP_DIR/BACKUP_MANIFEST.txt" # ── Done ───────────────────────────────────────────────────────── log "" log "All Finished! Your data is now backed up to a third location." log "Please eject the drive safely before removing it from your Sovran Pro." set_status "SUCCESS"