#!/usr/bin/env bash
#
# Borg script for home folder backups.
# Adapted from https://borgbackup.readthedocs.io/en/stable/quickstart.html#automating-backups
#
# See also: https://borgbackup.readthedocs.io/en/stable/faq.html#if-a-backup-stops-mid-way-does-the-already-backed-up-data-stay-there
# https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-placeholders
#
# Copyright (C) 2019 Silvio Rhatto - rhatto at riseup.net
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or any later version.
#
# This program 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 this program. If not, see .
#
# Parameters
FULLNAME="$0"
BASENAME="`basename $0`"
DESTINATION="$1"
OPTION="$2"
BASE_CONFIG="$HOME/.config/borger"
CONFIG="$BASE_CONFIG/$DESTINATION"
INTERVAL="2h"
TMP="${TMP:-/tmp}"
# Print info
function info {
printf "[$BASENAME] [%s] %s\n" "$(date)" "$*" >&2;
}
# Fata error
function fatal {
info [fatal] $*
exit 1;
}
# Display usage
function borger_usage {
echo "usage: $BASENAME [--continuous|--list|--check|--info]"
echo -n "available destinations from $BASE_CONFIG: "
ls $BASE_CONFIG
exit 1
}
# Setup
function borger_setup {
# Ensure we have our base config folder
mkdir -p $BASE_CONFIG
# Check config
if [ ! -e "$CONFIG" ]; then
fatal "No such config $CONFIG"
exit 1
elif [ -d "$CONFIG" ]; then
MULTIPLE="yes"
fi
# Lockfile location
LOCKFILE="$TMP/$BASENAME/$DESTINATION.lock"
}
# Process configuration
function borger_config {
# Ensure we have an username
if [ -z "$USER" ]; then
USER="`whoami`"
fi
# In case your home folder is a symlink
if [ ! -z "`readlink $HOME`" ]; then
ORIG="`readlink $HOME`"
else
ORIG="$HOME"
fi
# Default backup config
KEEPDAILY="7"
KEEPWEEKLY="4"
KEEPMONTHLY="6"
ENCRYPTION="keyfile"
PLACEHOLDER="{user}"
source $CONFIG
# Setting this, so the repo does not need to be given on the commandline:
if [ -z "$BORG_REPO" ]; then
export BORG_REPO_DIR="/var/backups/users/$USER/borg"
export BORG_REPO="ssh://$SSH_SERVER:$SSH_PORT/$BORG_REPO_DIR"
fi
}
# List
function borger_list {
borg list
exit $?
}
# Check
function borger_check {
borg check
exit $?
}
# Info
function borger_info {
borg info
exit $?
}
# Our trap
function borger_trap {
trap 'info Backup interrupted >&2; exit 2' INT TERM
}
# Initialize
function borger_init {
if [ ! -z "$SSH_SERVER" ]; then
# Remote backup over SSH
if ! ssh $SSH_SERVER -p $SSH_PORT test -f $BORG_REPO_DIR/config; then
info "Initializing borg repository at $BORG_REPO..."
borg init --encryption=$ENCRYPTION $BORG_REPO
init_exit=$?
if [ "$init_exit" != "0" ]; then
fatal "Error initializing repository"
fi
fi
else
# Local backup
if [ ! -f "$BORG_REPO/config" ]; then
info "Initializing borg repository at $BORG_REPO..."
borg init --encryption=$ENCRYPTION $BORG_REPO
init_exit=$?
if [ "$init_exit" != "0" ]; then
fatal "Error initializing repository"
fi
fi
fi
}
# Backup the most important directories into an archive named after
# the machine this script is currently running on:
function borger_create {
info "Starting backup..."
borg create \
--verbose \
--filter AME \
--list \
--stats \
--show-rc \
--compression lz4 \
--exclude-caches \
::"${PLACEHOLDER}-{now}" \
$ORIG
backup_exit=$?
if [ "$backup_exit" != "0" ]; then
info "Error pruning repository"
info "Trying to break the lockfile..."
borg break-lock $BORG_REPO
fi
}
# Use the `prune` subcommand to maintain daily, weekly and monthly archives.
# The '${PLACEHOLDER}-' prefix is very important to limit prune's operation to
# one specific archive and not apply to archives also.
function borger_prune {
info "Pruning repository..."
borg prune \
--list \
--prefix "${PLACEHOLDER}-" \
--show-rc \
--keep-daily $KEEPDAILY \
--keep-weekly $KEEPWEEKLY \
--keep-monthly $KEEPMONTHLY \
prune_exit=$?
#if [ "$prune_exit" != "0" ]; then
# fatal "Error pruning repository"
#fi
}
# Create lockfile
function borger_set_lockfile {
if [ ! -z "$LOCKFILE" ]; then
mkdir -p `dirname $LOCKFILE`
if ( set -o noclobber; echo "$$" > "$LOCKFILE" ) &> /dev/null; then
trap 'borger_unset_lockfile' INT TERM EXIT
else
fatal "Could not create lockfile $LOCKFILE, exiting"
fi
fi
}
# Remove lockfile
function borger_unset_lockfile {
if [ ! -z "$LOCKFILE" ]; then
rm -f $LOCKFILE || echo "Could not remove lockfile $LOCKFILE"
fi
}
# Check lockfile
function borger_check_lockfile {
local pid process
if [ ! -z "$LOCKFILE" ] && [ -f "$LOCKFILE" ]; then
pid="`cat $LOCKFILE`"
process="`ps --no-headers -o comm $pid`"
if [ "$?" == "0" ] && [ "`ps --no-headers -o comm $$`" == "$process" ]; then
fatal "Another program is running for $LOCKFILE, skipping run"
else
echo "Found old lockfile $LOCKFILE, removing it"
borger_unset_lockfile
fi
fi
}
# Main backup procedure
function borger_run {
if [ ! -z "$STARTUP_DELAY" ]; then
sleep $STARTUP_DELAY
fi
borger_check_lockfile
borger_set_lockfile
# Run for single or multiple destinations
if [ "$MULTIPLE" == "yes" ]; then
borger_multiple
else
borger_config
borger_trap
borger_init
borger_create
borger_prune
borger_exit
fi
}
# Run for a single destination
function borger_single {
borger_config
# Convert the pass command to passphrase otherwise
# the user would be interrupted by a passphrase prompt
# at every iteration
if [ ! -z "$BORG_PASSCOMMAND" ] && [ -z "$BORG_PASSPHRASE" ]; then
info "Asking passphrase for borg key used at $DESTINATION"
export BORG_PASSPHRASE="`$BORG_PASSCOMMAND`"
export BORG_PASSCOMMAND=""
fi
# Run as a subprocess so we do not exit on any fatal error
$FULLNAME $DESTINATION
}
# Run for multiple destinations
function borger_multiple {
info "Multiple destination \"$DESTINATION\" found. Processing each subconfig..."
# Evaluate each config
for config in `ls $CONFIG`; do
# Include BORG_PASSPHRASE config for each destination in an array
if grep -q "BORG_PASSCOMMAND" $CONFIG/$config; then
# Ask the passphrase only once
if [ -z "${BORG_PASSPHRASES[$config]}" ]; then
info "Asking passphrase for borg key used at $config"
COMMAND="`grep BORG_PASSCOMMAND $CONFIG/$config | cut -d = -f 2 | sed -e "s/^'//" -e "s/'$//" -e 's/^"//' -e 's/"$//'`"
#BORG_PASSPHRASES[$config]="BORG_PASSPHRASE=`$COMMAND`"
BORG_PASSPHRASES[$config]="`$COMMAND`"
fi
else
BORG_PASSPHRASES[$config]=""
fi
done
# Serial approach
for config in `ls $CONFIG`; do
info "Calling borger for $DESTINATION/$config..."
export BORG_PASSCOMMAND=""
export BORG_PASSPHRASE="${BORG_PASSPHRASES[$config]}"
$FULLNAME $DESTINATION/$config 2>&1 | sed -e "s/^\[borger\]/[borger] [$config]/" -e "s/^\([^\[]\)/[borger] [$config] \1/"
done
# Parallel approach
## Config is a folder, so we iterate over all items
## and call borger for each config in parallel
#for config in `ls $CONFIG`; do
# info "Calling borger for $DESTINATION/$config..."
# (
# export BORG_PASSPHRASE="${BORG_PASSPHRASES[$config]}"
# $FULLNAME $DESTINATION/$config $MULTIPLE_OPTION 2>&1 | sed -e "s/^\[borger\]/[borger] [$config]/" -e "s/^\([^\[]\)/[borger] [$config] \1/"
# ) &
#done
## Since we dispatched everything to subprocesses,
## there's nothing to do here.
##exit
#wait
}
# Exit procedure
function borger_exit {
# Use highest exit code as global exit code
global_exit=$(( backup_exit > prune_exit ? backup_exit : prune_exit ))
if [ ${global_exit} -eq 1 ]; then
info "Backup and/or Prune finished with a warning"
fi
if [ ${global_exit} -gt 1 ]; then
info "Backup and/or Prune finished with an error"
fi
exit ${global_exit}
}
# Continuous backup processing
function borger_continuous {
borger_check_lockfile
borger_set_lockfile
# Run until interruption
while true; do
if [ "$MULTIPLE" == "yes" ]; then
borger_multiple
else
borger_single
fi
# Wait
info "Running on continous mode... sleeping $INTERVAL..."
sleep $INTERVAL
done
}
# Strong requirement: bash 4
# https://stackoverflow.com/questions/1494178/how-to-define-hash-tables-in-bash
if echo $BASH_VERSION | grep -q "^3"; then
fatal "$BASENAME requires bash 4 or newer."
fi
# Declare global passphrase array
declare -A BORG_PASSPHRASES
# Setup
borger_setup
# Dispatch
if [ -z "$DESTINATION" ]; then
borger_usage
elif [ -z "$OPTION" ]; then
borger_run
elif [ "$OPTION" == "--list" ]; then
borger_config
borger_list
elif [ "$OPTION" == "--check" ]; then
borger_config
borger_check
elif [ "$OPTION" == "--info" ]; then
borger_config
borger_info
elif [ "$OPTION" == "--continuous" ]; then
borger_continuous
fi