#!/bin/bash # Rod's Incremental Backup System # Define constants RIBS_VERSION="1.3.2" USER_CONF_DIR="$HOME/.ribs/conf-enabled" SYSTEM_CONF_DIR="/etc/ribs/conf-enabled" # Define functions function find_configs() { # Search for config files in the default places and build a list # Takes one optional argument, the name of a config file. # Defines the variable conf_files, a list of conf files conf_files=() # First check user's conf directory, then the system conf directory if [[ $1 != "" ]] then conf_files+=("$1") elif ls -1 "$USER_CONF_DIR"/* >/dev/null 2>&1 then for conf_file in "$USER_CONF_DIR"/* do conf_files+=("$conf_file") done elif ls -1 "$SYSTEM_CONF_DIR"/* >/dev/null 2>&1 then for conf_file in "$SYSTEM_CONF_DIR"/* do conf_files+=("$conf_file") done else echo "Couldn't find any config files in" echo "$USER_CONF_DIR or $SYSTEM_CONF_DIR," echo "and one was not specified on the command line." echo "Please refer to the file /etc/ribs/conf-available/ribs.conf.sample" echo "and create one." exit 1 fi } function read_config() { # Read the configuration file # Make sure the provided config file is readable. if [[ ! -r $CONF_FILE ]] then echo -n "Reading configuration file $CONF_FILE" echo -e "\e[31mFAILED\e[0m" echo "The config file $CONF_FILE does not exist or is not readable." echo "Aborting." exit 1 fi echo -n "Reading configuration file... " if ! source "$CONF_FILE" then echo -e "\e[31mFAILED\e[0m" echo "An error was encountered while reading the configuration file" echo "$CONF_FILE." echo "Aborting." exit 1 else echo -e "\e[32mOK\e[0m" fi # Add slash to the beginning of $BACKUP_SUBDIR if it's not there if [[ "$(echo "$BACKUP_SUBDIR" | cut -c1)" != "/" ]] then BACKUP_SUBDIR="/$BACKUP_SUBDIR" fi # Remove slash from the end of $BACKUP_SUBDIR if it's there if [[ "$(echo "$BACKUP_SUBDIR" | rev | cut -c1)" = "/" ]] then numchars=$(echo "$BACKUP_SUBDIR" | wc -m);let "numchars-=2" BACKUP_SUBDIR=$(echo "$BACKUP_SUBDIR" | cut -c-$numchars) fi } function generate_excludes() { # Generate exclude array declare -a excludes if [[ $EXCLUDE_LIST != "" ]] then for pattern in $EXCLUDE_LIST do echo "excluding pattern $pattern" excludes+=' --exclude="$pattern" ' done fi } function mount_storage() { # Takes one argument, rw, ro, or u mount_mode=$1 if [[ $mount_mode = "ro" ]] then mode_text="read-only" elif [[ $mount_mode = "rw" ]] then mode_text="read-write" elif [[ $mount_mode = "u" ]] then mode_text="unmounted" else echo "Program error: invalid option \"$mode_text\" passed to mount_storage()" exit 1 fi # Mount the backup drive # First, check to see if backup drive partition is mounted if ! mount|grep -q "$BACKUP_ROOT" then remount_option="remount," else remount_option="" fi if [[ $mount_mode = "u" ]] then # Unmount drive echo -n "Unmounting backup storage... " if ! umount "$BACKUP_ROOT" then echo -e "\e[31mFAILED\e[0m" echo "An error was encountered while attempting to unmount $BACKUP_ROOT." echo "Aborting." exit 1 else echo -e "\e[32mOK\e[0m" fi else # Mount the drive in the desired mode echo -n "Mounting backup storage $mode_text... " if [[ $MOUNT_PARAMETERS != "" ]] then if ! mount "$MOUNT_PARAMETERS" "$BACKUP_ROOT" -o"$remount_option""$mount_mode" then echo -e "\e[31mFAILED\e[0m" echo "An error was encountered while attempting to mount the backup storage" echo "on $BACKUP_ROOT in $mode_text mode. Check MOUNT_PARAMETERS in your" echo "configuration file." echo "Aborting." exit 1 else echo -e "\e[32mOK\e[0m" fi else echo -e "\e[31mFAILED\e[0m" echo "Unable to mount backup storage." echo "No MOUNT_PARAMETERS were provided in config file." echo "Aborting." exit 1 fi fi } function list_backups() { # Generate a list of backups based on storage type if [[ $STORAGE_TYPE = "REMOTE" ]] then ssh $REMOTE_USER_HOST cd $BACKUP_ROOT$BACKUP_SUBDIR && ls -1d ????-??-??* else cd $BACKUP_ROOT$BACKUP_SUBDIR && ls -1d ????-??-??* fi } function delete_backup() { # Delete a backup. Takes one argument, the date directory to delete echo -n "Removing old backup $1... " remove_fail=false if [[ $STORAGE_TYPE = "REMOTE" ]] then if ! ssh $REMOTE_USER_HOST rm -r $BACKUP_ROOT$BACKUP_SUBDIR/$1 then remove_fail=true fi else if ! rm -r $BACKUP_ROOT$BACKUP_SUBDIR/$1 then remove_fail=true fi fi if $remove_fail then echo -e "\e[31mFAILED\e[0m" else echo -e "\e[32mOK\e[0m" fi } function run_backup() { CONF_FILE="$1" echo "Run started with configuration file:" echo "$CONF_FILE" date "+%a %b %d %H:%M:%S %Z %Y" echo "" # Read config file read_config # Generate exclude array generate_excludes # Mount offline storage if required if [[ $STORAGE_TYPE == "OFFLINE" ]] then mount_storage rw fi # Today's date current_datetime=$(date +%Y-%m-%dT%H:%m:%S) echo "Today's date: $current_datetime" # Previous backup's date if $list_cmd >/dev/null 2>&1 then previous_datetime=$($list_cmd | tail -n1) else # there are no other date directories previous_datetime="" fi if [[ $previous_datetime == "" ]] then echo "No previous backup date." fi # set target and link destination directories and day to remove if [[ $STORAGE_TYPE == "REMOTE" ]] then TRG="$REMOTE_USER_HOST:$BACKUP_ROOT$BACKUP_SUBDIR/$current_datetime/" LNK="$BACKUP_ROOT$BACKUP_SUBDIR/$previous_datetime/" if ssh "$REMOTE_USER_HOST" test -e "$LNK" then link_dest_exists=true else link_dest_exists=false fi else TRG="$BACKUP_ROOT$BACKUP_SUBDIR/$current_datetime/" LNK="$BACKUP_ROOT$BACKUP_SUBDIR/$previous_datetime" if [[ -e "$LNK" ]] then link_dest_exists=true else link_dest_exists=false fi fi # rsync options for element in $excludes do echo "excludes array contains $element" done if ! $link_dest_exists then OPT="-s -ah --delete ${excludes[@]}" else OPT="-s -ah --delete --link-dest=$LNK ${excludes[@]}" fi if $DRY_RUN then OPT="$OPT -n" fi if [[ $STORAGE_TYPE != "REMOTE" ]] then # -------- Run some tests ----- testtimestamp=$(date +%Y%m%d%H%M%S%N) test_filename="RIBS${testtimestamp}" test_linkname="RIBS${testtimestamp}hardlink" # Make sure the backup root is writeable echo -n "Testing backup location for writeability... " if ! touch "$BACKUP_ROOT"/"$test_filename" >/dev/null 2>&1 then echo -e "\e[31mFAILED\e[0m" echo "Aborting." if [[ $STORAGE_TYPE = "OFFLINE" ]] then if $REMOUNT_RO then mount_storage ro else mount_storage u fi fi exit 1 else echo -e "\e[32mOK\e[0m" fi # Make sure the backup root supports hard links echo -n "Testing backup location for hard link support... " if ! ln "$BACKUP_ROOT"/"$test_filename" "$BACKUP_ROOT"/"$test_linkname" >/dev/null 2>&1 then echo -e "\e[31mFAILED\e[0m" echo "Aborting." rm -f "$BACKUP_ROOT"/"$test_filename" if [[ $STORAGE_TYPE == "OFFLINE" ]] then if $REMOUNT_RO then mount_storage ro else mount_storage u fi fi exit 1 else echo -e "\e[32mOK\e[0m" fi rm -f "$BACKUP_ROOT"/"$test_filename" rm -f "$BACKUP_ROOT"/"$test_linkname" fi # -------- Start backup ------- # Create backup subdir if it's not there if [[ $STORAGE_TYPE == "REMOTE" ]] then if ! $DRY_RUN && [[ $BACKUP_SUBDIR != "" ]] && ! ssh "$REMOTE_USER_HOST" test -d "$BACKUP_ROOT$BACKUP_SUBDIR" then ssh "$REMOTE_USER_HOST" mkdir -p "$BACKUP_ROOT""$BACKUP_SUBDIR" fi else if ! $DRY_RUN && [[ $BACKUP_SUBDIR != "" ]] && ! test -d "$BACKUP_ROOT$BACKUP_SUBDIR" then mkdir -p "$BACKUP_ROOT""$BACKUP_SUBDIR" fi fi echo "Starting incremental backup." if $DRY_RUN then echo "This will be a dry run. Nothing will actually be backed up." sleep 10 fi # Execute the backup echo -n "running rsync backup with command rsync $OPT $SRC $TRG... " if ! bash -c "rsync $OPT $SRC $TRG" then echo -e "\e[31mFAILED\e[0m" echo "An error was encountered while attempting to backup. No old" echo "backups will be removed." if [[ $STORAGE_TYPE == "OFFLINE" ]] then if $REMOUNT_RO then mount_storage ro else mount_storage u fi fi exit 1 else echo -e "\e[32mOK\e[0m" fi # Delete the specified backups, if it exists limit_date=$(date -d "$current_datetime $DAY_LMT days ago") limit_date_sec=$(date -d "$limit_date" +%s) if $DRY_RUN then echo "This is a dry run, so no old backups will be removed." for date_dir in $(list_backups) do date_dir_sec=$(date -d "$date_dir" +%s) if [[ $date_dir_sec < $limit_date_sec ]] then echo "We would be removing $date_dir" fi done else for date_dir in $(list_backups) do date_dir_sec=$(date -d "$date_dir" +%s) if [[ $date_dir_sec < $limit_date_sec ]] then delete_backup $date_dir fi done fi # Clean up echo -n "Syncing disks... " if ! sync then echo -e "\e[31mFAILED\e[0m" else echo -e "\e[32mOK\e[0m" fi # Update capacity file if using offline storage if [[ $STORAGE_TYPE == "OFFLINE" ]] then echo -n "Updating capacity file... " if ! df -h |grep "$BACKUP_ROOT" > "$BACKUP_ROOT"/capacity.txt then echo -e "\e[31mFAILED\e[0m" else echo -e "\e[32mOK\e[0m" fi fi # Unmount or remount offline storage as required if [[ $STORAGE_TYPE == "OFFLINE" ]] then if $REMOUNT_RO then echo -n "Remounting backup storage read-only... " if ! mount_storage ro then echo -e "\e[31mFAILED\e[0m" else echo -e "\e[32mOK\e[0m" fi else echo -n "Unmounting backup storage... " if ! mount_storage u then echo -e "\e[31mFAILED\e[0m" else echo -e "\e[32mOK\e[0m" fi fi fi echo "Backup complete." echo "" echo "Run ended with configuration file:" echo "$CONF_FILE" date "+%a %b %d %H:%M:%S %Z %Y" echo "" } function show_help() { echo "Usage: $0 [-h|--help] [config_file]" echo "" echo "Run an incremental backup using a config file specified." echo "If no config file is specified, search for and read all config" echo "files in the user's config directory ($USER_CONF_DIR)." echo "If none are found there, search for and read all config files in" echo "the system config directory ($SYSTEM_CONF_DIR)." echo "" echo "Please refer to the sample config file, " echo "/etc/ribs/conf-available/ribs.conf.sample for an explanation" echo "of parameters." echo "" echo " Options:" echo "" echo " -h|--help: Display this help message." echo "" } function main() { # Begin execution echo "RIBS $RIBS_VERSION" echo "Rod's Incremental Backup System" echo "" case "$arg" in -h|--help) show_help exit 0 ;; *) find_configs "$arg" ;; esac for each_conf in "${conf_files[@]}" do run_backup "$each_conf" done } main "$@" exit 0