#!/bin/bash
# Copyright 2019-2021 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=1
declare -r versionPatch=1
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 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).
"
}
# getGlobalConfigurationFiles
#
# Returns the paths to all existing valid global configuration files, in
# ascending order of priority
#
# The paths are returned in a newline-separated list
getGlobalConfigurationFiles () {
local suffix=".conf"
local baseDir
local pathPrefix
local configDir
local configFile
for baseDir in "/usr/lib" "/etc" "/run"; do
pathPrefix="${baseDir}/borgit/borgit"
configDir="${pathPrefix}.d"
configFile="${pathPrefix}$suffix"
if test -d "$configDir"; then
find "$configDir" -mindepth 1 -maxdepth 1 -type f -name "*$suffix" -print | sort
fi
if test -f "$configFile"; then
echo "$configFile"
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 systemd unit \"$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 systemd unit \"$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 find &> /dev/null || { echo "ERROR Required program \"find\" 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 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 < <(getGlobalConfigurationFiles)
unset globalConfigFile
# 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
unset config
# 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 Running backup job for archive prefix \"$archivePrefix\"" >&2
if isFunction beforeBackupCustom; then
beforeBackupCustom
else
beforeBackup
fi
backup