Simple Restic SH Backup Script w/ Hooks

Hello! I have been a user and others have helped me along the way. I want to share with the community as a Freebsd user who likes to keep it simple. I have a simple sh script used to execute a Restic backup weekly. I have pre and post hooks for some simple diff checks between snapshots, restic --read-data and restic forget/prune. Anyone is free to use it, make it better, give suggestions if they feel like it. Here it is!

#!/bin/sh
# restic-backup.sh — Clean FreeBSD-style restic backup script with hook support

SCRIPT_NAME="restic"

: "${LOG_FACILITY:=local6}"
: "${LOG_LEVEL:=info}"

CONFIG_FILE="/usr/local/etc/restic-backup.conf"
[ -n "$1" ] && CONFIG_FILE="$1"
[ ! -f "$CONFIG_FILE" ] && { logger -t "$SCRIPT_NAME" -p "$LOG_FACILITY.error" "Missing config: $CONFIG_FILE"; exit 1; }

. "$CONFIG_FILE"

log() {
  logger -t "$SCRIPT_NAME" -p "$LOG_FACILITY.$LOG_LEVEL" "$1"
}

log_err() {
  logger -t "$SCRIPT_NAME" -p "$LOG_FACILITY.error" "$1"
}

run_hooks() {
  local priority="$1"
  shift
  for hook in "$@"; do
    [ -f "$hook" ] || { log_err "Hook not found: $hook"; continue; }
    "$hook" 2>&1 | logger -t "$SCRIPT_NAME" -p "$priority"
  done
}

command -v restic >/dev/null 2>&1 || { log_err "restic not found in PATH"; exit 1; }

export RESTIC_REPOSITORY="sftp:${REMOTE_USER}@${REMOTE_SERVER}:${REMOTE_REPO_PATH}"
export RESTIC_PASSWORD_FILE="$PASSWORD_FILE"

log "Starting restic backup"

restic cat config >/dev/null 2>&1 || {
  log_err "Restic repository inaccessible or uninitialized"
  exit 1
}

[ -n "$PRE_HOOKS" ] && run_hooks "$LOG_FACILITY.$LOG_LEVEL" $PRE_HOOKS

# POSIX-safe alternative to eval, native to FreeBSD sh
set -- $(printf '%s\n' "$BACKUP_OPTIONS $BACKUP_PATHS")

if restic backup "$@" 2>&1 | sed '/^[[:space:]]*$/d' | logger -t "$SCRIPT_NAME" -p "$LOG_FACILITY.$LOG_LEVEL"; then
  log "Backup completed successfully"
  [ -n "$POST_HOOKS_SUCCESS" ] && run_hooks "$LOG_FACILITY.$LOG_LEVEL" $POST_HOOKS_SUCCESS
else
  log_err "Backup failed"
  [ -n "$POST_HOOKS_FAILURE" ] && run_hooks "$LOG_FACILITY.error" $POST_HOOKS_FAILURE
fi

.conf file

# Remote repository settings
REMOTE_USER=""
REMOTE_SERVER=""
REMOTE_REPO_PATH=""
PASSWORD_FILE=""
# Logging settings
LOG_FACILITY=""
LOG_LEVEL=""
BACKUP_PATHS=""
# Additional restic options
BACKUP_OPTIONS="--exclude-caches --one-file-system"
# Hooks (optional)
# space-separated paths to executable scripts
PRE_HOOKS="/usr/local/bin/restic-check.sh"
POST_HOOKS_SUCCESS="/usr/local/bin/restic-diff.sh /usr/local/bin/restic-clean.sh"
POST_HOOKS_FAILURE=""

pre_hooks:

#!/bin/sh
restic check --read-data-subset=10% 2>&1 | sed '/^[[:space:]]*$/d'

post_hooks_success:

#!/bin/sh
# resticdiff.sh — compare the latest two restic snapshots

snapshots=$(restic snapshots --json 2>&1)
snapshot_ids=$(echo "$snapshots" | grep -o '"short_id":"[^"]*"' | cut -d'"' -f4 | tail -2)

from=$(echo "$snapshot_ids" | head -1)
to=$(echo "$snapshot_ids" | tail -1)

[ -z "$from" ] || [ -z "$to" ] && {
    echo "Not enough snapshots to compare"
    exit 0
}

restic diff "$from" "$to" 2>&1 | sed '/^[[:space:]]*$/d'

last :

#!/bin/sh
(restic forget -c --keep-last 30 --keep-within 1y && restic prune) 2>&1 | sed '/^[[:space:]]*$/d'

Strictly speaking the short_id field is deprecated, please always use the full id instead. “Parsing” the json output using grep is also rather brittle and might break when future restic versions make unrelated changes. Just use jq instead.

Thanks for the advice!