aboutsummaryrefslogtreecommitdiff
path: root/rsync-backup.sh
diff options
context:
space:
mode:
authorEinhard Leichtfuß <alguien@respiranto.de>2018-04-02 04:01:47 +0200
committerEinhard Leichtfuß <alguien@respiranto.de>2018-04-02 04:09:53 +0200
commit4a4cd8e85cb0851f5d7056098f5112b9016794ad (patch)
tree88fa3142163f6293903d91ce2b3d978b1a5cf2fd /rsync-backup.sh
Initial commit
Diffstat (limited to 'rsync-backup.sh')
-rwxr-xr-xrsync-backup.sh296
1 files changed, 296 insertions, 0 deletions
diff --git a/rsync-backup.sh b/rsync-backup.sh
new file mode 100755
index 0000000..3a592d7
--- /dev/null
+++ b/rsync-backup.sh
@@ -0,0 +1,296 @@
+#!/bin/bash
+#
+# rsync-backup.sh - a backup script using rsync.
+#
+# Copyright 2017, 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 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 <http://www.gnu.org/licenses/>.
+#
+
+RSYNC=rsync
+
+typeset -a rsync_args
+typeset -a rsync_inc_args
+typeset -A bakpath
+typeset filter_file_all
+typeset -A filter_file
+
+# Default arguments for rsync.
+rsync_args=(
+ --recursive
+ --perms --times --owner --group
+ #--acls --xattrs
+ --links
+ --hard-links
+ --sparse
+ --timeout=60
+ -vv
+ )
+
+# 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_all -a -f "$filter_file_all"
+ then
+ filter_args=("--filter=merge ${filter_file_all}")
+ fi
+ if test -v filter_file[$dirpath] && test -f "${filter_file[$dirpath]}"
+ then
+ filter_args+=("--filter=merge ${filter_file[$dirpath]}")
+ 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_n_zeros=$((${#n} - 1))
+ i=1
+ while [ $i -le $max_n_zeros ]
+ do
+ zeros[i]=$(printf '%.s0' {$i..$max_n_zeros})
+ i+=1
+ done
+
+ i=0
+ while [ $i -le $n ]
+ do
+ ln -s "../${dirs[i]}" "$numdir/${zeros[${#i}]}$i"
+ i+=1
+ done
+}
+
+
+main "$@"