#!/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 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 . # 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 " # 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 []" 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 "$@"