diff options
-rw-r--r-- | README | 10 | ||||
-rw-r--r-- | TODO | 45 | ||||
-rwxr-xr-x | rsync-backup | 657 | ||||
-rwxr-xr-x | rsync-backup.sh | 299 |
4 files changed, 694 insertions, 317 deletions
@@ -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. @@ -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 "$@" |