diff --git a/nixos/common/software/cli/scripts.nix b/nixos/common/software/cli/scripts.nix new file mode 100644 index 00000000..f7863895 --- /dev/null +++ b/nixos/common/software/cli/scripts.nix @@ -0,0 +1,11 @@ +{ pkgs, ... }: +let + clean-hm = pkgs.writeScriptBin "clean-hm" "${builtins.readFile ./scripts/clean-hm.sh}"; + btrfs-backup = pkgs.writeScriptBin "btrfs-backup" "${builtins.readFile ./scripts/btrfs-backup.sh}"; +in { + environment.systemPackages = [ + clean-hm + btrfs-backup + ]; +} + diff --git a/nixos/common/software/cli/scripts/btrfs-backup.sh b/nixos/common/software/cli/scripts/btrfs-backup.sh new file mode 100644 index 00000000..0a70b683 --- /dev/null +++ b/nixos/common/software/cli/scripts/btrfs-backup.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +# Check for required argument +if [ $# -ne 1 ]; then + echo "Usage: $(basename "$0") SNAPPER_CONFIG" + echo "Example: $(basename "$0") root" + exit 1 +fi + +# Configuration +SNAPPER_CONFIG="$1" +SOURCE_PATH="/.snapshots" +REMOTE_HOST="root@synology" +DEST_PATH="/volume1/backups/${SNAPPER_CONFIG}" # Added config name to path +STATE_FILE="/var/lib/snapper-backup-${SNAPPER_CONFIG}.state" # Made state file unique per config +LOG_FILE="/var/log/snapper-backup-${SNAPPER_CONFIG}.log" # Made log file unique per config +KEEP_SNAPSHOTS=5 + +# Ensure we exit on any error +set -e + +# Verify snapper config exists +if ! snapper -c "$SNAPPER_CONFIG" list &>/dev/null; then + echo "ERROR: Snapper config '$SNAPPER_CONFIG' does not exist" + exit 1 +fi + +# Logging function +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" +} + +# Function to verify snapshot exists +verify_snapshot() { + local snapshot_num="$1" + if [ ! -d "$SOURCE_PATH/$snapshot_num/snapshot" ]; then + log "ERROR: Snapshot $snapshot_num does not exist" + return 1 + fi + return 0 +} + +# Function to verify remote connectivity +verify_remote() { + if ! ssh -q "$REMOTE_HOST" "exit"; then + log "ERROR: Cannot connect to remote host" + exit 1 + fi +} + +# Function to get remote snapshots +get_remote_snapshots() { + ssh "$REMOTE_HOST" "find '$DEST_PATH' -maxdepth 1 -type d -name 'snapshot*' | sort -n" +} + +# Function to cleanup old remote snapshots +cleanup_remote_snapshots() { + local snapshots=($(get_remote_snapshots)) + local count=${#snapshots[@]} + + if [ $count -gt $KEEP_SNAPSHOTS ]; then + local to_delete=$((count - KEEP_SNAPSHOTS)) + log "Cleaning up $to_delete old snapshots on remote system" + + for ((i=0; i<$to_delete; i++)); do + local snapshot="${snapshots[$i]}" + log "Deleting remote snapshot: $snapshot" + ssh "$REMOTE_HOST" "btrfs subvolume delete '$snapshot'" || \ + log "WARNING: Failed to delete snapshot $snapshot" + done + fi +} + +# Start backup process +log "Starting backup for snapper config: $SNAPPER_CONFIG" + +# Verify remote connectivity first +verify_remote + +# Get latest successful transfer number +LAST_TRANSFERRED=$(cat "$STATE_FILE" 2>/dev/null || echo "") + +# Get latest snapshot number from snapper +LATEST_SNAPSHOT=$(snapper -c "$SNAPPER_CONFIG" list | tail -n 1 | awk '{print $1}') + +# Verify snapshots exist +verify_snapshot "$LATEST_SNAPSHOT" || exit 1 +if [ -n "$LAST_TRANSFERRED" ]; then + verify_snapshot "$LAST_TRANSFERRED" || exit 1 +fi + +# Exit if no new snapshots to transfer +if [ "$LAST_TRANSFERRED" = "$LATEST_SNAPSHOT" ]; then + log "No new snapshots to transfer" + exit 0 +fi + +# Create destination directory if it doesn't exist +ssh "$REMOTE_HOST" "mkdir -p '$DEST_PATH'" + +# Perform the transfer +if [ -z "$LAST_TRANSFERRED" ]; then + # First time backup - full send + log "Performing full send of snapshot $LATEST_SNAPSHOT" + sudo btrfs send "$SOURCE_PATH/$LATEST_SNAPSHOT/snapshot" | \ + pv -bytes | \ + ssh "$REMOTE_HOST" "btrfs receive '$DEST_PATH'" && { + echo "$LATEST_SNAPSHOT" > "$STATE_FILE" + log "Full send completed successfully" + } || { + log "ERROR: Full send failed" + exit 1 + } +else + # Incremental send + log "Performing incremental send from $LAST_TRANSFERRED to $LATEST_SNAPSHOT" + sudo btrfs send -p "$SOURCE_PATH/$LAST_TRANSFERRED/snapshot" \ + "$SOURCE_PATH/$LATEST_SNAPSHOT/snapshot" | \ + pv -bytes | \ + ssh "$REMOTE_HOST" "btrfs receive '$DEST_PATH'" && { + echo "$LATEST_SNAPSHOT" > "$STATE_FILE" + log "Incremental send completed successfully" + } || { + log "ERROR: Incremental send failed" + exit 1 + } +fi + +# Cleanup old snapshots if transfer was successful +cleanup_remote_snapshots + +# Verify remote snapshots +log "Current remote snapshots:" +get_remote_snapshots | tee -a "$LOG_FILE" + +# Final verification +if ! ssh "$REMOTE_HOST" "btrfs subvolume show '$DEST_PATH/snapshot'" &>/dev/null; then + log "WARNING: Final verification failed" + exit 1 +fi + +log "Backup completed successfully" +