aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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 "$@"