#!/bin/sh set -eu umask 077 ROTATED=30 CONFDIR="/etc/rsync-backup" DESTDIR="/srv/backup" LOGDIR="/var/log/rsync-backup" RUNDIR="/var/run/rsync-backup" find_rotated() { # sort dailys from oldest to newest, daily.7 daily.6 daily.5 ... find "$1" -mindepth 1 -maxdepth 1 -type d -name "daily.*" | sort -V -r } rotate_dirs() { for host in "$@"; do # rotate dailys starting from oldest if [ ! -d "${DESTDIR}/${host}" ]; then continue fi find_rotated "${DESTDIR}/${host}" | while read -r dir; do ext="${dir##*.}" next="${dir%.*}.$((ext+1))" mv "$dir" "$next" done done # compress logs over 1 day old find "$LOGDIR" -type f -name '*.log' -mtime +1 -execdir gzip -f {} ';' } prune_dirs() { for host in "$@"; do # remove oldest dailys find_rotated "${DESTDIR}/${host}" | while read -r dir ; do num="$(basename "$dir" | sed -e 's/^daily.//')" if [ "$num" -gt $ROTATED ]; then rm -rf "$dir" fi done done # remove logs over ROTATED*2 days old find "$LOGDIR" -type f -name '*.log.gz' -mtime +$((ROTATED*2)) -delete } rsync_pull() { dirs="" opts="" host="$1" conf="${CONFDIR}/${host}.conf" if [ -s "$conf" ] && [ -x "$conf" ]; then # shellcheck source=/dev/null . "$conf" || return else echo "skipped: ${1}" 1>&2 return fi lockdir="${RUNDIR}/${host}.lock" mkdir -m 0755 "$lockdir" || return if [ "$host" = "$(hostname)" ]; then # skip ssh for localhost set -- $dirs else set -- $(for d in $dirs; do echo "${host}:${d}" ; done) fi base="${DESTDIR}/${host}" if [ ! -d "$base" ]; then mkdir -m 0700 "$base" || return fi dest="${base}/daily.0" last="${base}/daily.1" if [ ! -d "$dest" ]; then mkdir -m 0700 "$dest" || return fi if [ -d "$last" ]; then # hardlink unchanged files to previous daily opts="--ignore-existing --link-dest=${last}" fi logfile="${LOGDIR}/${host}.$(date +%Y%m%d-%H%M%S).log" if ! /usr/local/bin/rsync \ -e "ssh -o BatchMode=yes -i ${CONFDIR}/id_ed25519" \ -Raqxz --no-devices $opts \ --log-file="$logfile" \ "$@" "$dest" then echo "rsync log: ${logfile}" 1>&2 fi rmdir "$lockdir" } if [ ! -d "$DESTDIR" ]; then echo "ERROR: ${DESTDIR} does not exist" 1>&2 exit 1 fi if [ ! -d "$LOGDIR" ]; then echo "ERROR: ${LOGDIR} does not exist" 1>&2 exit 1 fi if [ ! -d "$RUNDIR" ]; then mkdir -m 0755 "$RUNDIR" fi ALL=false PRUNE=false ROTATE=false while getopts "apr" OPT; do case "$OPT" in a) ALL=true ;; p) PRUNE=true ;; r) ROTATE=true ;; *) echo "Usage: $(basename "$0") [-apr] [host ...]" 1>&2 exit 1 ;; esac done shift $((OPTIND-1)) mkdir -m 0755 "${RUNDIR}/daily.lock" trap 'rmdir "${RUNDIR}/daily.lock"' EXIT if [ $ALL ]; then for conf in "${CONFDIR}"/*.conf ; do host="$(basename "$conf" ".conf")" set -- "$host" "$@" done fi $ROTATE && rotate_dirs "$@" for host in "$@" ; do rsync_pull "$host" done $PRUNE && prune_dirs "$@"