#! /bin/bash
#########################################################################
#									#
#	netbckup is automatically generated,				#
#		please do not modify!					#
#									#
#########################################################################

#########################################################################
#									#
# Author: Copyright (C) 2012-2021, 2024, 2025  Mark Grant		#
#									#
# Released under the GPLv3 only.					#
# SPDX-License-Identifier: GPL-3.0-only					#
#									#
# Purpose:								#
# Depending on command line arguments runs:-				#
#	a full ad hoc backup.						#
#	a level 0 --listed-incremental (ie full) backup of entire file	#
#		system. It allows for a cycle of 5 backups for weekly	#
#		coverage, designated by 1 - 5.				#
#	a level 1 --listed-incremental backup to produce a differential	#
#		backup.							#
#	a level 1 - n --listed-incremental backup to produce an		#
#		incremental backup.					#
#	a selective backup.						#
#                                                            	  	#
# Syntax:      See usage().						#
#									#
# Exit codes used:-							#
# Bash standard Exit Codes:	0 - success				#
#				1 - general failure			#
# User-defined exit code range is 64 - 113				#
#	C/C++ Semi-standard exit codes from sysexits.h range is 64 - 78	#
#		EX_USAGE	64	command line usage error	#
#		EX_DATAERR	65	data format error		#
#		EX_NOINPUT	66	cannot open input		#
#		EX_NOUSER	67	addressee unknown		#
#		EX_NOHOST	68	host name unknown		#
#		EX_UNAVAILABLE	69	service unavailable		#
#		EX_SOFTWARE	70	internal software error		#
#		EX_OSERR	71	system error (e.g., can't fork)	#
#		EX_OSFILE	72	critical OS file missing	#
#		EX_CANTCREAT	73	can't create (user) output file	#
#		EX_IOERR	74	input/output error		#
#		EX_TEMPFAIL	75	temp failure; user is invited	#
#					to retry			#
#		EX_PROTOCOL	76	remote error in protocol	#
#		EX_NOPERM	77	permission denied		#
#		EX_CONFIG	78	configuration error		#
#	User-defined (here) exit codes range 79 - 113:			#
#		None							#
#									#
# Further Info:								#
#									#
#########################################################################


##################
# Init variables #
##################
outputprefix="$(basename "$0"):"
readonly outputprefix

readonly version=1.3.11				# Script version
readonly packageversion=1.5.0		# Package version

readonly etclocation=/etc/lixbackups	# Path to config directory
readonly loglocation=/var/log		# Log file location
tmpexclfile=/tmp/$$.$(basename "$0").exclude	# Temporary exclude file
readonly tmpexclfile

attachtype=""					# attbckshare - weekly or adhoc
bckupCL=""					# Backup command line
bckuptype=""					# Selected backup type
notifyuser=""


#############
# Functions #
#############

# -h --help output.
# No parameters
# No return value
usage()
{
cat << EOF
Usage is:-
$(basename "$0") {-d|-f|-i|-s|-w}
$(basename "$0") {-h|-V}
Usage is:-
$(basename "$0") [OPTIONS]
	-d or --differential - perform a differential backup
	-f or --full - perform a full ad hoc backup
	-h or --help displays usage information
	-i or --incremental - perform an incremental backup
	-s or --selective - perform a selective backup
	-V or --version displays version information
	-w or --weekly - perform a weekly full backup
EOF
}

# Standard function to tidy up and return exit code.
# Parameters - 	$1 is the exit code.
# No return value.
script_exit()
{
	# The backup share might or might not be mounted.
	if [[ $(/usr/sbin/chkbckshare -bt) == TRUE ]]; then
		/usr/sbin/detbckshare
		# No error check cos could loop via std_cmd_err_handler
	fi

	# FD's 6 & 7 may or may not be open at this point so test individually
	# for closing. This reads lsof with ANDed conditions of; suppress
	# warnings, FD 6, output only FD for this current PID and pipe it's
	# output to wc to count the lines. If FD 6 is open, 2 lines result, PID
	# number and f6.
	if (( $(lsof -aw -d 6 -F f -p $$ | wc -l) )); then
		output "$script_short_desc completed with status $1" "$1" 1
		exec 1>&6 6>&-	# Restore stdout and close FD 6
	fi
	if (( $(lsof -aw -d 7 -F f -p $$ | wc -l) )); then
		exec 2>&7 7>&-	# Restore stderr and close FD 7
	fi

	# Re-Allow logins
	rm -f /etc/nologin

	rm -f "$tmpexclfile"

	output "$script_short_desc completed with status $1" "$1" 0

	exit "$1"
}

# Standard function to test command error and exit if non-zero.
# Parameters - $1 is $?
# No return value.
std_cmd_err_handler()
{
	if (( $1 )); then
		script_exit "$1"
	fi
}

# Standard function to emit messages depending on various parameters.
# Parameters -	$1 What:-	The message to emit.
#		$2 Where:-	stdout == 0
#				stderr != 0
#		$3 Syslog:-	$3 == 0, No.
#				$3 != 0, Yes.
# No return value.
output()
{
	if (( !$2 )); then
		printf "%s %s %s\n" "$(date +'[ %a %b %d %T ]')" \
			"$outputprefix" "$1"
	else
		printf "%s %s %s\n" "$(date +'[ %a %b %d %T ]')" \
			"$outputprefix" "$1" 1>&2
	fi
	if (( $3 )); then
		logger "$outputprefix $1"
		std_cmd_err_handler $?
	fi
}

# Standard trap exit function.
# No parameters.
# No return value.
# shellcheck disable=SC2317  # Do not warn about unreachable commands in trap
# functions, they are legitimate.
trap_exit()
{
	local -i exit_code=$?
	local msg

	msg="Script terminating with exit code $exit_code due to trap received."
	output "$msg" 1 0
	script_exit "$exit_code"
}

# Process command line arguments with GNU getopt.
# Parameters -	$1 is the command line.
# No return value.
proc_CL()
{
	local -i day
	local differential=false fulladhoc=false incremental=false
	local selective=false weekly=false
	local GETOPTTEMP
	local msg
	local tmpGETOPTTEMP

	tmpGETOPTTEMP="getopt -o dfhisVw --long differential,full,help,"
	tmpGETOPTTEMP+="incremental,selective,version,weekly"
	GETOPTTEMP=$($tmpGETOPTTEMP -n "$0" -- "$@")
	std_cmd_err_handler $?

	eval set -- "$GETOPTTEMP"
	std_cmd_err_handler $?

	while true; do
		case "$1" in
		-d|--differential)
			if $fulladhoc || $incremental || $selective \
				|| $weekly; then
				msg="Options d, f, i, s and w are all mutually "
				msg+="exclusive."
				output "$msg" 1 0
				script_exit 64
			fi
			differential=true
			bckuptype="differential"
			attachtype="weekly"
			script_short_desc=$(uname -n)" "$(date +%A)
			script_short_desc+=" Daily Differential Backup"
			shift
			;;
		-f|--full)
			if $differential || $incremental || $selective \
				|| $weekly; then
				msg="Options d, f, i, s and w are all mutually "
				msg+="exclusive."
				output "$msg" 1 0
				script_exit 64
			fi
			fulladhoc=true
			bckuptype="fulladhoc"
			attachtype="adhoc"
			script_short_desc=$(uname -n)" Ad Hoc Full Backup "
			script_short_desc+=$(date '+%Y%m%d%H%M')
			shift
			;;
		-h|--help)
			usage
			shift
			script_exit 0
			;;
		-i|--incremental)
			if $fulladhoc || $differential || $selective \
				|| $weekly; then
				msg="Options d, f, i, s and w are all mutually "
				msg+="exclusive."
				output "$msg" 1 0
				script_exit 64
			fi
			incremental=true
			bckuptype="incremental"
			attachtype="weekly"
			script_short_desc=$(uname -n)" "$(date +%A)
			script_short_desc+=" Daily Incremental Backup"
			shift
			;;
		-s|--selective)
			if $fulladhoc || $differential || $incremental \
				|| $weekly; then
				msg="Options d, f, i, s and w are all mutually "
				msg+="exclusive."
				output "$msg" 1 0
				script_exit 64
			fi
			selective=true
			bckuptype="selective"
			attachtype="adhoc"
			script_short_desc="$(uname -n) $(date +%A) Selective "
			script_short_desc+="Backup"
			shift
			;;
		-V|--version)
			printf "%s Script version %s\n" "$0" $version
			printf "%s Package version %s\n" "$0" $packageversion
			shift
			script_exit 0
			;;
		-w|--weekly)
			if $fulladhoc || $differential || $incremental \
				|| $selective; then
				msg="Options d, f, i, s and w are all mutually "
				msg+="exclusive."
				output "$msg" 1 0
				script_exit 64
			fi
			weekly=true
			bckuptype="weekly"
			attachtype="weekly"
			# Calculate backup sequence number.
			# Remove leading zeros otherwise thinks it is octal
			day=$(date +%d | sed 's/^0*//')
			bckseq=1
			if (( day > 28 )); then
				bckseq=5
			elif (( day > 21 )); then
				bckseq=4
			elif (( day > 14 )); then
				bckseq=3
			elif (( day > 7 )); then
				bckseq=2
			fi
			script_short_desc="$(uname -n) Weekly Full Backup - "
			script_short_desc+="$bckseq"
			shift
			;;
		--)	shift
			break
			;;
		*)	output "Internal error." 1 0
			script_exit 64
			;;
		esac
	done

	# Script does not accept other arguments.
	if (( $# > 0 )); then
		output "Invalid argument." 1 0
		script_exit 64
	fi

	# One option has to be selected.
	if ! $differential && ! $fulladhoc && ! $incremental && ! $selective \
		&& ! $weekly; then
		output "Either d, f, i, s or w must be set." 1 0
		script_exit 64
	fi
}

# Create the logfile with ownership and permissions copied from syslog or
# messages, if available, or just defaulting if not.
# Parameters - none.
# No return value.
cre_logfile()
{
	local logname

	if [[ -f $loglocation/syslog ]]; then
		logname=$loglocation/syslog
	elif [[ -f $loglocation/messages ]]; then
		logname=$loglocation/messages
	fi
	touch $loglocation/lixbackups.log
	std_cmd_err_handler $?
	if [[ $logname ]]; then
		chown --reference=$logname  $loglocation/lixbackups.log
		std_cmd_err_handler $?
		chmod --reference=$logname  $loglocation/lixbackups.log
		std_cmd_err_handler $?
	fi
}

# Ensure logfile exists and is accessible.
# Parameters - None.
# No return value.
chk_logfile()
{
	local msg

	mkdir -p $loglocation
	std_cmd_err_handler $?

	if [[ ! -e $loglocation/lixbackups.log ]]; then
		cre_logfile
	elif [[ ! -f $loglocation/lixbackups.log \
		|| ! -r $loglocation/lixbackups.log \
		|| ! -w $loglocation/lixbackups.log ]]; then
		msg="Incorrect file type or permissions for"
		output "$msg $loglocation/lixbackups.log." 1 1
		script_exit 66
	fi
}

# Ensure required config files exist and are accessible.
# Parameters - None.
# No return value.
chk_config_files()
{
	local msg

	if [[ ! -f $etclocation/backups.conf || ! -r $etclocation/backups.conf \
		|| ! -w $etclocation/backups.conf ]]; then
		msg="Incorrect file type or permissions for"
		output "$msg $etclocation/backups.conf." 1 1
		script_exit 66
	fi

	if [[ ! -f $etclocation/bckdaily.exclude \
		|| ! -r $etclocation/bckdaily.exclude \
		|| ! -w $etclocation/bckdaily.exclude ]]; then
		msg="Incorrect file type or permissions for"
		output "$msg $etclocation/bckdaily.exclude." 1 1
		script_exit 66
	fi

	if [[ ! -f $etclocation/bckseldaily.exclude \
		|| ! -r $etclocation/bckseldaily.exclude \
		|| ! -w $etclocation/bckseldaily.exclude ]]; then
		msg="Incorrect file type or permissions for"
		output "$msg $etclocation/bckseldaily.exclude." 1 1
		script_exit 66
	fi

	if [[ ! -f $etclocation/bckseldaily.files \
		|| ! -r $etclocation/bckseldaily.files \
		|| ! -w $etclocation/bckseldaily.files ]]; then
		msg="Incorrect file type or permissions for"
		output "$msg $etclocation/bckseldaily.files." 1 1
		script_exit 66
	fi

	if [[ ! -f $etclocation/bcksystem.exclude \
		|| ! -r $etclocation/bcksystem.exclude \
		|| ! -w $etclocation/bcksystem.exclude ]]; then
		msg="Incorrect file type or permissions for"
		output "$msg $etclocation/bcksystem.exclude." 1 1
		script_exit 66
	fi

	if [[ ! -f $etclocation/bckweekly.exclude \
		|| ! -r $etclocation/bckweekly.exclude \
		|| ! -w $etclocation/bckweekly.exclude ]]; then
		msg="Incorrect file type or permissions for"
		output "$msg $etclocation/bckweekly.exclude." 1 1
		script_exit 66
	fi
}

# Check to see if root account is accessible, if so, prohibit non-root login
# Parameters - none
# No return value
try_login_denial()
{
	local msg
	local passwdstatus

	passwdstatus=$(passwd -S root | awk '{ print $2 }')

	# Different results are possible depending on distribution. Looking at
	# Debian, Fedora and openSUSE only. Possible values are Password Set P
	# (Debian and openSUSE) or PS (Fedora), No Password NP on all three and
	# Locked L (Debian and openSUSE) or LK (Fedora).
	if [[ $passwdstatus == *P* ]]; then
		msg="Root password status is $passwdstatus. Denying non-root "
		msg+="login access."
		output "$msg" 0 1
		printf "Login refused. Backup in progress.\n" > /etc/nologin
	else
		msg="Root password status is $passwdstatus. Login access not "
		msg+="being denied."
		output "$msg" 0 1
	fi
}

# Process the config file just for the parameters of interest.
# Parameters - None
# No return value.
proc_config_file()
{
	local input=()
	local oldIFS=$IFS
	local adhocnotifyuser weeklynotifyuser

	IFS="="

	exec 3<"$etclocation/backups.conf"
	while read -u3 -ra input; do
		case ${input[0]} in
		adhocnotifyuser)
			adhocnotifyuser=${input[1]}
			;;
		mntdir)
			mntdir=${input[1]}
			;;
		weeklynotifyuser)
			weeklynotifyuser=${input[1]}
			;;
		esac
	done
	exec 3<&-

	if [[ $attachtype == weekly ]]; then
		notifyuser=$weeklynotifyuser
	else
		notifyuser=$adhocnotifyuser
	fi

	IFS=$oldIFS
}

# Produce system-wide list of unreadable files and files of types to be excluded
# skipping directories appearing in bcksystem.exclude.
# Processing bcksystem.exclude:-
#	If the directory being excluded is explicit then use wholename option
#	so the path has to be an exact match.
#	If the directory is quoted then use -name which matches any part of the
#	full path.
# Parameters - none.
# No return value.
prod_adhoc_excl_list()
{
	local -i count=0
	local findCL
	local input
	local nameoption

	findCL="find / -type d '(' "
	exec 3<"$etclocation/bcksystem.exclude"
	while read -u3 -r input; do
		if (( count )); then
			findCL+=" -o "
		fi
		# In the substring test below we are looking for a single quote.
		# We could use "'"'"'" which works. The first single quote is to
		# exit out of the single quoted awk command and the last single
		# quote is to re-enter the single quoted awk command. "'" is for
		# using a literal single quote.
		# Using "\'" does not work because of the special single quote
		# meaning.
		# The chosen escape sequence using the single quote hex value
		# seems the simplest.
		nameoption=$(echo "$input" | awk '{if (substr($0,1,1)=="\x27")
					print "-name "
					else
					print "-wholename "}')
		findCL+="$nameoption$input"
		((count++))
	done
	exec 3<&-

	findCL+=" ')' -prune -o ! -readable -print "
	# GNU extension allowing -type b,c,p,s is not ubiquitous,
	# openSUSE 190128, so use the long method.
	findCL+="-o -type b -print -o -type c -print -o -type p -print "
	findCL+="-o -type s -print "
	findCL+="> $tmpexclfile"

	output "Find command used:- $findCL" 0 0

	eval "$findCL"
	std_cmd_err_handler $?
}

# Add standard excludes to backup CL.
# Parameters - none.
# No return value.
bckupCL_add_std_excl()
{
	bckupCL+="--exclude-from=$etclocation/bcksystem.exclude "
	bckupCL+="--exclude-from=$tmpexclfile "
	bckupCL+="--exclude-caches "
}

# Build the backup command line and associated filenames.
# Parameters -none.
# No return value.
build_bckupCL()
{
	local msg
	local snarpath

	bckupCL="tar -cpz --format=posix -f "


	case $bckuptype in
	differential)
		# Build the backup & incremental file names and paths
		backpath="$mntdir/backup$(date +%a).tar.gz"
		snarpath="$mntdir/backup$(date +%a).snar"

		# If the incremental files exists, delete.
		# (Just in case full backup has not done this)
		if [[ -f $snarpath ]]; then
			rm "$snarpath"
			std_cmd_err_handler $?
		fi

		# Copy level 0 incremental file in order to perform a
		# level 1, effective differential backup each run.
		if [[ -f $mntdir/backup.snar && -r $mntdir/backup.snar \
			&& -w $mntdir/backup.snar ]]; then
			cp "$mntdir/backup.snar" "$snarpath"
			std_cmd_err_handler $?
		else
			msg="backup.snar does not exist or is not accessible."
			output "$msg" 1 1
			script_exit 66
		fi

		bckupCL+="$backpath "
		bckupCL+="--exclude-from=$etclocation/bckdaily.exclude "
		# Exclude system and cache directories
		bckupCL_add_std_excl
		# --listed-incremental, by default, stores the device number in
		# the snar file. Some file systems, perhaps particularly NFS, do
		# not consistently use the same device number for the same mount
		# operation. If this happens then a device number mismatch leads
		# to a full backup. So, do not check the device numbers.
		bckupCL+="--listed-incremental=$snarpath --no-check-device "
		;;
	fulladhoc)
		# Build the backup file name and path
		backpath="$mntdir/backup$(date '+%Y%m%d%H%M').tar.gz"

		bckupCL+="$backpath "
		bckupCL_add_std_excl
		;;
	incremental)
		# Build the backup & incremental file names and paths
		backpath="$mntdir/backup$(date +%a).tar.gz"
		snarpath="$mntdir/backup.snar"

		# Check that the incremental file exists.
		if [[ ! -f $snarpath || ! -r $snarpath \
			|| ! -w $snarpath ]]; then
			msg="$snarpath does not exist or is not accessible."
			output "$msg" 1 1
			script_exit 66
		fi

		bckupCL+="$backpath "
		bckupCL+="--exclude-from=$etclocation/bckdaily.exclude "
		bckupCL_add_std_excl
		bckupCL+="--listed-incremental=$snarpath --no-check-device "
		;;
	selective)
		# Build the backup file name and path
		backpath="$mntdir/backupsel$(date +%a).tar.gz"

		bckupCL+="$backpath "
		bckupCL+="--exclude-from=$etclocation/bckseldaily.exclude "
		bckupCL_add_std_excl
		bckupCL+="--files-from=$etclocation/bckseldaily.files "
		;;
	weekly)
		# Build the backup & incremental file names and paths
		backpath="$mntdir/backup$bckseq.tar.gz"
		snarpath="$mntdir/backup.snar"

		# If the level 0 incremental file exists, delete
		if [[ -f $snarpath ]]; then
			rm "$snarpath"
			std_cmd_err_handler $?
		fi

		bckupCL+="$backpath "
		bckupCL+="--exclude-from=$etclocation/bckweekly.exclude "
		bckupCL_add_std_excl
		bckupCL+="--listed-incremental=$snarpath --no-check-device "
		;;
	esac

	# Selective uses --files-from so doesnt need starting node (root)
	if [[ $bckuptype != selective ]]; then
		bckupCL+="/"
	fi
}


########
# Main #
########

# Setup trap.
trap trap_exit SIGHUP SIGINT SIGQUIT SIGTERM

proc_CL "$@"

chk_logfile

output "Refer to $loglocation/lixbackups.log for further information." 0 1
output "$script_short_desc" 0 1


# Make copies of stdout & stderr then re-direct stdout & stderr to backup log
# and write initial entries
exec 6>&1 7>&2

exec 1>> $loglocation/lixbackups.log 2>&1

output "$script_short_desc" 0 0

chk_config_files

try_login_denial

# Get mntdir and notifyuser.
proc_config_file

# Check to see if the network backup share is mounted, if so umount as it could
# be mounted using different parameters, then mount
if [[ $(/usr/sbin/chkbckshare -bt) == TRUE ]]; then
	/usr/sbin/detbckshare
	std_cmd_err_handler $?
fi
/usr/sbin/attbckshare --$attachtype --verbose
std_cmd_err_handler $?

prod_adhoc_excl_list

build_bckupCL

# If the backup file exists, delete
if [[ -f $backpath ]]; then
	rm "$backpath"
	std_cmd_err_handler $?
fi

output "Attempting to process backup - $backpath" 0 1

# Empty the network trashbox if it exists
rm -fr "$mntdir"/trashbox/*
std_cmd_err_handler $?

output "Backup command used:- $bckupCL" 0 0
eval "$bckupCL"
std_cmd_err_handler $?

# Mail disk stats
df -h | mailx -s "$script_short_desc" "$notifyuser"
std_cmd_err_handler $?

# Mail backup file date hierarchy
# shellcheck disable=SC2012  # Output although not standardised is meant for
# humans, so ls is fine, find not needed.
ls -lht "$mntdir" | mailx -s "$script_short_desc" "$notifyuser"
std_cmd_err_handler $?

# And exit.
script_exit 0

