aboutsummaryrefslogtreecommitdiff
path: root/rsync-backup
diff options
context:
space:
mode:
Diffstat (limited to 'rsync-backup')
-rwxr-xr-xrsync-backup648
1 files changed, 14 insertions, 634 deletions
diff --git a/rsync-backup b/rsync-backup
index 0a7df33..86bff28 100755
--- a/rsync-backup
+++ b/rsync-backup
@@ -21,642 +21,22 @@
#
-typeset -r exec_name="${0##*/}"
+typeset -r full_exec_name="$(realpath "$0")"
+typeset -r exec_name="${full_exec_name##*/}"
+typeset -r source_code_dir="${full_exec_name%/*}"
+cd "$source_code_dir"
-# CONSTANTS
-typeset -r CONFIG=/etc/rsync-backup/config
-typeset -r RET_TYPES='
- RET_SUCCESS RET_FAILURE RET_BADSYNTAX RET_FILE_ERR RET_ERROR'
-typeset -r RET_SUCCESS=0
-typeset -r RET_FAILURE=1
-typeset -r RET_BADSYNTAX=2
-typeset -r RET_FILE_ERR=4
-typeset -r RET_ERROR=8
-typeset -r DATEFMT='%Y-%m-%d_%H%M%S'
-
-# Shell options:
-# nullglob: In case of a non-matching glob, return nothing.
-# dotglob: Include dotfiles (except . and ..) in glob matching.
-typeset -r SHOPTS='dotglob'
-typeset -r SHOPTS_AFTER_GET_ARGS='nullglob'
-
-
-# Global program variables.
-typeset src src_path src_host
-typeset dest dest_path dest_host
-typeset -a filter_args
-typeset -i n
-typeset bakdate
-
-
-# Variables that may be configured.
-typeset RSYNC=rsync
-typeset -a rsync_args
-typeset -a rsync_inc_args
-typeset -A bakpath
-typeset filter_file_all # Applied after specific filter.
-typeset -A filter_file
-
-# Default arguments for rsync (-rptogAXlHS, --timeout=, --info=).
-rsync_args=(
- --recursive
- --perms --times --owner --group
- --acls --xattrs
- --links
- --hard-links
- --sparse
- --timeout=60
- #-vv
- --info=progress2
- )
-
-# Supplemental arguments for rsync; only used when an actual incremental
-# update is performed, i.e. when --link-dest is used.
-rsync_inc_args=(
- --fuzzy --fuzzy
- )
-
-filter_file_all=/etc/rsync-backup/filter
-
-source "$CONFIG"
-
-
-# RSYNC args.
-# basic args: --recursive --perms --times --owner --group --links
-# extra args: --sparse --acls --xattrs --hard-links --timeout=60
-# --fuzzy --fuzzy
-# --filter="merge <file>"
-# likely: --compress
-# --partial{,dir=DIR}
-# --progress
-# possibly: --devices --specials
-# --max-size=SIZE-OF-FILE
-# --one-file-system
-# --log-file=FILE --log-file-format=FORMAT
-# --human-readable
-# unlikely: --omit-{dir,link}-times --update (!--inplace) --delete
-# --exclude
-# great: --link-dest=DIR (timestamps?)
-# testing: --verbose --dry-run (-vn)
-# interesting: --sockopts --itemize-changes --out-format=FORMAT
-# --stats
-
-function main
-{
- shopt -s $SHOPTS
-
- # Set $src, $dest_path and $filter_args.
- get_args "$@" || return $?
-
- shopt -s $SHOPTS_AFTER_GET_ARGS
-
- # Verify $src_path exists.
- run_function "$src_host" src_check_path '' 'src_path src' || return $?
-
- # Make sure $dest_path exists.
- run_function "$dest_host" dest_check_path '' 'dest_path dest' || return $?
-
- # Populate the global $dirs array.
- dirs=( $(
- run_function "$dest_host" dest_find_existing_backup_dirs '' 'dest_path' \
- || return $?
- ) )
-
- n=${#dirs[@]}
-
- run_function "$dest_host" dest_prepare 'dest_clear_file' \
- 'dest dirs[@] n dest_host dest_path' "$(date +$DATEFMT)" \
- || return $?
-
- perform_backup || return $?
-
- run_function "$dest_host" dest_finalize \
- 'dest_refresh_symlinks dest_clear_file' \
- 'dest_host dest_path bakdate n' \
- || return $?
-
- dest_refresh_symlinks
-
- return $RET_SUCCESS
-}
-
-
-# Set the globabl variables $src, $dest_path and $filter_args according to
-# command line parameters and the configuration.
-#
-# $@: command line parameters
-#
-# requires the shell option nullglob to be _unset_. (for test -v to work)
-#
-function get_args
-{
- if [ $# -eq 0 ]
- then
- echo "Usage: $exec_name <src> [<dest>]"
- return $RET_BADSYNTAX
- fi
-
- local l_src="$1"
- local l_dest
-
- # Set $src_path, $src_host and $src.
- resolve_host_path src "$l_src" || return $?
-
- if [ $# -gt 1 ]
- then
- l_dest="$2"
- elif test -v bakpath[$src] # $# -eq 1
- then
- l_dest="${bakpath[$src]}"
- else
- echo "Error: Backup path for $src neither configured"
- echo " nor specified on command line."
-
- return $RET_BADSYNTAX
- fi
-
- # Set $dest_path, $dest_host and $dest.
- resolve_host_path dest "$l_dest" || return $?
-
- filter_args=()
- if test -v filter_file[$src] && test -f "${filter_file[$src]}"
- then
- filter_args+=("--filter=merge ${filter_file[$src]}")
- fi
- if test -v filter_file_all -a -f "$filter_file_all"
- then
- filter_args+=("--filter=merge ${filter_file_all}")
- fi
-
- return $RET_SUCCESS
-}
-
-
-# Split host:target like rsync and convert local (!) relative paths to
-# absolute ones. This is needed at least for $src_path which is used as
-# key for associative arrays.
-#
-# The results are written to the global variables $<vtype>, $<vtype>_host
-# and $<vtype>_path, where <vtype> is either src or dest. $<vtype> gets
-# set to the original to be resolved string, unless we are local, i.e.
-# there is no host.
-#
-# Note: This is not a place for bash to shine. Perl for instance would do
-# this much more easily.
-#
-# Examples:
-# - a:b -> (a, b)
-# - a/b:c -> ('', a/b:c)
-#
-# $1: which global variables to set: (src|dest)
-# $2: path string potentially containing host prefix
-#
-# sets: ${$1}_host: host name (may be empty)
-# ${$1}_path: path (absolute, if local, i.e. ${$1}_host='')
-# ${$1}: ${$1}_path, if ${$1}_host='',
-# ${$1}_host:${$1}_path (= $2), elsewise.
-#
-function resolve_host_path
-{
- local vtype="$1"
- local combined="$2"
- local host path
-
- # Split host and path.
- host=${combined%%:*}
- if [[ "$host" == "$combined" || "$host" =~ '/' ]]
- then
- host=''
- path="$combined"
-
- # Resolve to absolute directory.
- # pwd -P avoids symlinks.
- # Do not resolve symlimks in ${path} or test for its existance.
- # `- realpath could do that.
- if ! [[ "$path" =~ / ]]
- then
- path="$(pwd -P)/${path}"
- fi
-
- combined="$path"
- else
- path="${combined#*:}"
- fi
-
- # Set global variables.
- if [[ "$vtype" == 'src' ]]
- then
- src_host="$host"
- src_path="$path"
- src="$combined"
- else # "$vtype" == 'dest'
- dest_host="$host"
- dest_path="$path"
- dest="$combined"
- fi
-
- return $RET_SUCCESS
-}
-
-
-# Run a function on a remote host.
-# If the host parameter is empty, run locally.
-#
-# $1: host: name
-# $2: function to run: name
-# $3: other functions to export: [name (. ' ' . name)*]
-# $4: variables to export: [name (. '[@]')? (. ' ' . name (. '[@]')?)*]
-# ${@:5}: function arguments: string+
-#
-function run_function
-{
- local host="$1"
- local fun=$2
- local funs="$fun $3"
- local vars="$4"
- local vars_set
-
- shift 4
-
- if [[ -z "$host" ]]
- then
- $fun "$@"
- else
- # Create string to export variables.
- vars_set=
- for var in $RET_TYPES $vars
- do
- vars_set+="$(eval echo \${${var}@A}); "
- done
-
- # Use ssh to run command remotely.
- # Make sure to properly escape the strings.
- ssh "$host" "shopt -s ${SHOPTS}; ${vars_set} $(typeset -f $funs);" \
- "$fun $(printf "'%s' " "$@")"
- fi
-
- return $?
-}
-
-
-# Run command remotely.
-#
-# Not used.
-#
-# $1: host: name
-# ${@:2}: command with arguments: string+
-#
-function do_thing
-{
- local host="$1"
- shift
-
- if [[ -z "$host" ]]
- then
- "$@"
- else
- # Use ssh to run command remotely.
- # Make sure to properly escape the strings.
- ssh "$host" "shopt -s ${SHOPTS}; $(printf "'%s' " "$@")"
- fi
-
- return $?
-}
-
-
-# Check if $src exists.
-# Must be run on source host.
-#
-# requires: $src_path, $src
-#
-function src_check_path
-{
- if ! test -d "$src_path"
- then
- echo "Source (\"$src\") does not exist."
- return $RET_FILE_ERR
- fi
-
- return $RET_SUCCESS
-}
-
-
-# Check if $dest_path exists and try to create it if necessary.
-# Must be run on destination host.
-#
-# requires: $dest_path, $dest
-# interactive.
-#
-function dest_check_path
-{
- local -l reply
-
- if ! test -d "$dest_path"
- then
- echo "Target (\"$dest\") does not exist."
- echo -n "Do you want do create it [y/N]? "
- read reply
- if ! [[ "$reply" =~ ^y(es)?$ ]]
- then
- return $RET_FAILURE
- fi
- echo
-
- if ! mkdir -p "$dest_path"
- then
- echo "Backup creation has failed."
- echo "\"$dest\" could not be created."
- return $RET_FILE_ERR
- fi
- fi
-
- return $RET_SUCCESS
-}
-
-
-# Print existing backup directories.
-# Must be run on destination host.
-#
-# requires: $dest_path
-#
-function dest_find_existing_backup_dirs
-{
- find "$dest_path" -mindepth 1 -maxdepth 1 -type d \
- -regextype posix-extended -regex \
- '.*/[0-9]{4}-[0-1][0-9]-[0-3][0-9]_[0-2][0-9]([0-5][0-9]){2}.backup' \
- -printf '%f\n' \
- | LC_ALL=C sort
-
- if [[ ${PIPESTATUS[0]} -eq 0 && ${PIPESTATUS[1]} -eq 0 ]]
- then
- return $RET_SUCCESS
- else
- return $RET_ERROR
- fi
-}
-
-
-# Prepare destination for the backup process.
-# Must be run on destination host.
-#
-# requires: $dirs[@], $n
-# $dest_host (self, <- dest_clear_file)
-# $dest_path
-#
-# $1: current date on local host in format $DATEFMT
-#
-# interactive (<- dest_clear_file)
-#
-function dest_prepare
-{
- local current_date="$1"
-
- # Check for dates in the future, which might mess with the yet to be
- # determined new backup's date.
- if [[ $n -gt 0 ]]
- then
- local last_date=${dirs[n-1]%.backup}
-
- if [[ "$current_date" == "$last_date" ]]
- then
- echo "Last backup was run in the same second."
- echo "Exiting."
- return $RET_FAILURE
- elif
- [[ "$current_date" < "$last_date" ]]
- then
- echo "Last backup lies in the future. Fix your clocks."
- echo "Exiting."
- return $RET_FAILURE
- fi
- fi
-
- dest_clear_file "${dest_path}/new.backup" "Backup creation has failed." \
- || return $?
-
- return $RET_SUCCESS
-}
-
-
-# Perform the actual backup.
-# To be run locally.
-#
-# requires: $DATEFMT,
-# $dest_path, $dest, $src_path, $src,
-# $n, $dirs[@],
-# $rsync_args[@], $rsync_inc_args[@], $filter_args[@]
-#
-# sets: $bakdate
-#
-function perform_backup
-{
- bakdate=`date +"$DATEFMT"`
-
- printf 'Starting backup nr. %u.\n' "$n"
- if [ $n -eq 0 ]
- then
- # Perform first backup, not incremental.
- $RSYNC "${rsync_args[@]}" \
- "${filter_args[@]}" \
- "$src"/ "${dest}/new.backup" \
- || return $RET_ERROR
- else
- # Perform incremental backup on the basis of the last.
- $RSYNC "${rsync_args[@]}" \
- "${rsync_inc_args[@]}" --link-dest="../${dirs[n-1]}" \
- "${filter_args[@]}" \
- "$src"/ "${dest}/new.backup" \
- || return $RET_ERROR
- fi
-
- return $RET_SUCCESS
-}
-
-
-# Finalize the backup creation on the destination host.
-#
-# requires: $dest_host (<- dest_refresh_symlinks),
-# $dest_path (self, <- dest_refresh_symlinks),
-# $bakdate (self, <- dest_refresh_symlinks),
-# $n (self, <- dest_refresh_symlinks),
-#
-# interactive (<- dest_refresh_symlinks)
-#
-function dest_finalize
-{
- mv "${dest_path}/new.backup" "${dest_path}/${bakdate}.backup" \
- || return $RET_FILE_ERR
-
- if
- dest_refresh_symlinks
- then
- printf '\nCreating of backup nr. %u successfully executed.\n' "$n"
- return $RET_SUCCESS
- else
- local ret=$?
- printf '\nCreating of backup nonetheless successfully executed.\n'
- return $ret
- fi
-}
-
-
-# Create symbolic links to backup directories.
-# Must be run on the destination host.
-#
-# requires: $dest_host, $dest_path, $bakdate, $n
-# interactive (<- dest_clear_file)
-#
-function dest_refresh_symlinks
-{
- local -i i
- local host_prefix="${dest_host}${dest_host:+:}"
- local linkdir ret
-
- linkdir="${dest_path}/latest"
- if test -L "$linkdir"
- then
- rm "$linkdir"
- ln -s "${bakdate}.backup" "$linkdir" || return $RET_ERROR
- elif
- dest_clear_file "$linkdir" "Creation of softlink \"latest\" failed." \
- || return $?
- then
- ln -s "${bakdate}.backup" "$linkdir" || return $RET_ERROR
- fi
-
- # by_number
- numdir="${dest_path}/by_number"
- if ! test -d "$numdir"
- then
- dest_clear_file "$dest_host" "$numdir" \
- "Setting up numbered softlinks failed." || return $?
-
- mkdir "$numdir" || return $RET_ERROR
- fi
-
- # Remove old symlinks.
- find "$numdir" -mindepth 1 -maxdepth 1 -type l -regextype posix-extended \
- -regex '.*/[0-9]*' -delete || return $RET_ERROR
-
- # TODO: Simplify.
- for file in "$numdir"/*
- do
- echo $file
- dest_clear_file "$file" "Setting up numbered softlinks failed." \
- || return $?
- done
-
- # Set last dir on the new one.
- dirs[$n]="${bakdate}.backup"
-
- for (( i = 0; i <= n; i++ ))
- do
- ln -s "../${dirs[i]}" "$(printf "%s/%.${#n}u" "$numdir" $i)"
- done
-
- return $RET_SUCCESS
-}
-
-
-# Make sure a file does not exist on the destination host.
-# Must be run on destination host.
-#
-# $1: file: path
-# $2: error message: string
-#
-# requires: $dest_host
-# interactive.
-#
-function dest_clear_file
-{
- local file_path="$1"
- local err_msg="$2"
- local file="${dest_host}${dest_host:+:}${file_path}"
-
- local -l reply
-
- if test -e "$file_path" -o -L "$file_path"
- then
- printf "%s %s\n %s\n%s\n" \
- "It seems like the last backup process failed or you have created" \
- "the file" "$file" "manually."
-
- echo -n "Do you want to delete it [y/N]? "
- read reply
-
- if [[ "$reply" =~ ^y(es)?$ ]]
- then
- printf "Deleting.\n"
-
- if rm -rf "$file_path"
- then
- return $RET_SUCCESS
- else
- printf '%s\n' "$err_msg"
- return $RET_FILE_ERR
- fi
- else
- printf '%s\n' "$err_msg"
- return $RET_FAILURE
- fi
- fi
-
- return $RET_SUCCESS
-}
-
-
-# Ask a yes/no question.
-# Note, that ^D is considered as a default answer.
-#
-# Not used.
-#
-# $1 default answer: (y|n|x)
-# $2 question: string
-#
-# returns yes / no: (0|1)
-#
-# interactive.
-#
-function ask
-{
- # -l auto-converts everything to lower case.
- local -l default="$1"
- local question="$2"
-
- while true
- do
- if [[ "$default" == "y" ]]
- then
- printf "%s [Y/n]? " "$question"
- elif [[ "$default" == "n" ]]
- then
- printf "%s [y/N]? " "$question"
- else
- printf "%s [y/n]? " "$question"
- default="x"
- fi
-
- local -l reply # -l converts to lowercase
- read reply
-
- if [[ "$reply" =~ ^y(es)?$ ]]
- then
- return 0
- elif [[ "$reply" =~ ^no?$ ]]
- then
- return 1
- elif [[ "$default" == "x" || "$reply" != "" ]]
- then
- continue
- elif [[ "$default" == "y" ]]
- then
- return 0
- else # "$default" == "n"
- return 1
- fi
- done
-}
+# Include the other files.
+. ./variables
+. ./files
+. ./write
+. ./remote
+. ./local
+. ./source
+. ./destination
main "$@"
+
+# vi: ft=bash ts=2 sw=2 noet