|
1 | 1 | #!/usr/bin/env bash |
2 | 2 |
|
3 | 3 | # ================================================================= |
4 | | -# Restic Backup Script v0.41 - 2025.11.24 |
| 4 | +# Restic Backup Script v0.42 - 2025.12.17 |
5 | 5 | # ================================================================= |
6 | 6 |
|
7 | | -export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
| 7 | +export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH |
8 | 8 | set -euo pipefail |
9 | 9 | umask 077 |
10 | 10 |
|
11 | 11 | # --- Script Constants --- |
12 | | -SCRIPT_VERSION="0.41" |
| 12 | +SCRIPT_VERSION="0.42" |
13 | 13 | SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) |
14 | 14 | PROG_NAME=$(basename "$0"); readonly PROG_NAME |
15 | 15 | CONFIG_FILE="${SCRIPT_DIR}/restic-backup.conf" |
|
35 | 35 | C_CYAN='' |
36 | 36 | fi |
37 | 37 |
|
| 38 | +# --- Ensure running as root --- |
| 39 | +display_help() { |
| 40 | + local readme_url="https://github.com/buildplan/restic-backup-script/blob/main/README.md" |
| 41 | + |
| 42 | + echo -e "${C_BOLD}${C_CYAN}Restic Backup Script (v${SCRIPT_VERSION})${C_RESET}" |
| 43 | + echo "Encrypted, deduplicated backups with restic." |
| 44 | + echo |
| 45 | + echo -e "${C_BOLD}${C_YELLOW}USAGE:${C_RESET}" |
| 46 | + echo -e " sudo $PROG_NAME ${C_GREEN}[options] [command]${C_RESET}" |
| 47 | + echo |
| 48 | + echo -e "${C_BOLD}${C_YELLOW}OPTIONS:${C_RESET}" |
| 49 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--verbose" "Show detailed live output." |
| 50 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--fix-permissions" "Interactive only: auto-fix 600/400 on conf/secret." |
| 51 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--help, -h" "Display this help message." |
| 52 | + echo |
| 53 | + echo -e "${C_BOLD}${C_YELLOW}COMMANDS:${C_RESET}" |
| 54 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "[no command]" "Run a standard backup and apply the retention policy." |
| 55 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--init" "Initialize a new restic repository (one-time setup)." |
| 56 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--diff" "Show a summary of changes between the last two snapshots." |
| 57 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--snapshots" "List all available snapshots in the repository." |
| 58 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--snapshots-delete" "Interactively select and permanently delete snapshots." |
| 59 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--stats" "Display repository size and file counts." |
| 60 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--check" "Verify repository integrity (subset)." |
| 61 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--check-full" "Verify all repository data (slow)." |
| 62 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--forget" "Apply retention policy; optionally prune." |
| 63 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--unlock" "Remove stale repository locks." |
| 64 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--dump <id> <path>" "Dump a single file from a snapshot to stdout." |
| 65 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--restore" "Interactive restore wizard." |
| 66 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--ls <snapshot_id>" "List files and directories inside a specific snapshot." |
| 67 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--find <pattern...>" "Search for files/dirs across all snapshots (e.g., --find \"*.log\" -l)." |
| 68 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--background-restore" "Run a non-interactive restore in the background." |
| 69 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--sync-restore" "Run a non-interactive restore in the foreground (for cron)." |
| 70 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--dry-run" "Preview backup changes (no snapshot)." |
| 71 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--test" "Validate config, permissions, connectivity." |
| 72 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--install-scheduler" "Install an automated schedule (systemd/cron)." |
| 73 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--recovery-kit" "Generate a self-contained recovery script (with embedded password)." |
| 74 | + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--uninstall-scheduler" "Remove an automated schedule." |
| 75 | + echo |
| 76 | + echo -e "${C_BOLD}${C_YELLOW}QUICK EXAMPLES:${C_RESET}" |
| 77 | + echo -e " Run a backup now: ${C_GREEN}sudo $PROG_NAME${C_RESET}" |
| 78 | + echo -e " Verbose diff summary: ${C_GREEN}sudo $PROG_NAME --verbose --diff${C_RESET}" |
| 79 | + echo -e " Fix perms (interactive): ${C_GREEN}sudo $PROG_NAME --fix-permissions --test${C_RESET}" |
| 80 | + echo -e " Background restore: ${C_GREEN}sudo $PROG_NAME --background-restore latest /mnt/restore${C_RESET}" |
| 81 | + echo -e " List snapshot contents: ${C_GREEN}sudo $PROG_NAME --ls latest /path/to/dir${C_RESET}" |
| 82 | + echo -e " Find a file everywhere: ${C_GREEN}sudo $PROG_NAME --find \"*.log\" -l${C_RESET}" |
| 83 | + echo -e " Dump one file to stdout: ${C_GREEN}sudo $PROG_NAME --dump latest /etc/hosts > hosts.txt${C_RESET}" |
| 84 | + echo |
| 85 | + echo -e "${C_BOLD}${C_YELLOW}DEPENDENCIES:${C_RESET}" |
| 86 | + echo -e " This script requires: ${C_GREEN}restic, curl, gpg, bzip2, less, jq, flock${C_RESET}" |
| 87 | + echo |
| 88 | + local display_log |
| 89 | + if [ -r "$CONFIG_FILE" ]; then |
| 90 | + # shellcheck source=/dev/null disable=SC2153 |
| 91 | + display_log=$(source "$CONFIG_FILE" >/dev/null 2>&1; echo "$LOG_FILE") |
| 92 | + else |
| 93 | + display_log="(requires sudo to read config)" |
| 94 | + fi |
| 95 | + echo -e "Config: ${C_DIM}${CONFIG_FILE}${C_RESET} Log: ${C_DIM}${display_log:-"(not set)"}${C_RESET}" |
| 96 | + echo |
| 97 | + echo -e "For full details, see the online documentation: \e]8;;${readme_url}\a${C_CYAN}README.md${C_RESET}\e]8;;\a" |
| 98 | + echo -e "${C_YELLOW}Note:${C_RESET} For restic official documentation See: https://restic.readthedocs.io/" |
| 99 | + echo |
| 100 | +} |
| 101 | +# Scan arguments for help flag immediately |
| 102 | +for arg in "$@"; do |
| 103 | + if [[ "$arg" == "-h" || "$arg" == "--help" ]]; then |
| 104 | + display_help |
| 105 | + exit 0 |
| 106 | + fi |
| 107 | +done |
| 108 | + |
38 | 109 | # --- Ensure running as root --- |
39 | 110 | if [[ $EUID -ne 0 ]]; then |
40 | 111 | echo -e "${C_BOLD}${C_YELLOW}This script requires root privileges.${C_RESET}" |
@@ -194,7 +265,7 @@ check_and_install_restic() { |
194 | 265 | echo "Decompressing and installing to /usr/local/bin/restic..." |
195 | 266 | if bunzip2 -c "$temp_binary" > /usr/local/bin/restic.tmp; then |
196 | 267 | chmod +x /usr/local/bin/restic.tmp |
197 | | - mv /usr/local/bin/restic.tmp /usr/local/bin/restic |
| 268 | + mv /usr/local/bin/restic.tmp /usr/local/bin/restic |
198 | 269 | if [[ "$IS_SELINUX_DISTRO" == "true" ]] && command -v restorecon &>/dev/null; then |
199 | 270 | echo "Applying SELinux context to binary..." |
200 | 271 | restorecon -v /usr/local/bin/restic || true |
@@ -296,62 +367,6 @@ done |
296 | 367 | # UTILITY FUNCTIONS |
297 | 368 | # ================================================================= |
298 | 369 |
|
299 | | -display_help() { |
300 | | - local readme_url="https://github.com/buildplan/restic-backup-script/blob/main/README.md" |
301 | | - |
302 | | - echo -e "${C_BOLD}${C_CYAN}Restic Backup Script (v${SCRIPT_VERSION})${C_RESET}" |
303 | | - echo "Encrypted, deduplicated backups with restic." |
304 | | - echo |
305 | | - echo -e "${C_BOLD}${C_YELLOW}USAGE:${C_RESET}" |
306 | | - echo -e " sudo $PROG_NAME ${C_GREEN}[options] [command]${C_RESET}" |
307 | | - echo |
308 | | - echo -e "${C_BOLD}${C_YELLOW}OPTIONS:${C_RESET}" |
309 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--verbose" "Show detailed live output." |
310 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--fix-permissions" "Interactive only: auto-fix 600/400 on conf/secret." |
311 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--help, -h" "Display this help message." |
312 | | - echo |
313 | | - echo -e "${C_BOLD}${C_YELLOW}COMMANDS:${C_RESET}" |
314 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "[no command]" "Run a standard backup and apply the retention policy." |
315 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--init" "Initialize a new restic repository (one-time setup)." |
316 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--diff" "Show a summary of changes between the last two snapshots." |
317 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--snapshots" "List all available snapshots in the repository." |
318 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--snapshots-delete" "Interactively select and permanently delete snapshots." |
319 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--stats" "Display repository size and file counts." |
320 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--check" "Verify repository integrity (subset)." |
321 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--check-full" "Verify all repository data (slow)." |
322 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--forget" "Apply retention policy; optionally prune." |
323 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--unlock" "Remove stale repository locks." |
324 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--dump <id> <path>" "Dump a single file from a snapshot to stdout." |
325 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--restore" "Interactive restore wizard." |
326 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--ls <snapshot_id>" "List files and directories inside a specific snapshot." |
327 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--find <pattern...>" "Search for files/dirs across all snapshots (e.g., --find \"*.log\" -l)." |
328 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--background-restore" "Run a non-interactive restore in the background." |
329 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--sync-restore" "Run a non-interactive restore in the foreground (for cron)." |
330 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--dry-run" "Preview backup changes (no snapshot)." |
331 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--test" "Validate config, permissions, connectivity." |
332 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--install-scheduler" "Install an automated schedule (systemd/cron)." |
333 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--recovery-kit" "Generate a self-contained recovery script (with embedded password)." |
334 | | - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--uninstall-scheduler" "Remove an automated schedule." |
335 | | - echo |
336 | | - echo -e "${C_BOLD}${C_YELLOW}QUICK EXAMPLES:${C_RESET}" |
337 | | - echo -e " Run a backup now: ${C_GREEN}sudo $PROG_NAME${C_RESET}" |
338 | | - echo -e " Verbose diff summary: ${C_GREEN}sudo $PROG_NAME --verbose --diff${C_RESET}" |
339 | | - echo -e " Fix perms (interactive): ${C_GREEN}sudo $PROG_NAME --fix-permissions --test${C_RESET}" |
340 | | - echo -e " Background restore: ${C_GREEN}sudo $PROG_NAME --background-restore latest /mnt/restore${C_RESET}" |
341 | | - echo -e " List snapshot contents: ${C_GREEN}sudo $PROG_NAME --ls latest /path/to/dir${C_RESET}" |
342 | | - echo -e " Find a file everywhere: ${C_GREEN}sudo $PROG_NAME --find \"*.log\" -l${C_RESET}" |
343 | | - echo -e " Dump one file to stdout: ${C_GREEN}sudo $PROG_NAME --dump latest /etc/hosts > hosts.txt${C_RESET}" |
344 | | - echo |
345 | | - echo -e "${C_BOLD}${C_YELLOW}DEPENDENCIES:${C_RESET}" |
346 | | - echo -e " This script requires: ${C_GREEN}restic, curl, gpg, bzip2, less, jq, flock${C_RESET}" |
347 | | - echo |
348 | | - echo -e "Config: ${C_DIM}${CONFIG_FILE}${C_RESET} Log: ${C_DIM}${LOG_FILE:-"(not set)"}${C_RESET}" |
349 | | - echo |
350 | | - echo -e "For full details, see the online documentation: \e]8;;${readme_url}\a${C_CYAN}README.md${C_RESET}\e]8;;\a" |
351 | | - echo -e "${C_YELLOW}Note:${C_RESET} For restic official documentation See: https://restic.readthedocs.io/" |
352 | | - echo |
353 | | -} |
354 | | - |
355 | 370 | detect_distro() { |
356 | 371 | if [ -f /etc/os-release ]; then |
357 | 372 | # shellcheck source=/dev/null |
@@ -808,6 +823,9 @@ cleanup() { |
808 | 823 | if [ -n "${LOCK_FD:-}" ]; then |
809 | 824 | flock -u "$LOCK_FD" |
810 | 825 | fi |
| 826 | + if [ -n "$(jobs -p)" ]; then |
| 827 | + pkill -P $$ || true |
| 828 | + fi |
811 | 829 | } |
812 | 830 |
|
813 | 831 | run_preflight_checks() { |
@@ -931,6 +949,18 @@ run_preflight_checks() { |
931 | 949 | else |
932 | 950 | if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi |
933 | 951 | fi |
| 952 | + # Disk Space Check for restic cache |
| 953 | + local check_dir="${RESTIC_CACHE_DIR:-/tmp}" |
| 954 | + mkdir -p "$check_dir" 2>/dev/null || true |
| 955 | + if command -v df >/dev/null && command -v awk >/dev/null; then |
| 956 | + if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Free space check ($check_dir)..."; fi |
| 957 | + local available_kb |
| 958 | + available_kb=$(df -k --output=avail "$check_dir" | tail -n1) |
| 959 | + if [[ "$available_kb" -lt 512000 ]]; then |
| 960 | + handle_failure "Insufficient free space in $check_dir. Need >500MB, found $((available_kb/1024))MB." "16" |
| 961 | + fi |
| 962 | + if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi |
| 963 | + fi |
934 | 964 | # Backup Sources |
935 | 965 | if [[ "$mode" == "backup" || "$mode" == "diff" ]]; then |
936 | 966 | if [[ "$verbosity" == "verbose" ]]; then echo -e "\n ${C_DIM}- Checking Backup Sources${C_RESET}"; fi |
@@ -1881,9 +1911,6 @@ case "${1:-}" in |
1881 | 1911 | run_preflight_checks "unlock" "quiet" |
1882 | 1912 | run_unlock |
1883 | 1913 | ;; |
1884 | | - --help | -h) |
1885 | | - display_help |
1886 | | - ;; |
1887 | 1914 | *) |
1888 | 1915 | if [ -n "${1:-}" ]; then |
1889 | 1916 | echo -e "${C_RED}Error: Unknown command '$1'${C_RESET}\n" >&2 |
|
0 commit comments