aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEinhard Leichtfuß <alguien@respiranto.de>2019-04-04 00:04:37 +0200
committerEinhard Leichtfuß <alguien@respiranto.de>2019-04-04 00:04:37 +0200
commitab0cff3bb36721e982301e14435c0f4c8f313995 (patch)
tree44702cebd2019a383e574db3f1ffbd94db670092
parent7831ff7b87b2a100ec04611eadefa0a9370f5057 (diff)
Add remote backup functionality
Create wrapper functions that take a host and a function with named arguments. The supplied function is the to be executed on the specified host, which may be the empty string signifying local execution. These wrapper functions essentially export the to be run function and any of its recursive dependencies, further the named arguments as variables, to the remote host, and run the function remotely. That is, if the host is non-local. Also, - use appropriate function names to differentiate between destination and source host wherever applicable. - add comments, - enhance TODO and README, - rename some variables in order to increase consistency, - move some global declarations further upward, - make variables readonly when reasonable, - use named return and exit codes, - add a general `ask'-function.
-rw-r--r--README10
-rw-r--r--TODO45
-rwxr-xr-xrsync-backup657
-rwxr-xr-xrsync-backup.sh299
4 files changed, 694 insertions, 317 deletions
diff --git a/README b/README
index 062c63e..1287b72 100644
--- a/README
+++ b/README
@@ -4,8 +4,14 @@
Dependencies
------------
+Most dependencies are required locally and remotely, as a huge part of the
+script may be executed remotely.
+
- bash>=4.2 (test -v)
-- ...
+- rsync
+- ssh
+- coreutils (realpath, date)
+- find
Notes
@@ -13,3 +19,5 @@ Notes
Symbolic links are simply copied, thus linking to the backed-up filetree, not
the backup-filetree itself.
+
+Dates are always calculated on the local host.
diff --git a/TODO b/TODO
index 3d0bfb6..9198ac4 100644
--- a/TODO
+++ b/TODO
@@ -1,27 +1,37 @@
+# -- BUGS -- #
+- $1 == /path/, where /path is configured behaves strangely.
+ `- $1 == /path is fine.
+ `- A backup is created in $PWD/
+
# -- TODO -- #
-- Make both remote source and destination possible.
- `- Therefore, one could analyze the respective path strings.
- `- Use ssh.
- `- Set up one single connection (maybe configurable?).
- `- ssh -oControlPath=/tmp/root-readable/something.sock \
- -oControlMaster=yes \
- -oControlPersist $remote /bin/true
- `- ssh -oControlPath=/tmp/root-readable/something.sock
+- For ssh, set up one single connection (maybe configurable?).
+ `- ssh -oControlPath=/tmp/root-readable/something.sock \
+ -oControlMaster=yes \
+ -oControlPersist $remote /bin/true
+ `- ssh -oControlPath=/tmp/root-readable/something.sock
- Replace echo by printf(-functions).
- Set up default configuration file.
-- When installing, copy the config file to both /etc and /usr/share,
- such that sourcing from the latter location removes the need to specify
- default options in the script itself.
+- [consider] When installing, copy the config file to both /etc and
+ /usr/share, such that sourcing from the latter location removes the need to
+ specify default options in the script itself.
- Test for read/write access at some point.
- Better error reporting.
-- Write an install script or such.
-- Delete 'too new' symlinks in by_number.
+- Use autotools.
- Per dirpath rsync_args.
-- Allow resuming of backups.
- `- See rsync's `--ignore-existing' flag
+- Allow for resuming of backups.
+ `- See rsync flags
+ `- --ignore-existing
+ `- Not a good idea if a notable amount of time passed.
+ `- [question] What happens in case of partially transferred files?
+ `- --delete-during, --delete-excluded
- Shell completion of configured backup paths.
-- Use `local'.
- Better error handling on failure (of rsync).
+- Use ask().
+ `- Allow for configuration of default answer.
+- [consider] Create subdirectories per year and/or month.
+- [consider] Get date on source host.
+- Named backups to allow for different destinations per source (and shorter
+ names).
# -- PROBLEMS -- #
- rsync bug: https://bugzilla.samba.org/show_bug.cgi?id=13445
@@ -40,7 +50,6 @@
`- Should return errors if not possible.
`- One could offer an exit option to the user
`- or auto fix using sudo.
-- Commands like mkdir can fail! - e.g. due to missing permissions.
# -- IDEAS -- #
- colorized Output.
@@ -48,3 +57,5 @@
- Use Hexadecimal numbers for by_number.
- Verbosity option.
`- For now, -v or -vv in rsync_args should work fine.
+- non-intercative option.
+- rsync_network_options (when remote src or dest, e.g. for --compress).
diff --git a/rsync-backup b/rsync-backup
new file mode 100755
index 0000000..e0ed37a
--- /dev/null
+++ b/rsync-backup
@@ -0,0 +1,657 @@
+#!/usr/bin/env bash
+#
+# rsync-backup - a backup script using rsync.
+#
+# Copyright 2015 - 2018 Einhard Leichtfuß
+#
+# This file is part of rsync-backup.
+#
+# rsync-backup is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# rsync-backup is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with rsync-backup. If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+typeset -r exec_name="${0##*/}"
+
+
+# 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='nullglob dotglob'
+
+
+# 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 $?
+
+ # 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
+#
+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]
+ 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
+}
+
+
+main "$@"
diff --git a/rsync-backup.sh b/rsync-backup.sh
deleted file mode 100755
index 0c48018..0000000
--- a/rsync-backup.sh
+++ /dev/null
@@ -1,299 +0,0 @@
-#!/bin/bash
-#
-# rsync-backup.sh - a backup script using rsync.
-#
-# Copyright 2015 - 2018 Einhard Leichtfuß
-#
-# This file is part of rsync-backup.
-#
-# rsync-backup is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# rsync-backup is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with rsync-backup. If not, see <https://www.gnu.org/licenses/>.
-#
-
-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
-
-config=/etc/rsync-backup/backup.conf
-source "$config"
-
-src=/home/respiranto/txt/bash/rsync-backup/src
-dest=/home/respiranto/txt/bash/rsync-backup/dest
-
-typeset -l reply
-typeset -i i n
-
-# 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
-{
- # In case of a non-matching wildcard, return nothing.
- shopt -s nullglob
- # Include dotfiles (except . and ..) in regex matching.
- shopt -s dotglob
-
- get_args "$@" || return $?
-
- check_paths || return $?
-
- # Change the separation char to $'\n' to avoid confusion with filenames
- # containing spaces (default is $' \t\n').
- IFS=$'\n'
-
- find_existing_backup_dirs || return $?
-
- perform_backup || return $?
-
- refresh_symlinks || return $?
-}
-
-
-function get_args
-{
- if [ $# -eq 0 ]
- then
- echo "Usage: $0 <src> [<dest>]"
- return 1
- fi
-
- dirpath="$(realpath "$1")"
-
- if [ $# -gt 1 ]
- then
- bakpath="$(realpath $2)"
- elif test -v bakpath[$dirpath]
- then
- bakpath="${bakpath[$dirpath]}"
- else
- echo "Error: Backup path for $dirpath not configured."
- return 1
- fi
-
- filter_args=()
- if test -v filter_file[$dirpath] && test -f "${filter_file[$dirpath]}"
- then
- filter_args+=("--filter=merge ${filter_file[$dirpath]}")
- fi
- if test -v filter_file_all -a -f "$filter_file_all"
- then
- filter_args+=("--filter=merge ${filter_file_all}")
- fi
-}
-
-
-function check_paths
-{
- # Test whether the dirs exist already.
- if ! test -d "$dirpath"
- then
- echo "Source (\"$dirpath\") does not exist."
- return 1
- fi
-
- if ! test -d "$bakpath"
- then
- echo "Target (\"$bakpath\") does not exist."
- echo -n "Do you want do create it [y/N]? "
- read reply
- if [[ "$reply" != "y" && "$reply" != "yes" ]]
- then
- return 1
- fi
-
- if ! mkdir -p "$bakpath"
- then
- echo "Backup creation has failed."
- echo "\"$bakpath\" could not be created."
- return 1
- fi
- fi
-}
-
-
-function find_existing_backup_dirs
-{
- n=0
- for file in `ls "$bakpath"`; do
- if test -d $bakpath/$file \
- && [[ "$file" =~ ^[0-9]{4}-[0-1][0-9]-[0-3][0-9]_[0-2][0-9]([0-5][0-9]){2}.backup$ ]]
- then
- dirs[n]="$file"
- n+=1
- fi
- done
-}
-
-
-# $1 path
-# $2 error message: string
-function clear_file
-{
- if test -e "$1" -o -L "$1"; then
- echo "It seems like the last backup process failed or you have created" \
- "the file \"$1\" manually."
- echo -n "Do you want to delete it [y/N]? "
- read reply
- if [[ "$reply" == "y" || "$reply" == "yes" ]]
- then
- printf "Deleting"
- if rm -rf "$1"; then
- printf " - done\n"
- return 0
- else
- printf '\n%s\n' "$2"
- printf 'The file \"%s\" could not be deleted.\n' "$1"
- return 1
- fi
- else
- printf '%s\n' "$2"
- printf '%s %s\n' "Please remove the above mentioned file manually" \
- "or run this script once more."
- return 1
- fi
- fi
- return 0
-}
-
-
-function perform_backup
-{
- bakdate=`date +%Y-%m-%d_%H%M%S.backup`
- finaldir="$bakpath/$bakdate"
- tempdir="$bakpath/new.backup"
- clear_file "$tempdir" "Backup creation has failed." || return 1
- clear_file "$finaldir" "Backup creation has failed." || return 1
-
- # note: $n == ${#dirs[@]}
- printf 'Starting backup nr. %u.\n' "$n"
- if [ $n -eq 0 ]
- then
- # Perform first backup, not incremental.
- $RSYNC "${rsync_args[@]}" \
- "${filter_args[@]}" \
- "$dirpath"/ "$tempdir" \
- || return 1
- else
- # Perform incremental backup on the basis of the last.
- $RSYNC "${rsync_args[@]}" \
- "${rsync_inc_args[@]}" --link-dest="${bakpath}/${dirs[n-1]}" \
- "${filter_args[@]}" \
- "$dirpath"/ "$tempdir" \
- || return 1
- fi
-
- mv "$tempdir" "$finaldir" || return 1
- printf '\nCreating of backup nr. %u succesfully executed.\n' "$n"
-}
-
-
-function refresh_symlinks
-{
- linkdir="$bakpath/latest"
- if test -L "$linkdir"
- then
- rm "$linkdir"
- ln -s "$bakdate" "$linkdir"
- elif clear_file "$linkdir" "Creation of softlink \"$linkdir\" failed."
- then
- ln -s "$bakdate" "$linkdir"
- fi
-
- # by_number
- numdir="$bakpath/by_number"
- if ! test -d "$numdir"
- then
- if clear_file "$numdir" "Creating directory $numdir failed."
- then
- mkdir "$numdir"
- else
- return 1
- fi
- fi
-
- for file in "$numdir"/*
- do
- if test -L "$file"
- then
- rm "$file"
- else
- if ! clear_file "$file" "Setting up numbered softlinks failed."
- then
- return 1
- fi
- fi
- done
-
- dirs[$n]="$bakdate" # Set last dir on current.
-
- max_digits=${#n}
- zeros[max_digits]=''
- i=$max_digits-1
- while [ $i -ge 1 ]
- do
- zeros[i]=${zeros[i+1]}0
- i+=-1
- done
-
- i=0
- while [ $i -le $n ]
- do
- ln -s "../${dirs[i]}" "$numdir/${zeros[${#i}]}$i"
- i+=1
- done
-}
-
-
-main "$@"