#!/bin/bash

# Copyright 2019 eomanis
# 
# This file is part of borgit.
# 
# borgit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
# 
# borgit 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 General Public License
# along with borgit.  If not, see <http://www.gnu.org/licenses/>.

# TODO Bash completion

set -o nounset
set -o noclobber
set -o errexit
shopt -qs inherit_errexit

# Semantic versioning
declare -r versionMajor=0
declare -r versionMinor=0
declare -r versionPatch=3
declare -r versionLabel=""

getVersion () {
	echo -n "${versionMajor}.${versionMinor}.${versionPatch}"
	test "$versionLabel" != "" && echo "-$versionLabel" || echo ""
}

# isFunction command
#
# Returns with code 0 if the given command refers to a function
# (and not, for example, to a shell builtin or an executable)
isFunction () {
	local command="$1"; shift
	local commandType

	! commandType="$(type -t "$command")" && return 1
	! test "function" = "$commandType" && return 1
	return 0
}

printHelp () {
	echo -n "borgit "; getVersion
	echo -n \
"Write your Borg backup jobs with sourced bash configuration files

borgit runs a single Borg backup job according to the given command line
options and further options defined in the given sourced configuration
files.

Usage:
  borgit
    [-s|--archive-suffix archiveSuffix]
    [--]
    configurationFile [configurationFile...]
  borgit --help
  borgit --version

E.g.
  borgit -- repos/system-online jobs/ftp-server

To run multiple backup jobs against the same repository use borgit's
companion application, borgem.

For more information, and for information about writing configuration
files, see manual page borgit(1).
"
}

# getGlobalConfigFiles applicationName [configurationPathPrefix...]
#
# Returns the paths to all existing configuration files for the given
# application name, in general-to-specific order, for the given
# configuration path prefix(es)
# The prefixes must be supplied in general-to-specific order
#
# The paths are returned in a newline-separated list:
#   <prefix><appName>.d/*.conf
#   <prefix><appName>.conf
getGlobalConfigFiles () {
	local appName
	local configRoot
	local configRootDir
	local configRootFile

	appName="$1"; shift
	while test $# -gt 0; do
		configRoot="${1}$appName"; shift
		configRootDir="${configRoot}.d"
		configRootFile="${configRoot}.conf"

		if test -d "$configRootDir"; then
			find "$configRootDir" -mindepth 1 -maxdepth 1 -type f -name '*.conf' -print | sort
		fi
		if test -f "$configRootFile"; then
			echo "$configRootFile"
		fi
	done
}

# Reads: $paths
#
# Complains with ERROR messages about non-existing paths and returns
# with code 1 if any of the paths do not exist
validatePaths () {
	local path
	local exitCode=0

	if test ${#paths[@]} -eq 0; then
		echo " WARN  No paths to be backed up specified" >&2
		return 0
	fi
	for path in "${paths[@]}"; do
		test -e "$path" && continue
		exitCode=1
		echo "ERROR  Path does not exist: \"$path\"" >&2
	done
	return "$exitCode"
}

# Reads: $services
# Appends to: $servicesStopped
#
# Stops running systemd units and stores stopped units into
# $servicesStopped
stopServices () {

	# Stop running systemd services (or possibly timers)
	for service in "${services[@]}"; do
		! systemctl --quiet is-active "$service" && continue
		echo " INFO  Stopping \"$service\"" >&2
		systemctl stop "$service"
		servicesStopped+=( "$service" )
	done
}

# Reads: $servicesStopped
#
# Starts systemd units listed in $servicesStopped
startServices () {

	# Re-start stopped systemd services / timers
	for service in "${servicesStopped[@]}"; do
		echo " INFO  Starting \"$service\"" >&2
		systemctl start "$service"
	done
}

# Reads: $paths, $services, $archivePrefix
# Appends to: $servicesStopped
beforeBackup () {

	validatePaths
	stopServices
}

# Reads: $borgCreate, $repo, $archivePrefix, $archiveSuffix, $paths
backup () {
	# Because of "set -o nounset" the application terminates with a bad
	# exit code if $archivePrefix is not set; the same goes for $repo
	# shellcheck disable=SC2154
	local archive="${archivePrefix}${archiveSuffix}"
	local exitCode

	# shellcheck disable=SC2154
	echo " INFO  Backing up ${#paths[@]} path(s) to \"${repo}::${archive}\"" >&2
	borg "${borgCreate[@]}" "${repo}::${archive}" -- "${paths[@]}" && exitCode=$? || exitCode=$?
	if test "$exitCode" -ne 0; then
		echo "ERROR  Borg returned with exit code $exitCode" >&2
	fi
	return "$exitCode"
}

# Reads: $servicesStopped
afterBackup () {
	startServices
}

# Catch missing arguments
if test $# -eq 0; then
	printHelp
	exit 1
fi

# Catch --help as single argument
if test $# -eq 1 && test "$1" = "--help"; then
	printHelp
	exit 0
fi

# Catch --version as single argument
if test $# -eq 1 && test "$1" = "--version"; then
	getVersion
	exit 0
fi

# Make sure the required programs are available
allRequiredProgramsPresent=true
type borg &> /dev/null     || { echo "ERROR  Required program \"borg\" is not available" >&2;     allRequiredProgramsPresent=false; }
type date &> /dev/null     || { echo "ERROR  Required program \"date\" is not available" >&2;     allRequiredProgramsPresent=false; }
$allRequiredProgramsPresent || exit 1
unset allRequiredProgramsPresent

# Set internal global variables
servicesStopped=()

# Set argument options defaults
archiveSuffix=".$(date --utc --iso-8601)"
configs=()

# Parse the arguments
trailingArgs=false
while test $# -gt 0; do
	if ! $trailingArgs && test "$1" = "--"; then
		trailingArgs=true; shift
	elif $trailingArgs || ! { test ${#1} -ge 1 && test "${1:0:1}" = "-"; }; then
		# Trailing arguments section reached, or the argument does not
		# start with "-": Must be a configuration file
		configs+=( "$1" ); shift
	elif test "$1" = "-s" || test "$1" = "--archive-suffix"; then
		test $# -ge 2 || { echo "ERROR  No value given for argument \"$1\"" >&2; exit 1; }
		shift
		archiveSuffix="$1"; shift
	else
		echo "ERROR  Unknown argument \"$1\"" >&2; exit 1
	fi
done
unset trailingArgs

# Set configuration file options defaults
borgCreate=( create )
paths=()
services=()
# Source global configuration files
while read -rs globalConfigFile; do
	# shellcheck source=/dev/null
	source -- "$globalConfigFile" && exitCode=$? || exitCode=$?
	if test "$exitCode" -ne 0; then
		echo "ERROR  Could not source global configuration file \"$globalConfigFile\"" >&2
		exit "$exitCode"
	fi
done < <(getGlobalConfigFiles "borgit" "/usr/lib/" "/run/" "/etc/")
# Source the configuration files given as trailing arguments
for config in "${configs[@]}"; do
	# shellcheck source=/dev/null
	source -- "$config" && exitCode=$? || exitCode=$?
	if test "$exitCode" -ne 0; then
		echo "ERROR  Could not source configuration file \"$config\"" >&2
		exit "$exitCode"
	fi
done

# Set up the cleanup after-backup trap
if isFunction afterBackupCustom; then
	trap afterBackupCustom EXIT HUP INT TERM
else
	trap afterBackup EXIT HUP INT TERM
fi

# All right, here goes...
echo " INFO  Backing up Borg archive prefix \"$archivePrefix\"" >&2
if isFunction beforeBackupCustom; then
	beforeBackupCustom
else
	beforeBackup
fi
backup