#!/bin/sh # ## borg helper script for viewing and restoring backups ## ## script written by Asif Bacchus, last updated May 15, 2021. ## ## The author of this script is not affiliated with 'borgbackup' in any way and ## does not warrant anything about this script, its operation, suitability or ## fitness for use in any environment or under any conditions. You are using ## this script entirely at your own risk. # ### trap trap trapExit 1 2 3 6 ### functions cleanup() { # remove borg temp directory, if it exists # shellcheck disable=SC2154 if [ -d "${borgBaseDir}/tmp" ]; then if ! rm -rf "${borgBaseDir}/tmp" >/dev/null 2>&1; then consoleError 3 "Script completed successfully but could not remove temporary directory at '$borgBaseDir/tmp'. Sorry to be messy." fi fi if [ -f "${restorePath}/touch.test" ]; then if ! rm -f "${restorePath}/touch.test" >/dev/null 2>&1; then consoleError 5 "Script completed successfully but could not remove test file at '$restorePath/touch.test'. Sorry to be messy." fi fi } consoleError() { printf "%s\n%s\n" "$err" "$2" printf "Exiting.\n\n%s" "$norm" exit "$1" } scriptHelp() { printf "\n" textBlock "${bold}Usage: borghelper.sh [parameters]${norm}" printf "\n" textBlock "Simple script to read connection parameters from a flat text file and process borg 'info', 'list' and 'restore' commands without the very long command lines that are required when specifying repo names and passwords, etc." printf "\n" textBlock "${magenta}The script has the following parameters:${norm}" printf "\n" textBlock "${magenta}--- Required Parameters ---${norm}" printf "\n" textBlockParams "-v|--vars" textBlock "Path to the .borgvars file from which to read borg connection information. This is not required if run with '--makevars'." printf "\n" textBlock "${magenta}--- Operation Modes ---${norm}" printf "\n" textBlockParams "--makevars" textBlock "Create a sample .borgvars file that you can fill in and use with this script." textBlockParams "-i|--info" textBlock "Get information about a specified borg repo archive. Requires you supply '--archive'." textBlockParams "-l|--list" textBlock "List contents of a specified borg repo archive. Requires you supply '--archive'. You can optionally specify a file/pattern to search for using '--file'." textBlockParams "-la|--list-all" textBlock "List all available archives within the repo specified in your .borgvars file." textBlockParams "-r|--restore" textBlock "Restore the specified borg repo archive/file(s). Requires you supply '--archive'." printf "\n" textBlock "${magenta}--- Selector Parameters ---${norm}" printf "\n" textBlockParams "-a|--archive" textBlock "The archive within your borg repo you wish to work with." textBlockParams "--exclude" textBlock "Pattern (python/borg) of files to exclude from a restore operation." textBlockParams "-f|--file" textBlock "Specific file/pattern (python/borg) within an archive for which you want to restore or search. Requires that you supply '--archive'." textBlockParams "-p|--path" textBlock "Path to which you want your archive/files restored. This script will attempt to create the directory for you if it does not already exist." printf "\n" textBlock "${magenta}--- Restore Options ---${norm}" printf "\n" textBlockParams "--progress" textBlock "Display progress indicator during restore operations. WARNING: This can drastically slow down operations on larger archives!" textBlockParams "--verbose" textBlock "List the individual files being processed during restore operations." printf "\n" textBlock "${magenta}--- Other Parameters ---${norm}" printf "\n" textBlockParams "-h|-?|--help" textBlock "This help screen." printf "\n" exit 0 } textBlock() { printf "%s\n" "$1" | fold -w "$width" -s } textBlockParams() { printf "%s%s%s\n" "$cyan" "$1" "$norm" } trapExit() { cleanup printf "%s\nScript execution terminated via signal.\n\n%s" "$err" "$norm" exit 99 } ### text formatting presets if command -v tput >/dev/null; then bold=$(tput bold) cyan=$(tput setaf 6) err=$(tput bold)$(tput setaf 1) magenta=$(tput setaf 5) norm=$(tput sgr0) width=$(tput cols) else bold="" cyan="" err="[ERROR] " magenta="" norm="" width=80 fi ### pre-requisites # is user root? if [ ! "$(id -u)" -eq 0 ]; then consoleError 1 'You must be root to run this script.' fi # has a parameter been passed to this script? if [ -z "$1" ]; then scriptHelp fi # process startup parameters while [ $# -gt 0 ]; do case "$1" in -a | --archive) # name of backup archive if [ -z "$2" ]; then consoleError 1 "Please provide the name of the backup archive you want to work with or use '--list-all' to get a full list." fi archiveName="$2" shift ;; --exclude) # exclude files from restore if [ -z "$2" ]; then consoleError 1 "Please provide a list of exclusions in the proper borg format." fi exclusions="$2" shift ;; -f | --file) # specific file/pattern to restore if [ -z "$2" ]; then consoleError 1 'Please provide the name of the specific file/pattern for which you want to restore or search.' fi fileName="$2" shift ;; -h | -\? | --help) # display help scriptHelp ;; -i | --info) # show archive information operation='info' ;; -l | --list) # list contents of specific backup operation='viewarchive' ;; -la | --list-all) # list all backup archives operation='listall' ;; --makevars) # make a borgvars template file operation='makevars' ;; -p | --path) # path to restore files if [ -z "$2" ]; then consoleError 1 'Please specify a path where you want files restored.' fi restorePath="${2%/}" shift ;; --progress) # show progress commonOptions="$commonOptions --progress" ;; -r | --restore) # restore archive/file operation='restore' ;; -v | --vars) # location of borgvars file if [ -z "$2" ]; then consoleError 1 'Please provide the path to the file with your borg connection information.' elif [ ! -f "$2" ]; then consoleError 1 'The specified borg connection information file does not exist.' exit 1 fi varsFile="$2" shift ;; --verbose) # display each file being processed restoreOptions="$restoreOptions --list" ;; *) # invalid option printf "%s\nUnknown option: %s\n" "$err" "$1" printf "Use '--help' for valid options.\n\n%s" "$norm" exit 1 ;; esac shift done ### process 'makevars' operation if [ "$operation" = 'makevars' ]; then if ! printf "sshKeyFile=\nborgBaseDir=\nborgRepo=\nborgRepoPassword=\nborgRepoKey=\nborgRemote=\n" >./sample.borgvars; then consoleError 4 'Could not write sample borgvars file.' else exit 0 fi fi ### check parameter validity # no operation if [ -z "$operation" ]; then consoleError 1 'Nothing to do!' fi # no borg information file if [ -z "$varsFile" ]; then consoleError 1 'You must provide a valid .borgvars file with information about your borg repo.' fi # info without archive if [ "$operation" = 'info' ] && [ -z "$archiveName" ]; then consoleError 1 "Info operation requested but no archive name provided. Please use '--list-all' for a list of all available archives." fi # list without archive if [ "$operation" = 'viewarchive' ] && [ -z "$archiveName" ]; then consoleError 1 "List operation requested but no archive name provided. Please use '--list-all' for a list of all available archives." fi # restore with no path if [ "$operation" = 'restore' ] && [ -z "$restorePath" ]; then consoleError 1 "Restore operation requested but no restore path provided." # restore with no archive elif [ "$operation" = 'restore' ] && [ -z "$archiveName" ]; then consoleError 1 "Restore operation requested but no archive name provided." fi # file provided but no archive if [ "$fileName" ] && [ -z "$archiveName" ]; then consoleError 1 "Filename specified without an associated archive name." fi # clean-up leading spaces in option strings if [ "$commonOptions" ]; then commonOptions=${commonOptions##[[:space:]]}; fi if [ "$restoreOptions" ]; then restoreOptions=${restoreOptions##[[:space:]]}; fi # check/create restore path if [ "$operation" = 'restore' ]; then if [ -d "$restorePath" ]; then # convert to absolute path restorePath=$( cd "$restorePath" || consoleError 5 'Cannot access specified restore directory.' pwd -P ) if ! touch "${restorePath}/touch.test" >/dev/null 2>&1; then consoleError 5 'Cannot write to specified restore directory.' fi else if ! mkdir -p "${restorePath}" >/dev/null 2>&1; then consoleError 5 'Cannot create specified restore directory.' else # convert to absolute path restorePath=$( cd "$restorePath" || consoleError 5 'Cannot access specified restore directory.' pwd -P ) if ! touch "${restorePath}/touch.test" >/dev/null 2>&1; then consoleError 5 'Cannot write to specified restore directory.' fi fi fi fi ### read borg information file # check if file was provided as a relative or absolute path case "${varsFile}" in /*) # absolute path, no need to rewrite variable # shellcheck disable=SC1090 . "${varsFile}" ;; *) # relative path, prepend './' to create absolute path # shellcheck disable=SC1090 . "./${varsFile}" ;; esac # verify borg base directory if [ -z "${borgBaseDir}" ]; then consoleError 2 "$varsFile: 'borgBaseDir' is not specified." elif [ ! -d "${borgBaseDir}" ]; then consoleError 2 "$varsFile: 'borgBaseDir' does not exist." fi export BORG_BASE_DIR="${borgBaseDir%/}" ## check path to SSH keyfile # shellcheck disable=SC2154 if [ -z "${sshKeyFile}" ]; then consoleError 2 "$varsFile: 'sshKeyFile' is not specified." elif [ ! -f "${sshKeyFile}" ]; then consoleError 2 "$varsFile: 'sshKeyFile' does not exist." fi export BORG_RSH="ssh -i ${sshKeyFile}" # check borg repo connect string # shellcheck disable=SC2154 if [ -z "${borgRepo}" ]; then consoleError 2 "$varsFile: 'borgRepo' is not specified." fi export BORG_REPO="${borgRepo}" # check borg repo password # shellcheck disable=SC2154 if [ -n "${borgRepoPassword}" ]; then export BORG_PASSPHRASE="${borgRepoPassword}" elif [ "${borgRepoPassword}" = 'none' ]; then export BORG_PASSPHRASE="" else consoleError 2 "$varsFile: 'borgRepoPassword' must be specified or must be 'none' if no password has been set (VERY INSECURE!)." fi # check borg keyfile if supplied # shellcheck disable=SC2154 if [ -n "${borgRepoKey}" ]; then export BORG_KEY_FILE="${borgRepoKey}" fi # export borg remote path, if specified # shellcheck disable=SC2154 if [ -n "${borgRemote}" ]; then export BORG_REMOTE_PATH="${borgRemote}"; fi ### create borg temp dir: ## python requires a writable temporary directory when unpacking borg and ## executing commands. This defaults to /tmp but many systems mount /tmp with ## the 'noexec' option for security. Thus, we will use/create a 'tmp' folder ## within the BORG_BASE_DIR and instruct python to use that instead of /tmp # check if BORG_BASE_DIR/tmp exists, if not, create it if [ ! -d "${borgBaseDir}/tmp" ]; then if ! mkdir "${borgBaseDir}/tmp"; then consoleError 3 'Unable to create temp working directory for borg.' fi fi export TMPDIR="${borgBaseDir}/tmp" ### execute borg operations # info operations if [ "$operation" = 'info' ]; then borg info ::"${archiveName}" # list operations elif [ "$operation" = 'listall' ]; then borg list elif [ "$operation" = 'viewarchive' ]; then if [ "$fileName" ]; then borg list ::"${archiveName}" "${fileName}" else borg list ::"${archiveName}" fi # restore operations elif [ "$operation" = 'restore' ]; then cd "$restorePath" || consoleError 4 'Could not change to restore directory.' if [ -z "$fileName" ]; then # restore entire archive if [ "$exclusions" ]; then # shellcheck disable=SC2086 borg --show-rc ${commonOptions} extract ${restoreOptions} ::"${archiveName}" --exclude "${exclusions}" else # shellcheck disable=SC2086 borg --show-rc ${commonOptions} extract ${restoreOptions} ::"${archiveName}" fi elif [ "$fileName" ]; then # restore file/pattern if [ "$exclusions" ]; then # shellcheck disable=SC2086 borg --show-rc ${commonOptions} extract ${restoreOptions} ::"${archiveName}" "${fileName}" --exclude "${exclusions}" else # shellcheck disable=SC2086 borg --show-rc ${commonOptions} extract ${restoreOptions} ::"${archiveName}" "${fileName}" fi fi fi ### exit gracefully cleanup exit 0 ### exit codes # 0: no errors, script completed successfully # 1: parameter error (missing, non-existent or invalid input) # 2: parameter missing/invalid in .borgvars file # 3: could not create/remove borg tmp directory # 4: could not write sample borgvars file (permissions?) # 5: cannot access/create/write to restore path or could not remove test file #EOF