From 09f0ca6f12ac9cf103b42574bccd5957661edbb7 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 25 Oct 2025 18:45:25 +0100 Subject: [PATCH 1/9] add new dump and find functionalities --- restic-backup.sh | 60 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 2fbae58..101e6aa 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # ================================================================= -# Restic Backup Script v0.38.3 - 2025.10.18 +# Restic Backup Script v0.39 - 2025.10.25 # ================================================================= export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin @@ -9,7 +9,7 @@ set -euo pipefail umask 077 # --- Script Constants --- -SCRIPT_VERSION="0.38.3" +SCRIPT_VERSION="0.39" SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) CONFIG_FILE="${SCRIPT_DIR}/restic-backup.conf" LOCK_FILE="/tmp/restic-backup.lock" @@ -302,8 +302,10 @@ display_help() { printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--check-full" "Verify all repository data (slow)." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--forget" "Apply retention policy; optionally prune." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--unlock" "Remove stale repository locks." + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--dump " "Dump a single file from a snapshot to stdout." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--restore" "Interactive restore wizard." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--ls " "List files and directories inside a specific snapshot." + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--find " "Search for files/dirs across all snapshots (e.g., --find \"*.log\" -l)." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--background-restore" "Run a non-interactive restore in the background." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--sync-restore" "Run a non-interactive restore in the foreground (for cron)." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--dry-run" "Preview backup changes (no snapshot)." @@ -329,7 +331,8 @@ display_help() { log_message() { local message="$1" - local timestamp="$(date '+%Y-%m-%d %H:%M:%S')" + local timestamp + timestamp="$(date '+%Y-%m-%d %H:%M:%S')" echo "[$HOSTNAME] [$timestamp] $message" >> "$LOG_FILE" if [[ "${VERBOSE_MODE:-false}" == "true" ]]; then @@ -503,6 +506,37 @@ run_ls() { fi } +run_find() { + if [ $# -eq 0 ]; then + echo -e "${C_RED}Error: --find requires a pattern to search for.${C_RESET}" >&2 + echo -e "Example: ${C_GREEN}sudo $prog --find \"*.log\" -l -i${C_RESET}" >&2 + return 1 + fi + echo -e "${C_BOLD}--- Finding Files (searching all snapshots) ---${C_RESET}" + log_message "Running find with patterns: $@" + echo -e "${C_DIM}Searching... (use arrow keys to scroll, 'q' to quit)...${C_RESET}" + if ! restic find "$@" | less -f; then + echo -e "${C_RED}Error: Find command failed. Check your pattern(s).${C_RESET}" >&2 + return 1 + fi +} + +run_dump() { + if [ $# -ne 2 ]; then + echo -e "${C_RED}Error: --dump requires and .${C_RESET}" >&2 + echo -e "Example: ${C_GREEN}sudo $prog --dump latest /etc/hosts > hosts.txt${C_RESET}" >&2 + return 1 + fi + local snapshot_id="$1" + local file_path="$2" + log_message "Dumping file: $file_path from snapshot $snapshot_id" + if ! restic dump "$snapshot_id" "$file_path"; then + log_message "ERROR: Failed to dump file $file_path from $snapshot_id" + echo -e "${C_RED}❌ Failed to dump file. Check snapshot ID and path.${C_RESET}" >&2 + return 1 + fi +} + send_ntfy() { local title="$1" local tags="$2" @@ -534,8 +568,9 @@ 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 escaped_title escaped_message + escaped_title=$(echo "$title" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') + 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)" @@ -612,8 +647,9 @@ send_slack() { failure) color="#d50200" ;; *) color="#808080" ;; 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 escaped_title escaped_message + escaped_title=$(echo "$title" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') + 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 '{ "attachments": [ @@ -1625,6 +1661,11 @@ case "${1:-}" in run_preflight_checks "restore" "quiet" run_restore ;; + --dump) + run_preflight_checks "restore" "quiet" + shift + run_dump "$@" + ;; --background-restore) shift run_preflight_checks "restore" "quiet" @@ -1667,6 +1708,11 @@ case "${1:-}" in run_preflight_checks "backup" "quiet" run_snapshots_delete ;; + --find) + run_preflight_checks "backup" "quiet" + shift + run_find "$@" + ;; --stats) run_preflight_checks "backup" "quiet" run_stats From 471305f4016035d7681698bbf2cb105020923668 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 25 Oct 2025 19:42:16 +0100 Subject: [PATCH 2/9] fix: Improve user prompts and variable usage --- restic-backup.sh | 84 +++++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 101e6aa..716dcce 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -11,6 +11,7 @@ umask 077 # --- Script Constants --- SCRIPT_VERSION="0.39" SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +PROG_NAME=$(basename "$0"); readonly PROG_NAME CONFIG_FILE="${SCRIPT_DIR}/restic-backup.conf" LOCK_FILE="/tmp/restic-backup.lock" HOSTNAME=$(hostname -s) @@ -127,7 +128,7 @@ check_and_install_restic() { display_update_info "Restic" "$local_version" "$latest_version" "$release_notes" if [ -t 1 ]; then - read -p "Would you like to download and install it? (y/n): " confirm + read -rp "Would you like to download and install it? (y/n): " confirm if [[ "${confirm,,}" != "y" && "${confirm,,}" != "yes" ]]; then echo "Skipping installation." return 0 @@ -193,7 +194,7 @@ check_for_script_update() { echo -e "${C_YELLOW}Skipping script update check: 'jq' command not found.${C_RESET}" return 0 fi - echo -e "${C_BOLD}--- Checking for script updates ---${C_RESET}" + echo -e "${C_BOLD}--- Checking for script updates ---${C_RESET}" local SCRIPT_API_URL="https://api.github.com/repos/buildplan/restic-backup-script/releases/latest" local release_info release_info=$(curl -sL -H "Cache-Control: no-cache" -H "Pragma: no-cache" "$SCRIPT_API_URL") @@ -257,6 +258,7 @@ if [ ! -f "$CONFIG_FILE" ]; then echo -e "${C_RED}ERROR: Configuration file not found: $CONFIG_FILE${C_RESET}" >&2 exit 1 fi +# shellcheck source=restic-backup.conf source "$CONFIG_FILE" REQUIRED_VARS=( "RESTIC_REPOSITORY" @@ -277,14 +279,12 @@ done display_help() { local readme_url="https://github.com/buildplan/restic-backup-script/blob/main/README.md" - local prog - prog=$(basename "$0") echo -e "${C_BOLD}${C_CYAN}Restic Backup Script (v${SCRIPT_VERSION})${C_RESET}" echo "Encrypted, deduplicated backups with restic." echo echo -e "${C_BOLD}${C_YELLOW}USAGE:${C_RESET}" - echo -e " sudo $prog ${C_GREEN}[options] [command]${C_RESET}" + echo -e " sudo $PROG_NAME ${C_GREEN}[options] [command]${C_RESET}" echo echo -e "${C_BOLD}${C_YELLOW}OPTIONS:${C_RESET}" printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--verbose" "Show detailed live output." @@ -314,11 +314,13 @@ display_help() { printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--uninstall-scheduler" "Remove an automated schedule." echo echo -e "${C_BOLD}${C_YELLOW}QUICK EXAMPLES:${C_RESET}" - echo -e " Run a backup now: ${C_GREEN}sudo $prog${C_RESET}" - echo -e " Verbose diff summary: ${C_GREEN}sudo $prog --verbose --diff${C_RESET}" - echo -e " Fix perms (interactive): ${C_GREEN}sudo $prog --fix-permissions --test${C_RESET}" - echo -e " Background restore: ${C_GREEN}sudo $prog --background-restore latest /mnt/restore${C_RESET}" - echo -e " List snapshot contents: ${C_GREEN}sudo $prog --ls latest /path/to/dir${C_RESET}" + echo -e " Run a backup now: ${C_GREEN}sudo $PROG_NAME${C_RESET}" + echo -e " Verbose diff summary: ${C_GREEN}sudo $PROG_NAME --verbose --diff${C_RESET}" + echo -e " Fix perms (interactive): ${C_GREEN}sudo $PROG_NAME --fix-permissions --test${C_RESET}" + echo -e " Background restore: ${C_GREEN}sudo $PROG_NAME --background-restore latest /mnt/restore${C_RESET}" + echo -e " List snapshot contents: ${C_GREEN}sudo $PROG_NAME --ls latest /path/to/dir${C_RESET}" + echo -e " Find a file everywhere: ${C_GREEN}sudo $PROG_NAME --find \"*.log\" -l${C_RESET}" + echo -e " Dump one file to stdout: ${C_GREEN}sudo $PROG_NAME --dump latest /etc/hosts > hosts.txt${C_RESET}" echo echo -e "${C_BOLD}${C_YELLOW}DEPENDENCIES:${C_RESET}" echo -e " This script requires: ${C_GREEN}restic, curl, gpg, bzip2, less, jq, flock${C_RESET}" @@ -462,7 +464,7 @@ run_unlock() { if [ -n "$other_processes" ]; then echo -e "${C_YELLOW}WARNING: Another restic process appears to be running:${C_RESET}" echo "$other_processes" - read -p "Are you sure you want to proceed? This could interrupt a live backup. (y/n): " confirm + read -rp "Are you sure you want to proceed? This could interrupt a live backup. (y/n): " confirm if [[ "${confirm,,}" != "y" && "${confirm,,}" != "yes" ]]; then echo "Unlock cancelled by user." log_message "Unlock cancelled by user due to active processes." @@ -500,7 +502,8 @@ run_ls() { ls_cmd+=("${filter_paths[@]}") fi echo -e "${C_DIM}Displaying snapshot contents (use arrow keys to scroll, 'q' to quit)...${C_RESET}" - if ! "${ls_cmd[@]}" | less -f; then + "${ls_cmd[@]}" | less -f + if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo -e "${C_RED}Error: Failed to list contents for snapshot '${snapshot_id}'. Please check the ID and paths.${C_RESET}" >&2 return 1 fi @@ -509,13 +512,14 @@ run_ls() { run_find() { if [ $# -eq 0 ]; then echo -e "${C_RED}Error: --find requires a pattern to search for.${C_RESET}" >&2 - echo -e "Example: ${C_GREEN}sudo $prog --find \"*.log\" -l -i${C_RESET}" >&2 + echo -e "Example: ${C_GREEN}sudo $PROG_NAME --find \"*.log\" -l -i${C_RESET}" >&2 return 1 fi echo -e "${C_BOLD}--- Finding Files (searching all snapshots) ---${C_RESET}" log_message "Running find with patterns: $@" - echo -e "${C_DIM}Searching... (use arrow keys to scroll, 'q' to quit)...${C_RESET}" - if ! restic find "$@" | less -f; then + echo -e "${C_DIM}Searching... (use arrow keys to scroll, 'q' to quit)...${C_RESET}" + restic find "$@" | less -f + if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo -e "${C_RED}Error: Find command failed. Check your pattern(s).${C_RESET}" >&2 return 1 fi @@ -524,12 +528,12 @@ run_find() { run_dump() { if [ $# -ne 2 ]; then echo -e "${C_RED}Error: --dump requires and .${C_RESET}" >&2 - echo -e "Example: ${C_GREEN}sudo $prog --dump latest /etc/hosts > hosts.txt${C_RESET}" >&2 + echo -e "Example: ${C_GREEN}sudo $PROG_NAME --dump latest /etc/hosts > hosts.txt${C_RESET}" >&2 return 1 fi local snapshot_id="$1" local file_path="$2" - log_message "Dumping file: $file_path from snapshot $snapshot_id" + log_message "Dumping file: $file_path from snapshot $snapshot_id" if ! restic dump "$snapshot_id" "$file_path"; then log_message "ERROR: Failed to dump file $file_path from $snapshot_id" echo -e "${C_RED}❌ Failed to dump file. Check snapshot ID and path.${C_RESET}" >&2 @@ -635,7 +639,7 @@ send_teams() { send_slack() { local title="$1" - local status="$2" + local status="$2" local message="$3" if [[ "${SLACK_ENABLED:-false}" != "true" ]] || [ -z "${SLACK_WEBHOOK_URL:-}" ]; then return 0 @@ -914,14 +918,14 @@ run_install_scheduler() { echo -e " 1) ${C_GREEN}systemd timer${C_RESET} (Modern, recommended, more flexible logging)" echo -e " 2) ${C_CYAN}crontab${C_RESET} (Classic, simple, universally available)" local scheduler_choice - read -p "Enter your choice [1]: " scheduler_choice + read -rp "Enter your choice [1]: " scheduler_choice scheduler_choice=${scheduler_choice:-1} echo -e "\n${C_YELLOW}How often would you like the backup to run?${C_RESET}" echo -e " 1) ${C_GREEN}Once daily${C_RESET}" echo -e " 2) ${C_GREEN}Twice daily${C_RESET} (e.g., every 12 hours)" echo -e " 3) ${C_CYAN}Custom schedule${C_RESET} (provide your own expression)" local schedule_choice - read -p "Enter your choice [1]: " schedule_choice + read -rp "Enter your choice [1]: " schedule_choice schedule_choice=${schedule_choice:-1} local systemd_schedule cron_schedule @@ -929,7 +933,7 @@ run_install_scheduler() { 1) local daily_time while true; do - read -p "Enter the time to run the backup (24-hour HH:MM format) [03:00]: " daily_time + read -rp "Enter the time to run the backup (24-hour HH:MM format) [03:00]: " daily_time daily_time=${daily_time:-03:00} if [[ "$daily_time" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]]; then break; else echo -e "${C_RED}Invalid format. Please use HH:MM.${C_RESET}"; fi done @@ -940,12 +944,12 @@ run_install_scheduler() { 2) local time1 time2 while true; do - read -p "Enter the first time (24-hour HH:MM format) [03:00]: " time1 + read -rp "Enter the first time (24-hour HH:MM format) [03:00]: " time1 time1=${time1:-03:00} if [[ "$time1" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]]; then break; else echo -e "${C_RED}Invalid format. Please use HH:MM.${C_RESET}"; fi done while true; do - read -p "Enter the second time (24-hour HH:MM format) [15:30]: " time2 + read -rp "Enter the second time (24-hour HH:MM format) [15:30]: " time2 time2=${time2:-15:30} if [[ "$time2" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]]; then break; else echo -e "${C_RED}Invalid format. Please use HH:MM.${C_RESET}"; fi done @@ -956,13 +960,13 @@ run_install_scheduler() { ;; 3) if [[ "$scheduler_choice" == "1" ]]; then - read -p "Enter a custom systemd 'OnCalendar' expression: " systemd_schedule + read -rp "Enter a custom systemd 'OnCalendar' expression: " systemd_schedule if command -v systemd-analyze >/dev/null && ! systemd-analyze calendar "$systemd_schedule" --iterations=1 >/dev/null 2>&1; then echo -e "${C_RED}Warning: '$systemd_schedule' may be an invalid expression.${C_RESET}" fi else while true; do - read -p "Enter a custom cron expression (e.g., '0 4 * * *'): " cron_schedule + read -rp "Enter a custom cron expression (e.g., '0 4 * * *'): " cron_schedule if echo "$cron_schedule" | grep -qE '^([0-9*,/-]+\s){4}[0-9*,/-]+$'; then break else @@ -981,14 +985,14 @@ run_install_scheduler() { echo -e " ${C_DIM}Scheduler:${C_RESET} systemd timer" printf " ${C_DIM}Schedule:%b\n%s${C_RESET}\n" "${C_RESET}" "$systemd_schedule" echo - read -p "Proceed with installation? (y/n): " confirm + read -rp "Proceed with installation? (y/n): " confirm if [[ "${confirm,,}" != "y" ]]; then echo "Aborted."; return 1; fi install_systemd_timer "$script_path" "$systemd_schedule" "$CONFIG_FILE" else echo -e " ${C_DIM}Scheduler:${C_RESET} crontab" printf " ${C_DIM}Schedule:%b\n%s${C_RESET}\n" "${C_RESET}" "$cron_schedule" echo - read -p "Proceed with installation? (y/n): " confirm + read -rp "Proceed with installation? (y/n): " confirm if [[ "${confirm,,}" != "y" ]]; then echo "Aborted."; return 1; fi install_crontab "$script_path" "$cron_schedule" "$LOG_FILE" fi @@ -1002,7 +1006,7 @@ install_systemd_timer() { local timer_file="/etc/systemd/system/restic-backup.timer" if [ -f "$service_file" ] || [ -f "$timer_file" ]; then - read -p "Existing systemd files found. Overwrite? (y/n): " confirm + read -rp "Existing systemd files found. Overwrite? (y/n): " confirm if [[ "${confirm,,}" != "y" ]]; then echo "Aborted."; return 1; fi fi echo "Creating systemd service file: $service_file" @@ -1059,7 +1063,7 @@ install_crontab() { echo -e "${C_YELLOW}Existing cron file found at $cron_file:${C_RESET}" cat "$cron_file" echo - read -p "Add new schedule(s) to this file? (y/n): " confirm + read -rp "Add new schedule(s) to this file? (y/n): " confirm if [[ "${confirm,,}" != "y" ]]; then echo "Aborted." return 1 @@ -1132,7 +1136,7 @@ run_uninstall_scheduler() { echo " - $file" done echo - read -p "Are you sure you want to proceed? (y/n): " confirm + read -rp "Are you sure you want to proceed? (y/n): " confirm if [[ "${confirm,,}" != "y" ]]; then echo "Aborted by user." return 0 @@ -1216,12 +1220,12 @@ run_stats() { } run_backup() { - local start_time=$(date +%s) + local start_time; 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_log; backup_log=$(mktemp) local backup_success=false if run_with_priority "${backup_cmd[@]}" 2>&1 | tee "$backup_log"; then backup_success=true @@ -1237,8 +1241,8 @@ run_backup() { fi cat "$backup_log" >> "$LOG_FILE" rm -f "$backup_log" - local end_time=$(date +%s) - local duration=$((end_time - start_time)) + local end_time; end_time=$(date +%s) + local duration; duration=$((end_time - start_time)) if [ "$backup_success" = true ]; then log_message "Backup completed successfully" echo -e "${C_GREEN}✅ Backup completed${C_RESET}" @@ -1318,18 +1322,18 @@ run_restore() { echo "Available snapshots:" restic snapshots --compact echo - read -p "Enter snapshot ID to restore (or 'latest'): " snapshot_id + read -rp "Enter snapshot ID to restore (or 'latest'): " snapshot_id if [ -z "$snapshot_id" ]; then echo "No snapshot specified, exiting" return 0 fi local list_confirm - read -p "Would you like to list the contents of this snapshot to find exact paths? (y/n): " list_confirm + read -rp "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 - read -p "Enter restore destination (absolute path): " restore_dest + read -rp "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 0 @@ -1351,7 +1355,7 @@ run_restore() { echo -e "\n${C_RED}${C_BOLD}WARNING: Restoring to critical system directory '$restore_dest'${C_RESET}" echo -e "${C_RED}This could damage your system or make it unbootable!${C_RESET}" local confirm - read -p "${C_YELLOW}Type 'DANGEROUS' to proceed or anything else to cancel: ${C_RESET}" confirm + read -rp "${C_YELLOW}Type 'DANGEROUS' to proceed or anything else to cancel: ${C_RESET}" confirm if [[ "$confirm" != "DANGEROUS" ]]; then echo -e "${C_GREEN}Restore cancelled for safety.${C_RESET}" return 0 @@ -1359,7 +1363,7 @@ run_restore() { log_message "WARNING: User confirmed dangerous restore to: $restore_dest" fi local include_paths=() - read -p "Optional: Enter specific file(s) to restore, separated by spaces (leave blank for full restore): " -a include_paths + read -rp "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) if [ ${#include_paths[@]} -gt 0 ]; then for path in "${include_paths[@]}"; do @@ -1374,7 +1378,7 @@ run_restore() { fi echo -e "${C_BOLD}--- Dry Run Complete ---${C_RESET}" local proceed_confirm - read -p "Proceed with the actual restore? (y/n): " proceed_confirm + read -rp "Proceed with the actual restore? (y/n): " proceed_confirm if [[ "${proceed_confirm,,}" != "y" && "${proceed_confirm,,}" != "yes" ]]; then echo "Restore cancelled by user." return 0 From 8e8b0bd0cacd3db3505d079a50915cfd8fe43750 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 25 Oct 2025 20:05:08 +0100 Subject: [PATCH 3/9] fix: Update variable usage in prompts --- restic-backup.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 716dcce..a5b79a6 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -516,7 +516,7 @@ run_find() { return 1 fi echo -e "${C_BOLD}--- Finding Files (searching all snapshots) ---${C_RESET}" - log_message "Running find with patterns: $@" + log_message "Running find with patterns: $*" echo -e "${C_DIM}Searching... (use arrow keys to scroll, 'q' to quit)...${C_RESET}" restic find "$@" | less -f if [ "${PIPESTATUS[0]}" -ne 0 ]; then @@ -1474,13 +1474,13 @@ run_background_restore() { echo -e "${C_RED}Error: Destination must be a non-empty, absolute path. Aborting.${C_RESET}" >&2 exit 1 fi - local restore_log="/tmp/restic-restore-${snapshot_id:0:8}-$(date +%s).log" + local restore_log; restore_log="/tmp/restic-restore-${snapshot_id:0:8}-$(date +%s).log" echo "Restore job started. Details will be logged to: ${restore_log}" log_message "Starting background restore of snapshot ${snapshot_id} to ${restore_dest}. See ${restore_log} for details." ( - local start_time=$(date +%s) + local start_time; start_time=$(date +%s) if _run_restore_command "$@"; then - local end_time=$(date +%s) + local end_time; end_time=$(date +%s) local duration=$((end_time - start_time)) _handle_restore_ownership "$restore_dest" log_message "Background restore SUCCESS: ${snapshot_id} to ${restore_dest} in ${duration}s." @@ -1526,7 +1526,7 @@ run_snapshots_delete() { fi echo local -a ids_to_delete - read -p "Enter snapshot ID(s) to delete, separated by spaces: " -a ids_to_delete + read -rp "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 0 @@ -1536,7 +1536,7 @@ run_snapshots_delete() { echo " - $id" done echo - read -p "Are you absolutely sure you want to PERMANENTLY delete these snapshots? (Type 'yes' to confirm): " confirm + read -rp "Are you absolutely sure you want to PERMANENTLY delete these snapshots? (Type 'yes' to confirm): " confirm if [[ "$confirm" != "yes" ]]; then echo "Confirmation not received. Aborting deletion." return 0 @@ -1551,7 +1551,7 @@ run_snapshots_delete() { echo -e "${C_RED}❌ Failed to delete snapshots.${C_RESET}" >&2 return 1 fi - read -p "Would you like to run 'prune' now to reclaim disk space? (y/n): " prune_confirm + read -rp "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}" log_message "Running prune after manual forget" From 8828496367c4970e44f0302504fe1fb3fc413fff Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 25 Oct 2025 20:21:47 +0100 Subject: [PATCH 4/9] better error handling in find function --- restic-backup.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index a5b79a6..87fa0f0 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -328,6 +328,7 @@ display_help() { echo -e "Config: ${C_DIM}${CONFIG_FILE}${C_RESET} Log: ${C_DIM}${LOG_FILE}${C_RESET}" echo echo -e "For full details, see the online documentation: \e]8;;${readme_url}\a${C_CYAN}README.md${C_RESET}\e]8;;\a" + echo -e "${C_YELLOW}Note:${C_RESET} For restic official documentation See: https://restic.readthedocs.io/" echo } @@ -518,11 +519,19 @@ run_find() { echo -e "${C_BOLD}--- Finding Files (searching all snapshots) ---${C_RESET}" log_message "Running find with patterns: $*" echo -e "${C_DIM}Searching... (use arrow keys to scroll, 'q' to quit)...${C_RESET}" - restic find "$@" | less -f + local find_stderr; find_stderr=$(mktemp) + restic find "$@" 2> >(tee "$find_stderr" >&2) | less -f if [ "${PIPESTATUS[0]}" -ne 0 ]; then - echo -e "${C_RED}Error: Find command failed. Check your pattern(s).${C_RESET}" >&2 + echo -e "${C_RED}Error: Find command failed.${C_RESET}" >&2 + if [ -s "$find_stderr" ]; then + echo -e "${C_YELLOW}--- restic error output ---${C_RESET}" >&2 + cat "$find_stderr" >&2 + echo -e "${C_YELLOW}--------------------------${C_RESET}" >&2 + fi + rm -f "$find_stderr" return 1 fi + rm -f "$find_stderr" } run_dump() { From b3a8d5f2640b6564d01b0de8615d54e6008b6f45 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 25 Oct 2025 20:35:07 +0100 Subject: [PATCH 5/9] messages for successful dump --- restic-backup.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/restic-backup.sh b/restic-backup.sh index 87fa0f0..0051e5a 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -548,6 +548,8 @@ run_dump() { echo -e "${C_RED}❌ Failed to dump file. Check snapshot ID and path.${C_RESET}" >&2 return 1 fi + echo -e "${C_GREEN}✅ Successfully dumped:${C_RESET} ${C_BOLD}${file_path}${C_RESET} ${C_GREEN}from snapshot${C_RESET} ${C_BOLD}${snapshot_id}${C_RESET}" >&2 + echo -e "${C_DIM} (File content was sent to stdout for redirection)${C_RESET}" >&2 } send_ntfy() { From bb5eb8d3fd0613de3404d5dddbe10048bf74e353 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 25 Oct 2025 21:33:26 +0100 Subject: [PATCH 6/9] Add recovery kit generation --- restic-backup.sh | 75 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 0051e5a..78fd8cf 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -311,6 +311,7 @@ display_help() { printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--dry-run" "Preview backup changes (no snapshot)." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--test" "Validate config, permissions, connectivity." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--install-scheduler" "Install an automated schedule (systemd/cron)." + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--recovery-kit" "Generate a self-contained recovery script (with embedded password)." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--uninstall-scheduler" "Remove an automated schedule." echo echo -e "${C_BOLD}${C_YELLOW}QUICK EXAMPLES:${C_RESET}" @@ -503,7 +504,7 @@ run_ls() { ls_cmd+=("${filter_paths[@]}") fi echo -e "${C_DIM}Displaying snapshot contents (use arrow keys to scroll, 'q' to quit)...${C_RESET}" - "${ls_cmd[@]}" | less -f + "${ls_cmd[@]}" | less -fR if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo -e "${C_RED}Error: Failed to list contents for snapshot '${snapshot_id}'. Please check the ID and paths.${C_RESET}" >&2 return 1 @@ -520,7 +521,7 @@ run_find() { log_message "Running find with patterns: $*" echo -e "${C_DIM}Searching... (use arrow keys to scroll, 'q' to quit)...${C_RESET}" local find_stderr; find_stderr=$(mktemp) - restic find "$@" 2> >(tee "$find_stderr" >&2) | less -f + restic find "$@" 2> >(tee "$find_stderr" >&2) | less -fR if [ "${PIPESTATUS[0]}" -ne 0 ]; then echo -e "${C_RED}Error: Find command failed.${C_RESET}" >&2 if [ -s "$find_stderr" ]; then @@ -1342,7 +1343,7 @@ run_restore() { read -rp "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") + less -fR <(restic ls -l "$snapshot_id") fi read -rp "Enter restore destination (absolute path): " restore_dest if [[ -z "$restore_dest" || "$restore_dest" != /* ]]; then @@ -1578,6 +1579,70 @@ run_snapshots_delete() { fi } +recovery_kit() { + echo -e "${C_BOLD}--- Generating Disaster Recovery Kit ---${C_RESET}" + local recovery_pass + if ! recovery_pass=$(cat "$RESTIC_PASSWORD_FILE"); then + echo -e "${C_RED}Error: Could not read password file: $RESTIC_PASSWORD_FILE${C_RESET}" >&2 + return 1 + fi + if [ -z "$recovery_pass" ]; then + echo -e "${C_RED}Error: Password file is empty: $RESTIC_PASSWORD_FILE${C_RESET}" >&2 + return 1 + fi + local recovery_file backup_sources_str + recovery_file="${SCRIPT_DIR}/restic-recovery-kit-${HOSTNAME}-$(date +%Y%m%d).sh" + backup_sources_str="${BACKUP_SOURCES[*]}" + + cat > "$recovery_file" << EOF +#!/usr/bin/env bash +# ================================================================= +# --- Restic Emergency Recovery Kit --- +# ================================================================= +# Generated by $0 on $(date) for host $HOSTNAME +# +# !! WARNING: This file contains your repository password in plain text !! +# !! Store it securely (e.g., encrypted USB, password manager) !! +# +# To use: +# 1. Install restic on a new system: +# (e.g.,) curl -L https://github.com/restic/restic/releases/latest/download/restic_latest_linux_amd64.bz2 | bunzip2 > restic +# (e.g.,) chmod +x restic && sudo mv restic /usr/local/bin/ +# +# 2. Make this script executable: chmod +x $recovery_file +# 3. Run this script OR manually export the variables. +# 4. Restore your data. + +# --- Embedded Configuration --- +export RESTIC_REPOSITORY="${RESTIC_REPOSITORY}" +export RESTIC_PASSWORD="${recovery_pass}" + +# --- Repository Info (for reference) --- +echo "--- Repository Information ---" +echo "Repository: \$RESTIC_REPOSITORY" +echo "Backed up host: $HOSTNAME" +echo "Original backup sources: ${backup_sources_str}" +echo "" + +# --- Example Commands --- +echo "--- Listing Snapshots (run 'restic snapshots') ---" +restic snapshots +echo "" +echo "--- Example Restore Command (MODIFY AS NEEDED) ---" +echo "To restore the latest snapshot to /mnt/restore, uncomment and run:" +# restic restore latest --target /mnt/restore +echo "" +echo "To restore a specific directory from the latest snapshot:" +# restic restore latest --target /mnt/restore --include "/home/user_files" + +EOF + + chmod 400 "$recovery_file" + echo -e "\n${C_GREEN}✅ Recovery Kit generated: ${C_BOLD}${recovery_file}${C_RESET}" + echo -e "${C_BOLD}${C_RED}WARNING: This file contains your repository password.${C_RESET}" + echo -e "${C_YELLOW}Store this file securely and OFFLINE (e.g., encrypted USB, password manager).${C_RESET}" +} + # ================================================================= # MAIN SCRIPT EXECUTION # ================================================================= @@ -1663,6 +1728,10 @@ case "${1:-}" in run_preflight_checks "backup" "verbose" echo -e "${C_GREEN}✅ All tests passed${C_RESET}" ;; + --recovery-kit) + run_preflight_checks "backup" "quiet" + recovery_kit + ;; --snapshots) run_preflight_checks "backup" "quiet" run_snapshots From 002278d0e0d64b8a4ec3968d05e74fa507c16500 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 25 Oct 2025 21:50:02 +0100 Subject: [PATCH 7/9] Improve error handling and recovery kit generation --- restic-backup.sh | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 78fd8cf..dd6dac8 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -505,7 +505,8 @@ run_ls() { fi echo -e "${C_DIM}Displaying snapshot contents (use arrow keys to scroll, 'q' to quit)...${C_RESET}" "${ls_cmd[@]}" | less -fR - if [ "${PIPESTATUS[0]}" -ne 0 ]; then + local ls_status; ls_status=${PIPESTATUS[0]} + if [ "$ls_status" -ne 0 ]; then echo -e "${C_RED}Error: Failed to list contents for snapshot '${snapshot_id}'. Please check the ID and paths.${C_RESET}" >&2 return 1 fi @@ -522,7 +523,8 @@ run_find() { echo -e "${C_DIM}Searching... (use arrow keys to scroll, 'q' to quit)...${C_RESET}" local find_stderr; find_stderr=$(mktemp) restic find "$@" 2> >(tee "$find_stderr" >&2) | less -fR - if [ "${PIPESTATUS[0]}" -ne 0 ]; then + local restic_find_status; restic_find_status=${PIPESTATUS[0]} + if [ "$restic_find_status" -ne 0 ]; then echo -e "${C_RED}Error: Find command failed.${C_RESET}" >&2 if [ -s "$find_stderr" ]; then echo -e "${C_YELLOW}--- restic error output ---${C_RESET}" >&2 @@ -1593,8 +1595,12 @@ recovery_kit() { local recovery_file backup_sources_str recovery_file="${SCRIPT_DIR}/restic-recovery-kit-${HOSTNAME}-$(date +%Y%m%d).sh" backup_sources_str="${BACKUP_SOURCES[*]}" - - cat > "$recovery_file" << EOF + local tmpfile + tmpfile=$(mktemp) || { + echo -e "${C_RED}ERROR: Could not create temporary file for recovery kit.${C_RESET}" >&2 + return 1 + } + cat > "$tmpfile" << EOF #!/usr/bin/env bash # ================================================================= # --- Restic Emergency Recovery Kit --- @@ -1609,7 +1615,7 @@ recovery_kit() { # (e.g.,) curl -L https://github.com/restic/restic/releases/latest/download/restic_latest_linux_amd64.bz2 | bunzip2 > restic # (e.g.,) chmod +x restic && sudo mv restic /usr/local/bin/ # -# 2. Make this script executable: chmod +x $recovery_file +# 2. Make this script executable: chmod +x ${recovery_file##*/} # 3. Run this script OR manually export the variables. # 4. Restore your data. @@ -1636,8 +1642,8 @@ echo "To restore a specific directory from the latest snapshot:" # restic restore latest --target /mnt/restore --include "/home/user_files" EOF - - chmod 400 "$recovery_file" + chmod 400 "$tmpfile" + mv -f "$tmpfile" "$recovery_file" echo -e "\n${C_GREEN}✅ Recovery Kit generated: ${C_BOLD}${recovery_file}${C_RESET}" echo -e "${C_BOLD}${C_RED}WARNING: This file contains your repository password.${C_RESET}" echo -e "${C_YELLOW}Store this file securely and OFFLINE (e.g., encrypted USB, password manager).${C_RESET}" From 5039b7894ca7918357627ff59ab23f864b88444d Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 25 Oct 2025 22:03:49 +0100 Subject: [PATCH 8/9] new commands for dumping files, find and recovery kit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2f07b4e..2e04d70 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ For those familiar with setting up backup scripts, here is a fast track to get y - `sudo ./restic-backup.sh --install-scheduler` — Interactive schedule wizard (systemd/cron). - `sudo ./restic-backup.sh --uninstall-scheduler` — Remove the installed schedule. - `sudo ./restic-backup.sh --restore` — Interactive restore wizard with dry-run preview. +- `sudo ./restic-backup.sh --dump ` - Dump a single file from a snapshot to stdout. - `sudo ./restic-backup.sh --background-restore ` — Non-blocking background restore (logs to /tmp). - `sudo ./restic-backup.sh --sync-restore ` — Blocking restore suitable for cron/automation. - `sudo ./restic-backup.sh --forget` — Apply retention policy (optionally prunes). @@ -97,6 +98,8 @@ For those familiar with setting up backup scripts, here is a fast track to get y - `sudo ./restic-backup.sh --snapshots` — List snapshots. - `sudo ./restic-backup.sh --snapshots-delete` — Interactively delete snapshots (irreversible). - `sudo ./restic-backup.sh --ls [snapshot_id] [path ...]` — List files/dirs within a snapshot (paged with `less`). +- `sudo ./restic-backup.sh --find ` - Search for files/dirs across all snapshots (e.g., --find \"*.log\" -l). +- `sudo ./restic-backup.sh --recovery-kit` - Generate a self-contained recovery script (with embedded password). Tip: `--verbose` is interactive; cron should use the default quiet mode. The script auto-reexecs with sudo if not run as root. From ba5783ab5e19297e9df2b0e9ddad9f1d8fd021c7 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 25 Oct 2025 22:04:51 +0100 Subject: [PATCH 9/9] checksum v0.39 --- 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 a023f7a..3d8b3fd 100644 --- a/restic-backup.sh.sha256 +++ b/restic-backup.sh.sha256 @@ -1 +1 @@ -568618563ca890afec48f97cb9bc88fc36d2735cd4d031951713ea4bdd8e4806 restic-backup.sh +6fa38405aef2bdeb4f6418630601ab1d91021c59ddbeea7fcfa219d39cc477c8 restic-backup.sh