#!/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="1h" # 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 { 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