#!/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" BACKUP_DESCRIPTION="btrfs-backup" # Get the actual snapshot location from snapper config SOURCE_PATH=$(sudo snapper -c "$SNAPPER_CONFIG" get-config | grep '^SUBVOLUME' | cut -d'=' -f2 | tr -d '"'| awk {'print $3'}) echo "SOURCE_PATH: $SOURCE_PATH" if [ -z "$SOURCE_PATH" ]; then echo "ERROR: Could not determine snapshot path for config '$SNAPPER_CONFIG'" exit 1 fi # Convert subvolume path to snapshot path SNAPSHOT_PATH="$SOURCE_PATH/.snapshots" if [ ! -d "$SNAPSHOT_PATH" ]; then echo "ERROR: Snapshot directory '$SNAPSHOT_PATH' does not exist" exit 1 fi # Create new snapshot with backup description NEW_SNAPSHOT=$(sudo snapper -c "$SNAPPER_CONFIG" create --description "$BACKUP_DESCRIPTION" --print-number) if [ -z "$NEW_SNAPSHOT" ]; then echo "ERROR: Failed to create new snapshot" exit 1 fi HOSTNME=$(hostname) REMOTE_HOST="root@synology" BASE_DEST_PATH="/volume1/BTRFS_Receives/`hostname`/${SNAPPER_CONFIG}" DEST_PATH="/volume1/BTRFS_Receives/`hostname`/${SNAPPER_CONFIG}/${NEW_SNAPSHOT}" STATE_FILE="/var/lib/snapper-backup-${SNAPPER_CONFIG}.state" LOG_FILE="/var/log/snapper-backup-${SNAPPER_CONFIG}.log" KEEP_SNAPSHOTS=14 # Get latest successful transfer number from snapshots with backup description LAST_TRANSFERRED=$(cat "$STATE_FILE" 2>/dev/null || echo "") # Ensure we exit on any error set -e # Verify snapper config exists if ! sudo 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" | sudo tee -a "$LOG_FILE" } # Function to verify snapshot exists verify_snapshot() { local snapshot_num="$1" if [ ! -d "$SNAPSHOT_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 '$BASE_DEST_PATH' -maxdepth 1 -type d -name '[0-9]*' | sort -n" } # Function to get local backup snapshots get_local_snapshots() { sudo snapper -c "$SNAPPER_CONFIG" list | grep "$BACKUP_DESCRIPTION" | awk '{print $1}' | sort -n } # Function to cleanup old snapshots both locally and remotely cleanup_snapshots() { local local_snapshots=($(get_local_snapshots)) local remote_snapshots=($(get_remote_snapshots)) local count=${#local_snapshots[@]} if [ $count -gt $KEEP_SNAPSHOTS ]; then local to_delete=$((count - KEEP_SNAPSHOTS)) log "Cleaning up $to_delete old snapshots both locally and remotely" for ((i=0; i<$to_delete; i++)); do local snapshot="${local_snapshots[$i]}" # Delete remote snapshot first if ssh "$REMOTE_HOST" "[ -d '$BASE_DEST_PATH/$snapshot' ]"; then log "Deleting remote snapshot: $snapshot" ssh "$REMOTE_HOST" "btrfs subvolume delete '$BASE_DEST_PATH/$snapshot/snapshot'" || \ log "WARNING: Failed to delete remote snapshot $snapshot" ssh "$REMOTE_HOST" "rm -rf '$BASE_DEST_PATH/$snapshot'" || \ log "WARNING: Failed to cleanup remote snapshot directory $snapshot" fi # Then delete local snapshot log "Deleting local snapshot: $snapshot" sudo snapper -c "$SNAPPER_CONFIG" delete "$snapshot" || \ log "WARNING: Failed to delete local snapshot $snapshot" done fi } # Start backup process log "Starting backup for snapper config: $SNAPPER_CONFIG" log "Using snapshot path: $SNAPSHOT_PATH" log "Created new snapshot: $NEW_SNAPSHOT" # Verify remote connectivity first verify_remote # Verify snapshots exist verify_snapshot "$NEW_SNAPSHOT" || exit 1 if [ -n "$LAST_TRANSFERRED" ]; then verify_snapshot "$LAST_TRANSFERRED" || exit 1 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 $NEW_SNAPSHOT" sudo btrfs send "$SNAPSHOT_PATH/$NEW_SNAPSHOT/snapshot" | \ pv --bytes | \ ssh "$REMOTE_HOST" "btrfs receive '$DEST_PATH'" && { echo "$NEW_SNAPSHOT" | sudo tee "$STATE_FILE" log "Full send completed successfully" } || { log "ERROR: Full send failed" sudo snapper -c "$SNAPPER_CONFIG" delete "$NEW_SNAPSHOT" exit 1 } else # Incremental send log "Performing incremental send from $LAST_TRANSFERRED to $NEW_SNAPSHOT" sudo btrfs send -p "$SNAPSHOT_PATH/$LAST_TRANSFERRED/snapshot" \ "$SNAPSHOT_PATH/$NEW_SNAPSHOT/snapshot" | \ pv --bytes | \ ssh "$REMOTE_HOST" "btrfs receive '$DEST_PATH'" && { echo "$NEW_SNAPSHOT" | sudo tee "$STATE_FILE" log "Incremental send completed successfully" } || { log "ERROR: Incremental send failed" sudo snapper -c "$SNAPPER_CONFIG" delete "$NEW_SNAPSHOT" exit 1 } fi # Cleanup old snapshots if transfer was successful cleanup_snapshots # Verify remote snapshots log "Current remote snapshots:" get_remote_snapshots | sudo tee -a "$LOG_FILE" log "Current local snapshots:" get_local_snapshots | sudo 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"