Files
RIBS/dist/usr/local/bin/ribs

486 lines
13 KiB
Bash
Executable File

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