#!/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 (-rptoglHS, --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_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 "$@"