#!/bin/bash

# TODO make some way of having this script communicate with scripts on the backup server to handle compression
# and pruning on the local machine rather than over an ssh connection. Do this in a non votile way

# stop on errors
set -e -o pipefail

# Where logs are kept
logfolder="/tmp/logs"
# How long to keep backups in destination until they are deleted
retention=30
# The log name for this execution of the script
logfile="$logfolder/log-$(date +"%Y-%m-%dT%H:%M:%S%z").log"
# Start time of script
start=$(date +%s.%N)

# Prints message to stdout and appends to log file
# Takes message as argument
message() { echo "$1" | tee -a "$logfile"; }

# Depending on use flags prune either remotely or locally and prune either directories or archives
prune() { \
	# Check if user specified prune
	if [ -n "${prune+x}" ]; then
		message "Pruning files..."
		# if user is backing up locally and specified some sort of compression
		if [ -n "${compress_diff+x}" ] || [ -n "${compress+x}" ] || [ -n "${encryptor_name}" ] && [ -n "${local_backup+x}" ]; then
			message "Pruning old files on local ${dst} locally (compressed)"
			# look for files that end in compression extension or gpg extension and delete of over retention days
			find "${dst}" -mindepth 1 -maxdepth 2 -name '*.tar.gz' -mtime +${retention} -o -name "*.tar.gz.gpg" -mtime +${retention} -not -path "${dst}/full/*" -exec rm -rfv {} \; 2>&1 | tee -a "$logfile"
		# if user is backing to remote server and specified some sort of compression
		elif [ -n "${compress_diff+x}" ] || [ -n "${compress+x}" ] || [ -n "${encryptor_name}" ]; then
			# find command same as above but does not use -exec because that does not work over ssh
			remote_command="find ${dst#*:} -mindepth 1 -maxdepth 2 -name '*.tar.gz' -mtime +${retention} -o -name '*.tar.gz.gpg' -mtime +${retention} -not -path ${dst#*:}/full/* | xargs rm -rfv"
			message "Pruning old files on ${dst%%:*} located at ${dst#*:} (compressed)"
			# run the command over ssh
			ssh "${dst%%:*}" "eval ${remote_command}" 2>&1 | tee -a "$logfile"
		# if user is backing up locally and did not specify and compression
		elif [ -n "${local_backup+x}" ]; then
			message "Pruning old files on local ${dst} locally (not compressed)"
			# look for directiories over retention
			find "${dst}" -mindepth 1 -maxdepth 1 -type d -mtime +${retention} -not -path "${dst}/full" -exec rm -rfv {} \; 2>&1 | tee -a "$logfile"
		# must be backing up remotley and did not specify any compression
		else
			# look for directiories over retention
			remote_command="find ${dst#*:} -mindepth 1 -maxdepth 1 -type d -mtime +${retention} -not -path ${dst#*:}/full | xargs rm -rfv"
			message "Pruning old files on ${dst%%:*} located at ${dst#*:} (not compressed)"
			# run the command over ssh
			ssh "${dst%%:*}" "eval ${remote_command}" 2>&1 | tee -a "$logfile"
		fi
	fi ;}

# Run ending messages and end with success
end_backup() { \
	message "Backup complete!"
	message "Finished at: $(date +%Y-%m-%d-%H%M)"
	message "Execution time: $(echo "$(date +%s.%N) - $start" | bc) seconds"
	message "##### Rsync Backup Finished ######"
	exit 0
}

# Help screen
info() { cat << EOF
rwrapper: bash wrapper script for rsync
Main actions:
  -s source_dir		provide source address for rsync (REQUIRED)
  -d destination_dir	provide destination address for rsync can be remote or local (REQUIRED)
			NOTE: if local add -m flag!
  -r			remove ALL source files after transfer
  -x			compress entire source folder then sync
  -l log_dir		folder to keep logs. Default: /tmp/logs
  -m 			local backup. Script wont work from local dir to local dir without this set.
			NOTE: do not have this set when doing remote transfers!
  -z			compress incremental backups need -i as well
  -p 			prune files if older than retention value
  -t number		set retention (days). Default: 30
  -e name		specify key name to encrypt backup archive with (implies -x)
  -i			Do incremental backups needed for -z to work
  -b rate		limit socket I/O bandwidth when running rsync

Examples:

	./rwrapper -s src -d fire@7.7.7.7:/home/keeper/backups -p -e fire

Compress and encrypt file, enable pruning and send over to remote host.
(space efficient, less likley to get corrupted data, and secure for off site backups)

	./rwrapper -s src -d fire@7.7.7.7:/home/keeper/backups -px

Same as above just don't encrypt data at rest (Best for space, less likley to get corrupted data)

	./rwrapper -s /data/src -d /two/data/dst -irzpm

Rsync files to local directory while keeping compressed incremental backups and removing source files
(for local to work -m MUST be specified)
(Most likley to have corrupted data over a remote connection because tar is being run over ssh)

	./rwrapper -s /data/src -d cool@2.2.2.2:/home/icebox

Just normal old rsync no compression or anything (good for not getting corrupted data, bad for space)

	./rwrapper -s /data/src -d cool@2.2.2.2:/home/coolbox -pi -t 100 -b 50

Set retention of (incremental backup) directories (not using compression) to 100 days and
set maximum transfer spped to 50KB/s

EOF
}

# Handles reading the users use flags and arguments entered
while getopts "hrnxlmzpis:d:l:t:e:b:" o; do case "${o}" in
    s) src="${OPTARG%/}/" ;;
    d) dst="${OPTARG%/}" ;;
	l) logfolder="${OPTARG%/}" ;;
	n) dryrun=True ;;
	x) compress=True ;;
	m) local_backup=True ;;
	z) compress_diff=True ;;
    r) remove=True ;;
	t) retention="${OPTARG}" ;;
	p) prune=True ;;
	e) encryptor_name="${OPTARG}" ;;
	i) incremental_backups=True ;;
	b) rate_limit="${OPTARG}";;
    *) info;  exit 1 ;;
esac done

# Wherever you see 2>&1 | tee -a "$logfile" that is a log to stdout and log file

# Check for no options
[ $OPTIND -eq 1 ] && info && exit 1

message "##### Rsync Backup Started ######"
message "Started at: $(date +%Y-%m-%d-%H%M)"

# check of source and destinations fields are entered
# rsync and other programs handle the errors from this point on
[ -z "$src" ] && message 'Missing source directory (missing flag -s) Exiting ...' >&2 && exit 1
[ -z "$dst" ] && message 'Missing destination directory (missing flag -d) Exiting ...' >&2 && exit 1

# Rsync config
# -a archive mode save most attributes of a file including permissions
# -P show progress of file, good if you are watching in the terminal
# -v verbose
# -h times in human readable format
# -i show transferring files in list format
# -O dont worry about changing directory times (without this rsync will throw a minor error about not being able to change directory times unless user owns file)
# -A preseve acls
# -X preserver xattrs
# -z compress when transferring
# --delete delete extraneous files from the receiving side (this is mostly to keep an up to date exact copy of the full directory)
# --stats display and log stats after execution
# --exclude exclude files maching pattern
OPT="-aPvhiOAXz --delete --stats --exclude='*.incomplete'"
# if user specifies -r remove source files after transfer successful to destination
[ -n "${remove+x}" ] && OPT="$OPT --remove-source-files"
# run in dryrun mode if specified
[ -n "${dryrun+x}" ] && OPT="$OPT -n"
# rsync will log to this programs log file
LOG="--log-file=$logfile"
# Keep the date dor incremental backups
DATE=$(date +%Y-%m-%d-%H%M)
# do incremental backups if user specified -i
BACKUP=""
[ -n "${incremental_backups}" ] && BACKUP="--backup-dir=../$DATE/"
# allow limiting of I/O socket bandwidth (read rsync man page for more on this option)
RATE=""
[ -n "${rate_limit}" ] && RATE="--bwlimit=${rate_limit}"

# concatenated rsync command. Notice destination goes to full directory thats so delete doesnt remove our
# incremental backups or other backups
command="rsync ${OPT} ${LOG} ${RATE} ${src} ${dst}/full/ ${BACKUP}"
# Rsync Notes:
#
# About checksums (from man page):
# Note that rsync always verifies that each transferred file was correctly reconstructed on the receiving side
# by checking a whole-file checksum that is generated as the file is transferred, but that automatic after-the-transfer
# verification has nothing to do with this option's before-the-transfer "Does this file need to be updated?" check.
#
# Sync transfer is encrypted as it happens through SSH, unles rsync is daemonized, but thats why this script exists

# Compress entire source directory (-x or -e)
if [ -n "${compress+x}" ] || [ -n "${encryptor_name+x}" ]; then
	# backup a directory up from the source directory just to avoid any problems
	backupfile="${src}../full-backup-$(date +%Y-%m-%d-%H%M).tar.gz"
	# if a local backup the archive can just be made in the destiantion directory
	[ -n "${local_backup+x}" ] && backupfile="$dst/full-backup-$(date +%Y-%m-%d-%H%M).tar.gz"
	# dry run will just make the archive command do nothing
	[ -n "${dryrun+x}" ] && backupfile="/dev/null"
	message "Compressing entire ${src} directory to ${backupfile}"
	# compress entire archive. Excluding .incomplete files keeping all permissions (--acls --xattrs -p)
	# --totals just logs the total size after compressed
	tar --exclude '*.incomplete' --acls --xattrs -cpzf "${backupfile}" --totals "${src}" 2>&1 | tee -a "$logfile"
	# check if we are encrypting if we are encrypt with specified public key and remove unencrypted archive
	[ -n "${encryptor_name+x}" ] && message "Encrypting archive with ${encryptor_name} key" \
		&& gpg -e --batch --yes --no-symkey-cache -r "${encryptor_name}" "${backupfile}" 2>&1 | tee -a "$logfile" \
		&& rm -fv "${backupfile}" 2>&1 | tee -a "$logfile" && backupfile="${backupfile}.gpg"
	# if user wants to remove source directory contents do so now we are done backing it up
	[ -n "${remove}" ] && message "Removing source directory" && rm -rfv "${src}"* 2>&1 | tee -a "$logfile"
	# local has no need for rsync, just end
	[ -n "${local_backup+x}" ] && prune && end_backup
	# rsync the archive to the remote server
	# change destination for rsync command and only sync the file
	# that way other files arent deleted by --delete option
	command="rsync ${OPT} ${LOG} ${RATE} ${backupfile} ${dst}"
	# since we just moving one file from outside the source directory lets get rsync to remove it when its done tranferring
	[ -z "${BACKUP}" ] && command="rsync ${OPT} --remove-source-files ${LOG} ${RATE} ${backupfile} ${dst}"
fi

# Run rsync
message "----- RSYNC START -----"
message "Running: $command"
# This loop will just keep going if rsync gets network errors as that will likley be resolved with time
# if it succeeds the script continues if it doesnt it exits with error
while true; do
	eval "${command}" 2>&1 | tee -a "$logfile"
	exit_code=$?
	case $exit_code in
		10|23|30|35) message "${DATE}: rsync failed with a network type error: ${exit_code}!" && DATE=$(date +%Y-%m-%d-%H%M) ;;
		0) message "${DATE}: rsync had no errors!" && break ;;
		*) message "${DATE}: rsync failed with unexpected error: ${exit_code}!" && exit 1 ;;
esac done
message "----- RSYNC END -----"

# Compress incremental backups created by rsync with the --backup command (-z and -i)
if [ -n "${incremental_backups}" ]; then
	if [ -n "${compress_diff+x}" ] && [ -z "${local_backup+x}" ]; then
		# Compress command to run on the remote machine
		# {dst#*:} takes data after the semi-colon i.e the directory
		# {dst%%:*} takes data before the semi colon i.e the host
		remote_command="[ -d '${dst#*:}/${DATE}' ] && tar --acls --xattrs -cpzf ${dst#*:}/${DATE}.tar.gz --totals ${dst#*:}/${DATE} && rm -rf ${dst#*:}/${DATE:?} || :"
		message "Compressing incremental backups on ${dst%%:*} located at ${dst#*:}"
		# Run the remote command over ssh
		ssh "${dst%%:*}" "eval ${remote_command}" 2>&1 | tee -a "$logfile"
	elif [ -n "${compress_diff+x}" ] && [ -n "${local_backup+x}" ] && [ -d "${dst}/${DATE}" ]; then
		message "Compressing incremental backups at ${dst} locally"
		# Just archive the incremental backup locally
		tar --acls --xattrs -cpzf "${dst}/${DATE}.tar.gz" --totals "${dst}/${DATE}" && rm -rf "${dst:?}/${DATE:?}" 2>&1 | tee -a "$logfile"
	fi
fi

# files all transferred clean up
prune
end_backup

# To decrypt if specified -e
# 1. Be on machine that has the key that matches the name you used to encrypt
# 2. run: gpg -d --output <outfile> <infile>
# This will decrypt with the private key as we encrypt with the public key
# To make gpg key run gpg --full-generate-key
