#!/bin/bash # Backup Dir export SOURCE_DIR=$1 # Fix basename / showing up as "/" -> change to "root" if [ $(basename $SOURCE_DIR) = / ]; then export SNAPSHOT_PREFIX=${SNAPSHOT_PREFIX:=root} else export SNAPSHOT_PREFIX=${SNAPSHOT_PREFIX:=$(basename $SOURCE_DIR)} fi # Set snapshot prefix based on basename export SNAPSHOT_TIME=$(date +"%y_%m_%d-%H.%M") export SNAPSHOT_NAME=$SNAPSHOT_PREFIX-$SNAPSHOT_TIME export SNAPSHOT_DIR=${SNAPSHOT_DIR:=/.snapshots} export LATEST=$SNAPSHOT_PREFIX-latest # For notifications export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ notifications.user.uid }}/bus export USER={{ notifications.user.name }} function exit_success { # Exit exit 0 } function exit_fail { # Exit exit 1 } function get_latest { DIR=$1 if [ -f $DIR/$LATEST ]; then echo $(cat $DIR/$LATEST) else echo "" fi } function update_latest { DIR=$1 NAME=$2 echo $2 > $DIR/$LATEST } function log { LEVEL=$1 MESSAGE=$2 log_msg="$(date) $LEVEL: $MESSAGE" echo $log_msg } function notify { LEVEL=$1 MESSAGE=$2 log "$LEVEL" "$MESSAGE" sudo -E -u $USER notify-send "$LEVEL" "$MESSAGE" --expire-time 10 } function get_mount { findmnt -o target /dev/disk/by-uuid/$1 --noheadings } # Show snapshot settings log "INFO" "SOURCE_DIR $SOURCE_DIR" log "INFO" "SNAPSHOT_PREFIX $SNAPSHOT_PREFIX" log "INFO" "SNAPSHOT_TIME $SNAPSHOT_TIME" log "INFO" "SNAPSHOT_NAME $SNAPSHOT_NAME" log "INFO" "SNAPSHOT_DIR $SNAPSHOT_DIR" log "INFO" "LATEST $LATEST" # Create readonly snapshot log "INFO" "Creating snapshot from $SOURCE_DIR as $SNAPSHOT_DIR/$SNAPSHOT_NAME" if [ -d $SNAPSHOT_DIR/$SNAPSHOT_NAME ]; then log "WARN" "Snapshot $SNAPSHOT_DIR/$SNAPSHOT_NAME already created. Skipping" else btrfs subvolume snapshot -r $SOURCE_DIR $SNAPSHOT_DIR/$SNAPSHOT_NAME fi # Update latest in snapshot dir log "INFO" "Updating latest in $SNAPSHOT_DIR to $SNAPSHOT_NAME." update_latest $SNAPSHOT_DIR $SNAPSHOT_NAME export BACKUP_DRIVE_UUID={{ disk.uuid }} get_mount $BACKUP_DRIVE_UUID if [ $? != 0 ]; then notify "WARN" "Snapshot complete but backup drive $BACKUP_DRIVE_UUID not mounted. Backup not sent." exit 1 fi export BACKUP_DRIVE_MNT=$(findmnt -o target /dev/disk/by-uuid/$BACKUP_DRIVE_UUID --noheadings) export BACKUP_DIR=$BACKUP_DRIVE_MNT/$(hostname) mkdir -p $BACKUP_DIR log "INFO" "BACKUP_DRIVE_MNT $BACKUP_DRIVE_MNT" log "INFO" "BACKUP_DIR $BACKUP_DIR" log "INFO" "Drive $BACKUP_DRIVE_UUID mounted at $BACKUP_DRIVE_MNT" # First check if the snapshot dir has a "latest" snapshot # This will be needed to send an incremental snapshot LATEST_SNAPSHOT="$(get_latest $SNAPSHOT_DIR)" log "INFO" "Latest snapshot is $LATEST_SNAPSHOT" # Next, check if the backup drive has a "latest" snapshot LATEST_BACKUP="$(get_latest $BACKUP_DIR)" log "INFO" "Latest backup is $LATEST_BACKUP" # Now check if the "latest" snapshots match # btrfs requires both the sending drive and receiving drive have # matching parent snapshots. # # There are a few scenarios to cover # 1. Neither the backup drive nor the local snapshot dir have a "latest" # This can happen if the backup occurs before any snapshots are # taken. Don't send anything. # 2. The backup drive has a "latest" but the snapshot dir doesn't # This can happen when the local drive is restored from backup # but the snapshot dir didn't copy over. nothing to send. # 3. The backup drive and snapshot dir have a "latest" and they are the # same. # Send backup with parent as normal. # 4. The snapshot dir has a "latest" but the backup drive doesn't # This can happen when backing up for the first time. Send the # snapshot without a parent # 5. Both the snapshot dir and backup drive have a latest, but they are # out of sync. # This can happen when snapshots are taken with the backup drive # disconnected. There's a few sub-scenarios here: # a. The snapshot dir has the "latest" snapshot from the backup dir, # it's just older than the "latest" snapshot in the snapshot dir # Re-sync the "latest" snapshot dir with the one in the # backup dir. Send as normal with parents. # b. The snapshot dir does not have the "latest" snapshot from the # backup dir. # Here be dragons. Something went wrong and will likely need # to be manually reconfigured. Raise a critical alert. # Scenario 1 and 2 if [ "$LATEST_SNAPSHOT" = "" ]; then notify "WARN" "Neither the snapshot dir nor the backup drive has a 'latest' snapshot." exit_success fi # Scenario 3 if [ "$LATEST_SNAPSHOT" = "$LATEST_BACKUP" ]; then log "INFO" "Proceeding with backups as normal." # Send incremental snapshot btrfs send -p $SNAPSHOT_DIR/$LATEST_SNAPSHOT $SNAPSHOT_DIR/$SNAPSHOT_NAME | btrfs receive $BACKUP_DIR if [ $? != 0 ]; then notify "ERROR" "btrfs send -p $SNAPSHOT_DIR/$LATEST_SNAPSHOT $SNAPSHOT_DIR/$SNAPSHOT_NAME failed." exit_fail fi # Update latest in backup dir update_latest $BACKUP_DIR $SNAPSHOT_NAME # Update latest in snapshot dir update_latest $SNAPSHOT_DIR $SNAPSHOT_NAME # Exit notify "INFO" "Backup completed" "Backup $SNAPSHOT_NAME completed successfully." exit_success fi # Scenario 4 if [ "$LATEST_BACKUP" = "" ]; then log "INFO" "No prior backups detected. Sending full backup." # Send incremental snapshot btrfs send $SNAPSHOT_DIR/$SNAPSHOT_NAME | btrfs receive $BACKUP_DIR if [ $? != 0 ]; then notify "ERROR" "btrfs send $SNAPSHOT_DIR/$SNAPSHOT_NAME failed." exit_fail fi # Update latest in backup dir update_latest $BACKUP_DIR $SNAPSHOT_NAME # Update latest in snapshot dir update_latest $SNAPSHOT_DIR $SNAPSHOT_NAME # Exit notify "INFO" "Backup $SNAPSHOT_NAME completed successfully." exit_success fi # Scenario 5a log "INFO" "Detected drift. Attempting to synchronize latest snapshot with backup. Set to $LATEST_BACKUP." if [ -d $SNAPSHOT_DIR/$LATEST_BACKUP ]; then log "INFO" "$LATEST_BACKUP found in snapshot dir. Synchronizing and proceeding." btrfs send -p $SNAPSHOT_DIR/$LATEST_BACKUP $SNAPSHOT_DIR/$SNAPSHOT_NAME | btrfs receive $BACKUP_DIR if [ $? != 0 ]; then notify "ERROR" "btrfs send -p $SNAPSHOT_DIR/$LATEST_BACKUP $SNAPSHOT_DIR/$SNAPSHOT_NAME failed." exit_fail fi # Update latest in backup dir update_latest $BACKUP_DIR $SNAPSHOT_NAME # Update latest in snapshot dir update_latest $SNAPSHOT_DIR $SNAPSHOT_NAME # Exit notify "INFO" "Backup $SNAPSHOT_NAME completed successfully." exit_success # Scenario 5b else log "ERROR" "Something went wrong. $LATEST_BACKUP not found in $SNAPSHOT_DIR." notify "ERROR" "$LATEST_BACKUP not found in $SNAPSHOT_DIR." exit_fail fi