#!/bin/bash # James W. Barnett # Takes snapshots of each snapper configuration. It then sends the snapshot to # a location on an external drive. After the initial transfer, it does # incremental snapshots on later calls. It's important not to delete the # snapshot created on your system since that will be used to determine the # difference for the next incremental snapshot. version="0.2" set -e while [[ $# -gt 0 ]]; do key="$1" case $key in -d|--description) description="$2" shift 2 ;; -c|--config) selected_configs="$2" shift 2 ;; -u|--UUID) uuid_cmdline="$2" shift 2 ;; -n|--noconfirm) noconfirm="yes" shift ;; -h|--help) printf "snap-sync $version\n\n" printf "Usage: snap-sync [options]\n\n" printf "Options:\n" printf " -d, --description Change the snapper description. Default: \"latest incremental backup\"\n" printf " -c, --config Specify the snapper configuration to use. Otherwise will perform for each snapper\n" printf " configuration. Can list multiple configurations within quotes, space-separated\n" printf " (e.g. -c \"root home\").\n" printf " -n, --noconfirm Do not ask for confirmation for each configuration. Will still prompt for backup\n" printf " directory name on first backup\n" printf " -u, --UUID Specify the UUID of the mounted BTRFS subvolume to back up to. Otherwise will prompt.\n" printf " If multiple mount points are found with the same UUID, will prompt user.\n" exit 1 ;; *) printf "ERROR: Unknown option: $key\n" printf "Run 'snap-sync -h' for valid options.\n" exit 1 ;; esac done description=${description:-"latest incremental backup"} uuid_cmdline=${uuid_cmdline:-"none"} noconfirm=${noconfirm:-"no"} if [[ $EUID -ne 0 ]]; then printf "Script must be run as root.\n" exit fi # It's important not to change this userdata in the snapshots, since that's how # we find the previous one. TARGETS="$(findmnt -n -v -t btrfs -o TARGET --list)" UUIDS="$(findmnt -n -v -t btrfs -o UUID --list)" declare -a TARGETS_ARRAY declare -a UUIDS_ARRAY i=0 disk=-1 disk_count=0 for x in $UUIDS; do UUIDS_ARRAY[$i]=$x if [[ "$x" == "$uuid_cmdline" ]]; then disk=$i disk_count=$(($disk_count+1)) fi i=$((i+1)) done i=0 for x in $TARGETS; do TARGETS_ARRAY[$i]=$x i=$((i+1)) done if [[ "$disk_count" > 1 ]]; then printf "Multiple mount points were found with UUID $uuid_cmdline.\n" disk="-1" fi if [[ "$disk" == -1 ]]; then if [[ "$disk_count" == 0 && "$uuid_cmdline" != "none" ]]; then printf "ERROR: A device with UUID $uuid_cmdline was not found to be mounted, or it is not a BTRFS device.\n" fi printf "Select a mounted BTRFS device to backup to.\n" while [[ $disk -lt 0 || $disk -gt $i ]]; do for x in "${!TARGETS_ARRAY[@]}"; do printf "%4s) %s (%s)\n" "$((x+1))" "${UUIDS_ARRAY[$x]}" "${TARGETS_ARRAY[$x]}" done printf "%4s) Exit\n" "0" read -r -p "Enter a number: " disk done if [[ $disk == 0 ]]; then exit 0 fi disk=$(($disk-1)) fi selected_uuid="${UUIDS_ARRAY[$((disk))]}" selected_mnt="${TARGETS_ARRAY[$((disk))]}" printf "\nYou selected the disk with UUID %s.\n" "$selected_uuid" printf "The disk is mounted at %s.\n" "$selected_mnt" if [[ -f /etc/conf.d/snapper ]]; then source /etc/conf.d/snapper else printf "ERROR: /etc/conf.d/snapper does not exist!\n" exit 1 fi selected_configs=${selected_configs:-$SNAPPER_CONFIGS} for x in $selected_configs; do printf "\n" if [[ -f "/etc/snapper/configs/$x" ]]; then source /etc/snapper/configs/$x else printf "ERROR: Selected snapper configuration $x does not exist.\n" exit 1 fi old_number=$(snapper -c "$x" list -t single | awk '/'"$selected_uuid"'/ {print $1}') old_snapshot=$SUBVOLUME/.snapshots/$old_number/snapshot if [[ -z "$old_number" ]]; then printf "No backups have been performed for '%s' on this disk.\n" "$x" read -r -p "Enter name of directory to store backups, relative to $selected_mnt (to be created if not existing): " mybackupdir printf "This will be the initial backup for snapper configuration '%s' to this disk. This could take awhile.\n" "$x" BACKUPDIR="$selected_mnt/$mybackupdir" mkdir -p -m700 "$BACKUPDIR" else mybackupdir=$(snapper -c root list -t single | awk -F"|" '/'"$selected_uuid"'/ {print $5}' | awk -F "," '/backupdir/ {print $1}' | awk -F"=" '{print $2}') BACKUPDIR="$selected_mnt/$mybackupdir" if [[ ! -d $BACKUPDIR ]]; then printf "ERROR: %s is not a directory on %s.\n" "$BACKUPDIR" "$selected_uuid" exit 1 fi fi new_number=$(snapper -c "$x" create --print-number -d "snap-sync backup in progress") new_snapshot=$SUBVOLUME/.snapshots/$new_number/snapshot new_info=$SUBVOLUME/.snapshots/$new_number/info.xml sync backup_location=$BACKUPDIR/$x/$new_number/ printf "Will backup %s to %s\n" "$new_snapshot" "$backup_location/snapshot" if [[ $noconfirm == "yes" ]]; then cont_backup="yes" else read -r -p "Continue with backup [Y/n]? " cont_backup fi if [[ "$cont_backup" != [Yy]"es" && "$cont_backup" != [Yy] && -n "$cont_backup" ]]; then printf "Aborting backup for this configuration.\n" snapper -c $x delete $new_number continue fi mkdir -p "$backup_location" if [[ -z "$old_number" ]]; then btrfs send "$new_snapshot" | btrfs receive "$backup_location" &>/dev/null else # Sends the difference between the new snapshot and old snapshot to the # backup location. Using the -c flag instead of -p tells it that there # is an identical subvolume to the old snapshot at the receiving # location where it can get its data. This helps speed up the transfer. btrfs send "$new_snapshot" -c "$old_snapshot" | btrfs receive "$backup_location" &>/dev/null snapper -c "$x" delete "$old_number" fi cp "$new_info" "$backup_location" userdata="backupdir=$mybackupdir, uuid=$selected_uuid" # Tag new snapshot as the latest snapper -v -c "$x" modify -d "$description" -u "$userdata" "$new_number" done printf "\nDone!\n"