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