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

