#!/usr/local/bin/bash

# Copyright (c) 2015-2020 Oliver Mahmoudi
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted providing that the following conditions 
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

### backupuser - a user backup utility

# Functions:
# make_package_list
# create_backup
# backup_etc
# do_hook
# make_clean
# strip_trailing_slash
# scp_get_args
# scp_get_destination
# copy_backup_scp
# get_path_to_file
# get_filename
# delete_backup
# usage

# Variables:
WHO=`whoami`		# == WHO=$(whoami)
DATE=$(date +%m.%d.%Y)
YEAR=$(date +%Y)	# This year
TIME=$(date +%H-%M-%S)
FILENAME=${WHO}_${DATE}_${TIME}.tar.gz
ETC_NAME=etc_${DATE}_${TIME}.tar.gz
USER_REPORT_FILE=${WHO}_report
ETC_REPORT_FILE=etc_report
BACKUP_BASE=${HOME}/backups
BACKUP_DIR=${BACKUP_BASE}/${YEAR}
BACKUP_DIR_ETC=${BACKUP_BASE}/etc/${YEAR}
BU_CONTENTS=		# contents of ${HOME}/.bu
SCP_DEST=
h_flag=0			# create backup in ${HOME}
j_flag=0			# ignore pre-backup processing
k_flag=0			# ignore post-backup processing
p_flag=0			# create installed packages list
r_flag=0			# do not log backup in user_report_file
s_flag=0			# scp flag
v_flag=0			# verbose flag
x_flag=0			# do not process ~/.bu/bu_excludes

#
# Save a list of the installed packages in root's home direcory if desired.
# Different operating systems use different package managers. 
# Currently supported are: Linux's yum, dpkg and FreeBSD's pkg.
#
function make_package_list()
{
		if [ $v_flag -eq 1 ] ; then
			echo -n "Generating installed packages list..."
		fi
		pkg info > ~/installed_packages_list
		if [ $v_flag -eq 1 ] ; then
			echo " done."
		fi
}

#
# Backup the user's home directoy. This is the main function.
#
function create_backup()
{
	if [ $v_flag -eq 1 ] ; then
		echo "Getting ready to backup ${WHO}'s home directory:"
	fi

	if [ "${WHO}" = "root" -a $p_flag -eq 1 ] ; then
		make_package_list
	fi

	if [ ! -d ${BACKUP_DIR} ] ; then
		if [ $v_flag -eq 1 ] ; then
			echo -n "Your backup directory: ${BACKUP_DIR} doesn't seem to exist. Creating it..."
		fi
		mkdir -p ${BACKUP_DIR}
		if [ $v_flag -eq 1 ] ; then
			echo " done."
		fi
	fi

	# Invoke the do_hook function which checks for any extra files, that begin with
	# any prefix other than the word 'post', in ${HOME}/.bu before the actual backup.
	# The file bu_excludes, if found, will not be sourced.
	if [ $j_flag -eq 0 ] ; then
		do_hook "pre"
	fi

	# Move to the temporary backup directory /tmp or else ${BACKUP_DIR} if h_flag is set
	if [ $v_flag -eq 1 -a $h_flag -eq 0 ] ; then
		echo -n "Moving to temporary backup folder /tmp..."
	elif [ $v_flag -eq 1 -a $h_flag -eq 1 ] ; then
		echo -n "Moving to backup folder ${BACKUP_DIR}..."
	fi

	if [ $h_flag -eq 0 ] ; then
		cd /tmp > /dev/null 2>&1
	else
		cd ${BACKUP_DIR} > /dev/null 2>&1
	fi

	if [ $v_flag -eq 1 ] ; then
		echo " done."
	fi

	# Create the backup
	if [ -e "${HOME}/.bu/bu_excludes" -a $x_flag -eq 0 ] ; then
		if [ $v_flag -eq 1 ] ; then
			echo "The following files and folders will not be backed up:"
			cat ${HOME}/.bu/bu_excludes
			echo
			if [ $h_flag -eq 0 ] ; then
				echo -n "Creating backup in /tmp..."
			else
				echo -n "Creating backup in ${BACKUP_DIR}..."
			fi
		fi
		tar -czf ${FILENAME} --exclude ${BACKUP_BASE} -X ${HOME}/.bu/bu_excludes ${HOME} > /dev/null 2>&1
		if [ $v_flag -eq 1 ] ; then
			echo " done."
		fi
	else
		if [ $v_flag -eq 1 -a $h_flag -eq 0 ] ; then
			echo -n "Creating backup in /tmp..."
		elif [ $v_flag -eq 1 -a $h_flag -eq 1 ] ; then
			echo -n "Creating backup in ${BACKUP_DIR}..."
		fi
		tar -czf ${FILENAME} --exclude ${BACKUP_BASE} ${HOME} > /dev/null 2>&1
		if [ $v_flag -eq 1 ] ; then
			echo " done."
		fi
	fi

	# If $h_flag == 0, move the backup to the directory it is destined for, ${BACKUP_DIR}
	# When being verbose, we here only need to report "echo done" to the user, when
	# we are compiling the archive in /tmp.
	if [ $v_flag -eq 1 -a $h_flag -eq 0 ] ; then
		echo -n "Moving backup from /tmp to ${BACKUP_DIR}..."
	fi

	if [ $h_flag -eq 0 ] ; then
		mv ${FILENAME} ${BACKUP_DIR} > /dev/null 2>&1
	fi

	if [ $v_flag -eq 1 -a $h_flag -eq 0 ] ; then
		echo " done."
	fi

	# In case $h_flag == 1, we are already there, so therefore...
	if [ $h_flag -eq 0 ] ; then
		cd ${BACKUP_DIR} > /dev/null 2>&1
	fi

	# Test the backup
	if [ $v_flag -eq 1 ] ; then
		echo -n "Testing the backup..."
	fi
	gzip --test ${FILENAME} > /dev/null 2>&1

	if [ $? -eq 0 ] ; then
		if [ $v_flag -eq 1 ] ; then
			echo ' OK!'
		fi
	else
		echo "File: ${FILENAME} is corrupted."
		rm -v ${FILENAME}
		exit 1
	fi

	# Checksums
	if [ $r_flag -eq 0 ] ; then
		if [ $v_flag -eq 1 ] ; then
			echo -n "Generating checksum in ${BACKUP_DIR}/${USER_REPORT_FILE}..."
		fi
		sha256 ${FILENAME} >> ${USER_REPORT_FILE}
		if [ $v_flag -eq 1 ] ; then
			echo " done."
		fi
	fi

	# Call the do_hook function which checks for and sources any file, that
	# begins with the word 'post' to process in ${HOME}/.bu after the backup.
	if [ $k_flag -eq 0 ] ; then
		do_hook "post"
	fi

	# Print out a status report.
	echo 'Backup successful!'
	echo "Your backup file is: ${BACKUP_DIR}/${FILENAME}"
}

#
# Make a backup of the /etc directory.
#
function backup_etc()
{
	if [ "${WHO}" != "root" ] ; then
		echo 'You need to be root to backup the /etc directory!'
		exit 1
	fi

	if [ $v_flag -eq 1 ] ; then
		echo "Getting ready to backup the /etc directory:"
	fi

	if [ ! -d ${BACKUP_DIR_ETC} ] ; then
		if [ $v_flag -eq 1 ] ; then
			echo -n "Backup directory: ${BACKUP_DIR_ETC} doesn't seem to exist. Creating it..."
		fi
		mkdir -p ${BACKUP_DIR_ETC}
		if [ $v_flag -eq 1 ] ; then
			echo " done."
		fi
	fi

	# Move to the temporary backup directory /tmp. Only root can initiate a backup of
	# /etc so /tmp is always available.
	if [ $v_flag -eq 1 ] ; then
		echo -n "Moving to temporary backup folder /tmp..."
	fi
	cd /tmp > /dev/null 2>&1
	if [ $v_flag -eq 1 ] ; then
		echo " done."
	fi

	if [ $v_flag -eq 1 ] ; then
		echo -n "Creating the backup..."
	fi
	tar -czf ${ETC_NAME} /etc > /dev/null 2>&1
	if [ $v_flag -eq 1 ] ; then
		echo " done."
	fi

	# Move the backup to the directory it is destined for
	if [ $v_flag -eq 1 ] ; then
		echo -n "Moving backup from /tmp to ${BACKUP_DIR_ETC}..."
	fi
	mv ${ETC_NAME} ${BACKUP_DIR_ETC} > /dev/null 2>&1
	if [ $v_flag -eq 1 ] ; then
		echo " done."
	fi
	cd ${BACKUP_DIR_ETC} > /dev/null 2>&1

	# test the backup
	if [ $v_flag -eq 1 ] ; then
		echo -n "Testing the backup..."
	fi
	gzip -t ${ETC_NAME} > /dev/null 2>&1

	if [ $? -eq 0 ] ; then
		if [ $v_flag -eq 1 ] ; then
			echo ' OK!'
		fi
	else
		echo "File: ${ETC_NAME} is corrupted."
		rm -v ${ETC_NAME}
		exit 1
	fi
		
	# Checksums
	if [ $r_flag -eq 0 ] ; then
		if [ $v_flag -eq 1 ] ; then
			echo -n "Generating checksums in ${BACKUP_DIR_ETC}/${ETC_REPORT_FILE}..."
		fi
		sha256 ${ETC_NAME} >> ${ETC_REPORT_FILE}
        if [ $v_flag -eq 1 ] ; then
                echo " done."
        fi
	fi

	# Print out a status report.
	echo 'Backup successful!'
	echo "/etc backup file is: ${BACKUP_DIR_ETC}/${ETC_NAME}"
}

#
# The modular hook function begings here. Check whether there are any files in the
# $HOME/.bu folder. If so, then source them.
#
function do_hook()
{
	if [ ! -d ${HOME}/.bu ] ; then
		return
	fi

	# Pre-Backup stage: Process any file, that begins with a prefix other then 'post'
	# and is not the bu_excludes file.
	if [ "$1" = "pre" ] ; then
		cd $HOME/.bu
		BU_CONTENTS=*
		if [ -n "$BU_CONTENTS" ] ; then
			if [ $v_flag -eq 1 ] ; then
				echo "Sourcing pre-backup files from folder $HOME/.bu:"
			fi
			for i in $BU_CONTENTS
			do
				if [ "$i" = "*" ] ; then
					if [ $v_flag -eq 1 ] ; then
						echo "Done sourcing $HOME/.bu."
					fi
					return
				fi
				if [ "${i:0:4}" = "post" -o "${i}" = "bu_excludes" ] ; then
					continue
				fi
				if [ $v_flag -eq 1 ] ; then
					echo "Sourcing $HOME/.bu/$i"
				fi
				source ./$i
			done
			if [ $v_flag -eq 1 ] ; then
				echo "Done sourcing $HOME/.bu."
			fi
		fi
	# Post-Backup stage: Process any file, that begins with the prefix 'post'.
	elif [ "$1" = "post" ] ; then
		cd $HOME/.bu
		BU_CONTENTS=*
		if [ -n "$BU_CONTENTS" ] ; then
			if [ $v_flag -eq 1 ] ; then
				echo "Sourcing post-backup files from folder $HOME/.bu:"
			fi
			for i in $BU_CONTENTS
			do
				if [ "$i" = "*" ] ; then
					if [ $v_flag -eq 1 ] ; then
						echo "Done sourcing $HOME/.bu."
					fi
					return
				fi
				if [ ! "${i:0:4}" = "post" ] ; then
					continue
				fi
				if [ $v_flag -eq 1 ] ; then
					echo "Sourcing $HOME/.bu/$i"
				fi
				source ./$i
			done
			if [ $v_flag -eq 1 ] ; then
				echo "Done sourcing $HOME/.bu."
			fi
		fi
	fi
}

#
# Clean the users backup directory.
#
function make_clean()
{
	local choice

	# Confirm
	echo "This will delete the entire contents of you backupfolder: ${BACKUP_BASE}"
	echo -n "Are you sure? [yes or no]: "
	read -t 30 choice		# We got 30 seconds to make a choice

	case $choice in

	[Yy][Ee][Ss] | [Yy] )			# remove old files : >
		rm -fvr ${BACKUP_BASE}/*
		;;
	[Nn][Oo] | [Nn] )
		echo 'Aborted!'
		exit 1
		;;
	*)
		echo "Terminating."
		exit 1
		;;
	esac
}

#
# Strip a trailing slash from given pathnames, i.e.
# /path/to/file/ becomes /path/to/file
#
function strip_trailing_slash()
{
	local _length _strlen _last_char

	_length=$(awk -v value=$1 'BEGIN {
		n = length(value);
		print n;
	}')

	_last_char=${1:$_strlen-1:1}

	if [ "$_last_char" = "/" ] ; then
		echo ${1:0:$_strlen-1}
	else
		echo $1
	fi
}

#
# Copy the backup to another local device.
#
function copy_backup_local()
{
	local _i

	_i=$1

	if [ -d $_i ] ; then
		if [ -w $_i ] ; then
			_i=$(strip_trailing_slash $_i)
			if [ $v_flag -eq 1 ] ; then
				echo -n "Copying backup to: $_i..."
			fi
			cp -i ${BACKUP_DIR}/${FILENAME} $_i
			if [ $v_flag -eq 1 ] ; then
				echo " done."
			fi
		else
			echo "$_i is not writable."
		fi
	else
		echo "$_i is not a directory."
	fi
}

#
# Get the destination for scp. Passed via -s.
#
function scp_get_destination_path()
{
	local _dest

	_dest=$(awk -v value="$1" 'BEGIN {
		n=split(value, a);
		print a[n];
	}')

	echo $_dest
}

#
# Get the arguments (if any) for scp. Passed via -s.
#
function scp_get_arguments()
{
	local _args

	_args=$(awk -v value="$1" 'BEGIN {
		args=""
		n=split(value, a);
		for(i = 1; i < n; i++)
			args=args" "a[i];

		print args;
	}')

	echo $_args
}

#
# Copy the backup via scp
#
function copy_backup_scp()
{
	local _scp_args _scp_path _file

	_file=$2

	if [ $v_flag -eq 1 ] ; then
		echo "Copying backup via scp:"
	fi

	_scp_args=$(scp_get_arguments "$1")
	_scp_path=$(scp_get_destination_path "$1")

	if [ ! -z "$_scp_args" ] ; then
		scp $_scp_args $_file $_scp_path
	else
		scp $_file $_scp_path
	fi
}

#
# Extract the path to a file: /path/to/file -> /path/to/
#
function get_path_to_file()
{
	local _awkvar _ptf

	# Get the left part of the last "/" aka path to file.
	_ptf=$(awk -v _awkvar=$1 'BEGIN { 
		string = "";
		n = split(_awkvar, a, "/");

		for(i = 2; i < n; i++) 
			string = string"/"a[i];

		string = string"/";
		print string;
		}')

	echo $_ptf
}

#
# Extract the filename out of a full path: /path/to/file -> file
#
function get_filename()
{
	local _awkvar _fn

	# Get the right part of the last "/" aka filename.
	_fn=$(awk -v _awkvar=$1 'BEGIN { 
		n = split(_awkvar, a, "/");
		print a[n];
		}')

	echo $_fn
}

#
# Delete a backup
#
function delete_backup()
{
	local _bu _choice _file _ln _nobs _rf _rf_flag

	# Check for the existence of the backup that we seek to delete.
	_bu=$(realpath $1)
	if [ ! -f $_bu ]; then
		echo "The backup: $_bu doesn't exist."
		exit
	fi

	# Construct the report file and see if it exists in the target directory.
	# If not, continue but set _rf_flag to 1
	_rf_flag=0
	_rf=$(get_path_to_file $_bu)${USER_REPORT_FILE}
	if [ ! -f $_rf ]; then
		_rf=$(get_path_to_file $_bu)${ETC_REPORT_FILE}
		if [ ! -f $_rf ]; then
			echo "The reportfile doesn't exist."
			_rf_flag=1
		fi
	fi

	# Check if the entry in our reportfile is unique. If so, get the line number.
	if [ $_rf_flag -eq 0 ] ; then
		_file=$(get_filename $_bu)
		_nobs=$(cat $_rf | grep -c $_file)
		if [ $_nobs -eq 0 ]; then
			echo "No entry for: $_file in reportfile."
			_rf_flag=1
		elif [ $_nobs -eq 1 ]; then
			_ln=$(cat $_rf | grep -n $_file)
		else
			# Not probable but still...
			echo "Entry for: $_file in reportfile not unique."
			_rf_flag=1
		fi
	fi

	# Make sure and delete the backup. Otherwise abort.
	echo "This will delete your backup: $_bu"
	echo "Are you sure? [yes or no]: "
	read -t 30 _choice		# We got 30 seconds to make a choice

	case $_choice in
	[Yy][Ee][Ss] | [Yy] )
		if [ $_rf_flag -eq 0 ] ; then
			sed -i ${_ln%:*}d $_rf
		fi
		rm $_bu
		exit 0
		;;
	[Nn][Oo] | [Nn] )
		echo 'Aborted!'
		exit 1
		;;
	*)
		echo "Terminating."
		exit 1
		;;
	esac
}

#
# usage function
#
function usage()
{
	echo "usage:"
	echo "backupuser [-hijkpruvx] [-d backup] [-s remote_server] [local_disks]"
	echo "backupuser [-rv] [-s remote_server] etc [local_disks]"
	echo "backupuser clean"
}

############################################################
##### Point of entry

while getopts ":d:hijkprs:uvx" opt ; do
        case $opt in
                d)
                        delete_backup $OPTARG
                        ;;
                h)
                        h_flag=1		# create archive in ${BACKUP_DIR}
                        ;;
                i)
                        j_flag=1		# no pre-backup processing in do_hook
						k_flag=1		# no post-backup processing in do_hook
                        ;;
                j)
                        j_flag=1		# no pre-backup processing in do_hook
                        ;;
                k)
                        k_flag=1		# no post-backup processing in do_hook
                        ;;
                p)
                        p_flag=1		# create installed packages list flag
                        ;;
                r)
                        r_flag=1		# do not log the backup in report_file
                        ;;
                s)
                        s_flag=1		# scp flag
						SCP_DEST=$OPTARG
                        ;;
                u)
                        usage
						exit 0
                        ;;
                v)
                        v_flag=1		# be more verbose
                        ;;
                x)
						x_flag=1		# do not process ~/.bu/bu_excludes
                        ;;
                \?)
                        echo "unkown flag: -$OPTARG."
                        usage
                        exit 1
                        ;;
				:)
						echo "The -$OPTARG flag needs an argument"
                        usage
						exit 1
						;;
        esac
done

shift $((OPTIND-1))

if [ "$1" = "clean" ] ; then
	make_clean
	echo 'Done!'
	exit 0
elif [ "$1" = "etc" ] ; then
	backup_etc
	if [ $s_flag -eq 1 ] ; then
		copy_backup_scp "$SCP_DEST" ${BACKUP_DIR_ETC}/${ETC_NAME}
	fi
	shift
else
	create_backup
	if [ $s_flag -eq 1 ] ; then
		copy_backup_scp "$SCP_DEST" ${BACKUP_DIR}/${FILENAME}
	fi
fi

for i in $*
do
	copy_backup_local $i
done

echo 'Done!'
exit 0
