486 lines
13 KiB
Bash
Executable File
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
|
|
|