From b5484f4ce497e6dc86b5fa4c92825572d08f53e6 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 18:42:29 +0100 Subject: [PATCH 01/10] optional jq dependency check --- restic-backup.sh | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 82324c2..69c420b 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash # ================================================================= -# Restic Backup Script v0.26 - 2025.09.11 +# Restic Backup Script v0.27 - 2025.09.12 # ================================================================= set -euo pipefail umask 077 # --- Script Constants --- -SCRIPT_VERSION="0.26" +SCRIPT_VERSION="0.27" SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) CONFIG_FILE="${SCRIPT_DIR}/restic-backup.conf" LOCK_FILE="/tmp/restic-backup.lock" @@ -294,11 +294,6 @@ build_backup_command() { run_diff() { echo -e "${C_BOLD}--- Generating Backup Summary ---${C_RESET}" log_message "Generating backup summary (diff)" - if ! command -v jq >/dev/null 2>&1; then - echo -e "${C_YELLOW}jq not found; install jq for JSON parsing (apt/dnf install jq).${C_RESET}" >&2 - log_message "WARNING: jq not installed; cannot run JSON-based diff summary." - return 1 - fi local path_args=() for p in "${BACKUP_SOURCES[@]}"; do path_args+=(--path "$p") @@ -524,7 +519,7 @@ run_preflight_checks() { # System Dependencies if [[ "$verbosity" == "verbose" ]]; then echo -e "\n ${C_DIM}- Checking System Dependencies${C_RESET}" - printf " %-65s" "Required commands (restic, curl, flock)..." + printf " %-65s" "Required commands (restic, curl, flock)..." fi local required_cmds=(restic curl flock) for cmd in "${required_cmds[@]}"; do @@ -533,6 +528,13 @@ run_preflight_checks() { fi done if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi + if [[ "$mode" == "diff" ]]; then + if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "jq command for --diff..."; fi + if ! command -v jq &>/dev/null; then + handle_failure "'jq' is required for the --diff command. Install on Debian based system with sudo apt install jq" + fi + if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi + fi # Configuration Files if [[ "$verbosity" == "verbose" ]]; then echo -e "\n ${C_DIM}- Checking Configuration Files${C_RESET}"; fi From 367234432f600299c4424169c471721bfd3828e9 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 18:58:24 +0100 Subject: [PATCH 02/10] cleanup --- restic-backup.sh | 120 ----------------------------------------------- 1 file changed, 120 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 69c420b..b98bbe2 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -75,7 +75,6 @@ import_restic_key() { check_and_install_restic() { echo -e "${C_BOLD}--- Checking Restic Version ---${C_RESET}" - if ! command -v bzip2 &>/dev/null || ! command -v curl &>/dev/null || ! command -v gpg &>/dev/null; then echo -e "${C_RED}ERROR: 'bzip2', 'curl', and 'gpg' are required for secure auto-installation.${C_RESET}" >&2 echo -e "${C_YELLOW}On Debian based systems install with: sudo apt-get install bzip2 curl gnupg${C_RESET}" >&2 @@ -217,18 +216,13 @@ if [ ! -f "$CONFIG_FILE" ]; then echo -e "${C_RED}ERROR: Configuration file not found: $CONFIG_FILE${C_RESET}" >&2 exit 1 fi - -# Source configuration file source "$CONFIG_FILE" - -# Validate required configuration REQUIRED_VARS=( "RESTIC_REPOSITORY" "RESTIC_PASSWORD_FILE" "BACKUP_SOURCES" "LOG_FILE" ) - for var in "${REQUIRED_VARS[@]}"; do if [ -z "${!var:-}" ]; then echo -e "${C_RED}ERROR: Required configuration variable '$var' is not set${C_RESET}" >&2 @@ -362,7 +356,6 @@ run_diff() { run_snapshots() { echo -e "${C_BOLD}--- Listing Snapshots ---${C_RESET}" log_message "Listing all snapshots" - if ! restic snapshots; then log_message "ERROR: Failed to list snapshots" echo -e "${C_RED}❌ Failed to list snapshots. Check repository connection and credentials.${C_RESET}" >&2 @@ -373,22 +366,17 @@ run_snapshots() { run_unlock() { echo -e "${C_BOLD}--- Unlocking Repository ---${C_RESET}" log_message "Attempting to unlock repository" - local lock_info lock_info=$(restic list locks --repo "$RESTIC_REPOSITORY" --password-file "$RESTIC_PASSWORD_FILE") - if [ -z "$lock_info" ]; then echo -e "${C_GREEN}✅ No locks found. Repository is clean.${C_RESET}" log_message "No stale locks found." return 0 fi - echo -e "${C_YELLOW}Found stale locks in the repository:${C_RESET}" echo "$lock_info" - local other_processes other_processes=$(ps aux | grep 'restic ' | grep -v 'grep' || true) - if [ -n "$other_processes" ]; then echo -e "${C_YELLOW}WARNING: Another restic process appears to be running:${C_RESET}" echo "$other_processes" @@ -401,7 +389,6 @@ run_unlock() { else echo -e "${C_GREEN}✅ No other active restic processes found. It is safe to proceed.${C_RESET}" fi - echo "Attempting to remove stale locks..." if restic unlock --repo "$RESTIC_REPOSITORY" --password-file "$RESTIC_PASSWORD_FILE"; then echo -e "${C_GREEN}✅ Repository unlocked successfully.${C_RESET}" @@ -418,11 +405,9 @@ send_ntfy() { local tags="$2" local priority="$3" local message="$4" - if [[ "${NTFY_ENABLED:-false}" != "true" ]] || [ -z "${NTFY_TOKEN:-}" ] || [ -z "${NTFY_URL:-}" ]; then return 0 fi - curl -s --max-time 15 \ -u ":$NTFY_TOKEN" \ -H "Title: $title" \ @@ -436,11 +421,9 @@ send_discord() { local title="$1" local status="$2" local message="$3" - if [[ "${DISCORD_ENABLED:-false}" != "true" ]] || [ -z "${DISCORD_WEBHOOK_URL:-}" ]; then return 0 fi - local color case "$status" in success) color=3066993 ;; @@ -448,14 +431,11 @@ send_discord() { failure) color=15158332 ;; *) color=9807270 ;; esac - local escaped_title=$(echo "$title" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') local escaped_message=$(echo "$message" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') - local json_payload printf -v json_payload '{"embeds": [{"title": "%s", "description": "%s", "color": %d, "timestamp": "%s"}]}' \ "$escaped_title" "$escaped_message" "$color" "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" - curl -s --max-time 15 \ -H "Content-Type: application/json" \ -d "$json_payload" \ @@ -468,17 +448,13 @@ send_notification() { local ntfy_priority="$3" local discord_status="$4" local message="$5" - send_ntfy "$title" "$tags" "$ntfy_priority" "$message" send_discord "$title" "$discord_status" "$message" } setup_environment() { - # Export restic environment variables export RESTIC_REPOSITORY export RESTIC_PASSWORD_FILE - - # Create exclude file from patterns if [ -n "${EXCLUDE_PATTERNS:-}" ]; then EXCLUDE_TEMP_FILE=$(mktemp) echo "$EXCLUDE_PATTERNS" | tr ' ' '\n' > "$EXCLUDE_TEMP_FILE" @@ -486,10 +462,7 @@ setup_environment() { } cleanup() { - # Remove temporary files [ -n "${EXCLUDE_TEMP_FILE:-}" ] && rm -f "$EXCLUDE_TEMP_FILE" - - # Release lock if [ -n "${LOCK_FD:-}" ]; then flock -u "$LOCK_FD" fi @@ -498,7 +471,6 @@ cleanup() { run_preflight_checks() { local mode="${1:-backup}" local verbosity="${2:-quiet}" - # Helper function for failure handle_failure() { local error_message="$1" @@ -511,11 +483,9 @@ run_preflight_checks() { "${NTFY_PRIORITY_FAILURE}" "failure" "$error_message" exit 1 } - if [[ "$verbosity" == "verbose" ]]; then echo -e "${C_BOLD}--- Running Pre-flight Checks ---${C_RESET}" fi - # System Dependencies if [[ "$verbosity" == "verbose" ]]; then echo -e "\n ${C_DIM}- Checking System Dependencies${C_RESET}" @@ -535,16 +505,13 @@ run_preflight_checks() { fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi fi - # Configuration Files if [[ "$verbosity" == "verbose" ]]; then echo -e "\n ${C_DIM}- Checking Configuration Files${C_RESET}"; fi - if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Password file ('$RESTIC_PASSWORD_FILE')..."; fi if [ ! -r "$RESTIC_PASSWORD_FILE" ]; then handle_failure "Password file not found or not readable: $RESTIC_PASSWORD_FILE" fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi - if [ -n "${EXCLUDE_FILE:-}" ]; then if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Exclude file ('$EXCLUDE_FILE')..."; fi if [ ! -r "$EXCLUDE_FILE" ]; then @@ -552,16 +519,13 @@ run_preflight_checks() { fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi fi - if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Log file writability ('$LOG_FILE')..."; fi if ! touch "$LOG_FILE" >/dev/null 2>&1; then handle_failure "The log file or its directory is not writable: ${LOG_FILE}" fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi - # Repository State if [[ "$verbosity" == "verbose" ]]; then echo -e "\n ${C_DIM}- Checking Repository State${C_RESET}"; fi - if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Repository connectivity and credentials..."; fi if ! restic cat config >/dev/null 2>&1; then if [[ "$mode" == "init" ]]; then @@ -571,7 +535,6 @@ run_preflight_checks() { handle_failure "Cannot access repository. Check credentials or run --init first." fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi - if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Stale repository locks..."; fi local lock_info lock_info=$(restic list locks 2>/dev/null || true) @@ -584,7 +547,6 @@ run_preflight_checks() { else if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi fi - # Backup Sources if [[ "$mode" == "backup" || "$mode" == "diff" ]]; then if [[ "$verbosity" == "verbose" ]]; then echo -e "\n ${C_DIM}- Checking Backup Sources${C_RESET}"; fi @@ -599,7 +561,6 @@ run_preflight_checks() { if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi done fi - if [[ "$verbosity" == "quiet" ]]; then echo -e "${C_GREEN}✅ Pre-flight checks passed.${C_RESET}" fi @@ -609,21 +570,16 @@ rotate_log() { if [ ! -f "$LOG_FILE" ]; then return 0 fi - local max_size_bytes=$(( ${MAX_LOG_SIZE_MB:-10} * 1024 * 1024 )) local log_size - if command -v stat >/dev/null 2>&1; then log_size=$(stat -f%z "$LOG_FILE" 2>/dev/null || stat -c%s "$LOG_FILE" 2>/dev/null || echo 0) else log_size=0 fi - if [ "$log_size" -gt "$max_size_bytes" ]; then mv "$LOG_FILE" "${LOG_FILE}.$(date +%Y%m%d_%H%M%S)" touch "$LOG_FILE" - - # Clean old rotated logs find "$(dirname "$LOG_FILE")" -name "$(basename "$LOG_FILE").*" \ -type f -mtime +"${LOG_RETENTION_DAYS:-30}" -delete 2>/dev/null || true fi @@ -631,14 +587,11 @@ rotate_log() { run_with_priority() { local cmd=("$@") - if [ "${LOW_PRIORITY:-true}" = "true" ]; then local priority_cmd=(nice -n "${NICE_LEVEL:-19}") - if command -v ionice >/dev/null 2>&1; then priority_cmd+=(ionice -c "${IONICE_CLASS:-3}") fi - priority_cmd+=("${cmd[@]}") "${priority_cmd[@]}" else @@ -652,14 +605,11 @@ run_with_priority() { init_repository() { echo -e "${C_BOLD}--- Initializing Repository ---${C_RESET}" - if restic cat config >/dev/null 2>&1; then echo -e "${C_YELLOW}Repository already exists${C_RESET}" return 0 fi - log_message "Initializing new repository: $RESTIC_REPOSITORY" - if restic init; then log_message "Repository initialized successfully" echo -e "${C_GREEN}✅ Repository initialized${C_RESET}" @@ -676,24 +626,17 @@ init_repository() { run_backup() { local start_time=$(date +%s) - echo -e "${C_BOLD}--- Starting Backup ---${C_RESET}" log_message "Starting backup of: ${BACKUP_SOURCES[*]}" - local backup_cmd=() mapfile -t backup_cmd < <(build_backup_command) - local backup_log=$(mktemp) local backup_success=false - if run_with_priority "${backup_cmd[@]}" 2>&1 | tee "$backup_log"; then backup_success=true fi - - # Parse backup results local files_new files_changed files_unmodified local data_added data_processed - if grep -q "Files:" "$backup_log"; then files_new=$(grep "Files:" "$backup_log" | tail -1 | awk '{print $2}') files_changed=$(grep "Files:" "$backup_log" | tail -1 | awk '{print $4}') @@ -701,17 +644,13 @@ run_backup() { data_added=$(grep "Added to the repository:" "$backup_log" | tail -1 | awk '{print $5" "$6}') data_processed=$(grep "processed" "$backup_log" | tail -1 | awk '{print $1" "$2}') fi - cat "$backup_log" >> "$LOG_FILE" rm -f "$backup_log" - local end_time=$(date +%s) local duration=$((end_time - start_time)) - if [ "$backup_success" = true ]; then log_message "Backup completed successfully" echo -e "${C_GREEN}✅ Backup completed${C_RESET}" - local stats_msg printf -v stats_msg "Files: %s new, %s changed, %s unmodified\nData added: %s\nDuration: %dm %ds" \ "${files_new:-0}" \ @@ -720,7 +659,6 @@ run_backup() { "${data_added:-Not applicable}" \ "$((duration / 60))" \ "$((duration % 60))" - send_notification "Backup SUCCESS: $HOSTNAME" "white_check_mark" \ "${NTFY_PRIORITY_SUCCESS}" "success" "$stats_msg" else @@ -735,7 +673,6 @@ run_backup() { run_forget() { echo -e "${C_BOLD}--- Cleaning Old Snapshots ---${C_RESET}" log_message "Running retention policy" - local forget_cmd=(restic forget) [ -n "${KEEP_LAST:-}" ] && forget_cmd+=(--keep-last "$KEEP_LAST") [ -n "${KEEP_DAILY:-}" ] && forget_cmd+=(--keep-daily "$KEEP_DAILY") @@ -743,7 +680,6 @@ run_forget() { [ -n "${KEEP_MONTHLY:-}" ] && forget_cmd+=(--keep-monthly "$KEEP_MONTHLY") [ -n "${KEEP_YEARLY:-}" ] && forget_cmd+=(--keep-yearly "$KEEP_YEARLY") [ "${PRUNE_AFTER_FORGET:-true}" = "true" ] && forget_cmd+=(--prune) - if run_with_priority "${forget_cmd[@]}" 2>&1 | tee -a "$LOG_FILE"; then log_message "Retention policy applied successfully" echo -e "${C_GREEN}✅ Old snapshots cleaned${C_RESET}" @@ -758,7 +694,6 @@ run_forget() { run_check() { echo -e "${C_BOLD}--- Checking Repository Integrity ---${C_RESET}" log_message "Running integrity check" - if restic check --read-data-subset=5% 2>&1 | tee -a "$LOG_FILE"; then log_message "Integrity check passed" echo -e "${C_GREEN}✅ Repository integrity OK${C_RESET}" @@ -772,35 +707,25 @@ run_check() { run_restore() { echo -e "${C_BOLD}--- Restore Mode ---${C_RESET}" - - # List available snapshots echo "Available snapshots:" restic snapshots --compact echo - - # Get snapshot ID read -p "Enter snapshot ID to restore (or 'latest'): " snapshot_id if [ -z "$snapshot_id" ]; then echo "No snapshot specified, exiting" return 1 fi - - # Offer to list snapshot contents local list_confirm read -p "Would you like to list the contents of this snapshot to find exact paths? (y/n): " list_confirm if [[ "${list_confirm,,}" == "y" || "${list_confirm,,}" == "yes" ]]; then echo -e "${C_DIM}Displaying snapshot contents (use arrow keys to scroll, 'q' to quit)...${C_RESET}" less -f <(restic ls -l "$snapshot_id") fi - - # Get restore destination read -p "Enter restore destination (absolute path): " restore_dest if [[ -z "$restore_dest" || "$restore_dest" != /* ]]; then echo -e "${C_RED}Error: Must be a non-empty, absolute path. Aborting.${C_RESET}" >&2 return 1 fi - - # Ask for specific paths to include local include_paths=() read -p "Optional: Enter specific file(s) to restore, separated by spaces (leave blank for full restore): " -a include_paths local restic_cmd=(restic restore "$snapshot_id" --target "$restore_dest" --verbose) @@ -810,39 +735,28 @@ run_restore() { done echo -e "${C_YELLOW}Will restore only the specified paths...${C_RESET}" fi - - # Perform a dry run for user confirmation echo -e "${C_BOLD}\n--- Performing Dry Run (No changes will be made) ---${C_RESET}" if ! "${restic_cmd[@]}" --dry-run; then echo -e "${C_RED}❌ Dry run failed. Aborting restore.${C_RESET}" >&2 return 1 fi echo -e "${C_BOLD}--- Dry Run Complete ---${C_RESET}" - - # Ask for final confirmation local proceed_confirm read -p "Proceed with the actual restore? (y/n): " proceed_confirm if [[ "${proceed_confirm,,}" != "y" && "${proceed_confirm,,}" != "yes" ]]; then echo "Restore cancelled by user." return 0 fi - - # Create destination if it doesn't exist and perform the restore mkdir -p "$restore_dest" echo -e "${C_BOLD}--- Performing Restore ---${C_RESET}" log_message "Restoring snapshot $snapshot_id to $restore_dest" - - # Restore Logic local restore_log restore_log=$(mktemp) local restore_success=false - if "${restic_cmd[@]}" 2>&1 | tee "$restore_log"; then restore_success=true fi cat "$restore_log" >> "$LOG_FILE" - - # Handle failure of the restic command if [ "$restore_success" = false ]; then log_message "ERROR: Restore failed" echo -e "${C_RED}❌ Restore failed${C_RESET}" >&2 @@ -851,8 +765,6 @@ run_restore() { rm -f "$restore_log" return 1 fi - - # Check if the restore was successful if grep -q "Summary: Restored 0 files/dirs" "$restore_log"; then echo -e "\n${C_YELLOW}⚠️ Restore completed, but no files were restored.${C_RESET}" echo -e "${C_YELLOW}This usually means the specific path(s) you provided do not exist in this snapshot.${C_RESET}" @@ -882,8 +794,6 @@ run_restore() { send_notification "Restore SUCCESS: $HOSTNAME" "white_check_mark" \ "${NTFY_PRIORITY_SUCCESS}" "success" "Restored $snapshot_id to $restore_dest" fi - - # Clean up the temporary log file rm -f "$restore_log" } @@ -891,25 +801,18 @@ run_snapshots_delete() { echo -e "${C_BOLD}--- Interactively Delete Snapshots ---${C_RESET}" echo -e "${C_BOLD}${C_RED}WARNING: This operation is permanent and cannot be undone.${C_RESET}" echo - - # List available snapshots for the user echo "Available snapshots:" if ! restic snapshots --compact; then echo -e "${C_RED}❌ Could not list snapshots. Aborting.${C_RESET}" >&2 return 1 fi echo - - # Prompt user for snapshot IDs local -a ids_to_delete read -p "Enter snapshot ID(s) to delete, separated by spaces: " -a ids_to_delete - if [ ${#ids_to_delete[@]} -eq 0 ]; then echo "No snapshot IDs entered. Aborting." return 1 fi - - # Final confirmation echo -e "\nYou have selected the following ${C_YELLOW}${#ids_to_delete[@]} snapshot(s)${C_RESET} for deletion:" for id in "${ids_to_delete[@]}"; do echo " - $id" @@ -921,8 +824,6 @@ run_snapshots_delete() { echo "Confirmation not received. Aborting deletion." return 0 fi - - # Execute the forget command echo -e "${C_BOLD}--- Deleting Snapshots ---${C_RESET}" log_message "User initiated deletion of snapshots: ${ids_to_delete[*]}" @@ -934,8 +835,6 @@ run_snapshots_delete() { echo -e "${C_RED}❌ Failed to delete snapshots.${C_RESET}" >&2 return 1 fi - - # Offer to run prune read -p "Would you like to run 'prune' now to reclaim disk space? (y/n): " prune_confirm if [[ "${prune_confirm,,}" == "y" || "${prune_confirm,,}" == "yes" ]]; then echo -e "${C_BOLD}--- Pruning Repository ---${C_RESET}" @@ -956,32 +855,21 @@ run_snapshots_delete() { # MAIN SCRIPT EXECUTION # ================================================================= -# Check for script updates (interactive mode only) check_for_script_update - -# Check for Restic and update if necessary check_and_install_restic - -# Set up signal handlers trap cleanup EXIT trap 'send_notification "Backup Crashed: $HOSTNAME" "x" "${NTFY_PRIORITY_FAILURE}" "failure" "Backup script terminated unexpectedly"' ERR - -# Parse command line arguments VERBOSE_MODE=false if [[ "${1:-}" == "--verbose" ]]; then VERBOSE_MODE=true shift fi - -# Acquire lock exec 200>"$LOCK_FILE" if ! flock -n 200; then echo -e "${C_RED}Another backup is already running${C_RESET}" >&2 exit 5 fi LOCK_FD=200 - -# Set up environment setup_environment rotate_log @@ -1041,23 +929,15 @@ case "${1:-}" in display_help exit 1 fi - - # Default: full backup run_preflight_checks "backup" "quiet" - log_message "=== Starting backup run ===" - if run_backup; then - # Only run forget/check if backup was successful run_forget - if [ "${CHECK_AFTER_BACKUP:-false}" = "true" ]; then run_check fi fi - log_message "=== Backup run completed ===" ;; esac - echo -e "${C_BOLD}--- Backup Script Completed ---${C_RESET}" From fb42f3d3757215777798716f58fb2f9ff5571dd0 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 19:34:06 +0100 Subject: [PATCH 03/10] added full integrity check and healthcheck --- restic-backup.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/restic-backup.sh b/restic-backup.sh index b98bbe2..3d4587d 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -248,6 +248,7 @@ display_help() { printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--snapshots" "List all available snapshots in the repository." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--snapshots-delete" "Interactively select and permanently delete specific snapshots." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--check" "Verify repository integrity by checking a subset of data." + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--check-full" "Run a FULL, slow check verifying all repository data." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--forget" "Manually apply the retention policy and prune old data." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--unlock" "Forcibly remove stale locks from the repository." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--restore" "Start the interactive restore wizard." @@ -705,6 +706,21 @@ run_check() { fi } +run_check_full() { + echo -e "${C_BOLD}--- Checking Repository Integrity (Full Data Scan) ---${C_RESET}" + echo -e "${C_YELLOW}⚠️ This will read ALL data and may be slow and consume significant bandwidth.${C_RESET}" + log_message "Running FULL integrity check (--read-data)" + if restic check --read-data 2>&1 | tee -a "$LOG_FILE"; then + log_message "Full integrity check passed" + echo -e "${C_GREEN}✅ Repository integrity OK (Full data scan complete).${C_RESET}" + else + log_message "CRITICAL: Full integrity check FAILED" + echo -e "${C_RED}❌ CRITICAL: Full integrity check FAILED.${C_RESET}" >&2 + send_notification "Repository Check FAILED: $HOSTNAME" "x" \ + "${NTFY_PRIORITY_FAILURE}" "failure" "CRITICAL: A full repository integrity check (--read-data) has failed!" + fi +} + run_restore() { echo -e "${C_BOLD}--- Restore Mode ---${C_RESET}" echo "Available snapshots:" @@ -904,6 +920,10 @@ case "${1:-}" in run_preflight_checks "backup" "quiet" run_check ;; + --check-full) + run_preflight_checks "backup" "quiet" + run_check_full + ;; --forget) run_preflight_checks "backup" "quiet" run_forget @@ -939,5 +959,12 @@ case "${1:-}" in fi log_message "=== Backup run completed ===" ;; + # --- Ping Healthchecks.io on success --- + if [[ -n "${HEALTHCHECKS_URL:-}" ]]; then + log_message "Pinging Healthchecks.io to signal successful run." + curl -fsS -m 15 --retry 3 "${HEALTHCHECKS_URL}" >/dev/null 2>>"$LOG_FILE" + fi + ;; esac + echo -e "${C_BOLD}--- Backup Script Completed ---${C_RESET}" From 8a97e03f90594bb0b42010dd358d75e492d74e53 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 19:38:39 +0100 Subject: [PATCH 04/10] added healthcheck --- restic-backup.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 3d4587d..6600cbd 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -958,13 +958,13 @@ case "${1:-}" in fi fi log_message "=== Backup run completed ===" + + # --- Ping Healthchecks.io on success --- + if [[ -n "${HEALTHCHECKS_URL:-}" ]]; then + log_message "Pinging Healthchecks.io to signal successful run." + curl -fsS -m 15 --retry 3 "${HEALTHCHECKS_URL}" >/dev/null 2>>"$LOG_FILE" + fi ;; - # --- Ping Healthchecks.io on success --- - if [[ -n "${HEALTHCHECKS_URL:-}" ]]; then - log_message "Pinging Healthchecks.io to signal successful run." - curl -fsS -m 15 --retry 3 "${HEALTHCHECKS_URL}" >/dev/null 2>>"$LOG_FILE" - fi - ;; esac echo -e "${C_BOLD}--- Backup Script Completed ---${C_RESET}" From f3f7eb527ce9715adc9b9aef974a6308bcf9fdd8 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 19:40:09 +0100 Subject: [PATCH 05/10] healthcheck.io --- restic-backup.conf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/restic-backup.conf b/restic-backup.conf index 61ef8a7..06be2e2 100644 --- a/restic-backup.conf +++ b/restic-backup.conf @@ -73,6 +73,13 @@ NTFY_PRIORITY_FAILURE=4 # Discord webhook (if enabled) DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/your/webhook_url_here" +# restic-backup.conf + +# --- Healthchecks.io --- +# URL for the "dead man's switch" service to ping on successful completion. +# Leave blank to disable. +# HEALTHCHECKS_URL="https://hc-ping.com/your-unique-uuid-here" + # --- Maintenance --- # Run integrity check after backup CHECK_AFTER_BACKUP=false From 41db9c8c2c5216db82abf603a9d5c46330e6bdc1 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 19:45:43 +0100 Subject: [PATCH 06/10] log notification failure --- restic-backup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restic-backup.sh b/restic-backup.sh index 6600cbd..cc199a4 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -874,7 +874,7 @@ run_snapshots_delete() { check_for_script_update check_and_install_restic trap cleanup EXIT -trap 'send_notification "Backup Crashed: $HOSTNAME" "x" "${NTFY_PRIORITY_FAILURE}" "failure" "Backup script terminated unexpectedly"' ERR +trap 'log_message "FATAL: Script terminated unexpectedly on line $LINENO. Sending crash notification."; send_notification "Backup Crashed: $HOSTNAME" "x" "${NTFY_PRIORITY_FAILURE}" "failure" "Backup script terminated unexpectedly on line $LINENO."' ERR VERBOSE_MODE=false if [[ "${1:-}" == "--verbose" ]]; then VERBOSE_MODE=true From 678edc94912b5840e2796314cfeb9ec47c545b1d Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 20:01:42 +0100 Subject: [PATCH 07/10] added full check flag --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 183b13a..2b0832b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ This script automates encrypted, deduplicated backups of local directories to a - `sudo ./restic-backup.sh --verbose` - Run with live progress and detailed output. - `sudo ./restic-backup.sh --dry-run` - Preview changes without creating a new snapshot. - `sudo ./restic-backup.sh --check` - Verify repository integrity by checking a subset of data. + - `sudo ./restic-backup.sh --check-full` - Run a full check verifying all repository data. - `sudo ./restic-backup.sh --test` - Validate configuration, permissions, and SSH connectivity. - `sudo ./restic-backup.sh --restore` - Start the interactive restore wizard. - `sudo ./restic-backup.sh --forget` - Manually apply the retention policy and prune old data. @@ -256,6 +257,9 @@ To run the backup automatically, edit the root crontab. # Run the retention/prune job every Sunday at 4:00 AM 0 4 * * 0 /root/scripts/backup/restic-backup.sh --forget > /dev/null 2>&1 + # Cron job for a monthly full check (e.g., first Sunday of the month at 3 AM) + 0 3 * * 0 [ $(date +\%d) -le 07 ] && /root/scripts/backup/restic-backup.sh --check-full > /dev/null 2>&1 + ``` *For pune job in your `restic-backup.conf`, set `PRUNE_AFTER_FORGET=true`.* *For more details on how forget flag work, see the [official Restic documentation on removing snapshots](https://restic.readthedocs.io/en/stable/060_forget.html).* From c6b4ebf08f2bc49c00bd905d779988eddc9aad92 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 20:17:47 +0100 Subject: [PATCH 08/10] error codes --- restic-backup.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index cc199a4..724db1b 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -475,6 +475,7 @@ run_preflight_checks() { # Helper function for failure handle_failure() { local error_message="$1" + local exit_code="${2:-1}" local notification_title="Pre-flight Check FAILED: $HOSTNAME" local full_error_message="ERROR: $error_message" log_message "$full_error_message" @@ -482,7 +483,7 @@ run_preflight_checks() { echo -e "${C_RED}$full_error_message${C_RESET}" >&2 send_notification "$notification_title" "x" \ "${NTFY_PRIORITY_FAILURE}" "failure" "$error_message" - exit 1 + exit "$exit_code" } if [[ "$verbosity" == "verbose" ]]; then echo -e "${C_BOLD}--- Running Pre-flight Checks ---${C_RESET}" @@ -495,14 +496,14 @@ run_preflight_checks() { local required_cmds=(restic curl flock) for cmd in "${required_cmds[@]}"; do if ! command -v "$cmd" &>/dev/null; then - handle_failure "Required command '$cmd' not found." + handle_failure "Required command '$cmd' not found." "10" fi done if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi if [[ "$mode" == "diff" ]]; then if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "jq command for --diff..."; fi if ! command -v jq &>/dev/null; then - handle_failure "'jq' is required for the --diff command. Install on Debian based system with sudo apt install jq" + handle_failure "'jq' is required for the --diff command. Install on Debian based system with sudo apt install jq" "10" fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi fi @@ -510,19 +511,19 @@ run_preflight_checks() { if [[ "$verbosity" == "verbose" ]]; then echo -e "\n ${C_DIM}- Checking Configuration Files${C_RESET}"; fi if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Password file ('$RESTIC_PASSWORD_FILE')..."; fi if [ ! -r "$RESTIC_PASSWORD_FILE" ]; then - handle_failure "Password file not found or not readable: $RESTIC_PASSWORD_FILE" + handle_failure "Password file not found or not readable: $RESTIC_PASSWORD_FILE" "11" fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi if [ -n "${EXCLUDE_FILE:-}" ]; then if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Exclude file ('$EXCLUDE_FILE')..."; fi if [ ! -r "$EXCLUDE_FILE" ]; then - handle_failure "The specified EXCLUDE_FILE is not readable: ${EXCLUDE_FILE}" + handle_failure "The specified EXCLUDE_FILE is not readable: ${EXCLUDE_FILE}" "14" fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi fi if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Log file writability ('$LOG_FILE')..."; fi if ! touch "$LOG_FILE" >/dev/null 2>&1; then - handle_failure "The log file or its directory is not writable: ${LOG_FILE}" + handle_failure "The log file or its directory is not writable: ${LOG_FILE}" "15" fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi # Repository State @@ -533,7 +534,7 @@ run_preflight_checks() { if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_YELLOW} SKIP ${C_RESET}] (OK for --init mode)"; fi return 0 fi - handle_failure "Cannot access repository. Check credentials or run --init first." + handle_failure "Cannot access repository. Check credentials or run --init first." "12" fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Stale repository locks..."; fi @@ -557,7 +558,7 @@ run_preflight_checks() { for source in "${BACKUP_SOURCES[@]}"; do if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Source directory ('$source')..."; fi if [ ! -d "$source" ] || [ ! -r "$source" ]; then - handle_failure "Source directory not found or not readable: $source" + handle_failure "Source directory not found or not readable: $source" "13" fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi done From e6ff7eeb6650e227da73ee3310c101b209233953 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 20:31:57 +0100 Subject: [PATCH 09/10] Healthcheck.io --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2b0832b..7880719 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This script automates encrypted, deduplicated backups of local directories to a - **Multiple Operation Modes**: Supports standard backups, dry runs, integrity checks, difference summaries, and a safe, interactive restore mode. - **Concurrency Control & Logging**: Prevents multiple instances from running simultaneously and handles its own log rotation. - **Pre-run Validation**: Performs checks for required commands and repository connectivity before execution. + - **Cron Job Monitoring**: Optional integration with [Healthchecks.io](https://healthchecks.io) for alerts if a backup job fails to run on schedule. ----- From b21bcf827ee353bb7bd605effa1a88562d9e8050 Mon Sep 17 00:00:00 2001 From: buildplan Date: Fri, 12 Sep 2025 20:36:18 +0100 Subject: [PATCH 10/10] sha256 for v0.27 --- restic-backup.sh.sha256 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restic-backup.sh.sha256 b/restic-backup.sh.sha256 index cea9c4b..7224e16 100644 --- a/restic-backup.sh.sha256 +++ b/restic-backup.sh.sha256 @@ -1 +1 @@ -0be741aeb52b448cd5cffbf976d76d06161642a33b8ec7781752a882c7e02ecb restic-backup.sh +5b586ba6d0b173cdf62f0eadf8fd2d63b57cb2923d65ecff32b8111fbe400c7c restic-backup.sh