diff --git a/README.md b/README.md index 9b2528e..0f91528 100644 --- a/README.md +++ b/README.md @@ -9,42 +9,43 @@ This script automates encrypted, deduplicated backups of local directories to a ## Features - - **Client-Side Encryption**: All data is encrypted on your server *before* being uploaded, ensuring zero-knowledge privacy from the storage provider. - - **Deduplication & Compression**: Saves significant storage space by only storing unique data blocks and applying compression. - - **Snapshot-Based Backups**: Creates point-in-time snapshots, allowing you to easily browse and restore files from any backup date. - - **Advanced Retention Policies**: Sophisticated rules to automatically keep daily, weekly, monthly, and yearly snapshots. - - **Unified Configuration**: All settings are managed in a single, easy-to-edit `restic-backup.conf` file. - - **Notification Support**: Sends detailed success, warning, or failure notifications to ntfy and/or Discord. - - **System Friendly**: Uses `nice` and `ionice` to minimize CPU and I/O impact during backups. - - **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. +- **Client-Side Encryption**: All data is encrypted on your server *before* being uploaded, ensuring zero-knowledge privacy from the storage provider. +- **Deduplication & Compression**: Saves significant storage space by only storing unique data blocks and applying compression. +- **Snapshot-Based Backups**: Creates point-in-time snapshots, allowing you to easily browse and restore files from any backup date. +- **Advanced Retention Policies**: Sophisticated rules to automatically keep daily, weekly, monthly, and yearly snapshots. +- **Unified Configuration**: All settings are managed in a single, easy-to-edit `restic-backup.conf` file. +- **Notification Support**: Sends detailed success, warning, or failure notifications to ntfy, Discord, Slack, and Microsoft Teams. +- **Flexible File Exclusions**: Exclude files and directories using either a dedicated exclusion file or by listing patterns directly in the configuration. +- **System Friendly**: Uses `nice` and `ionice` to minimize CPU and I/O impact during backups. +- **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. ----- ## Usage -#### Run Modes: - - - `sudo ./restic-backup.sh` - Run a standard backup silently (suitable for cron). - - `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 --install-scheduler` - Run the interactive wizard to set up an automated backup schedule (systemd/cron). - - `sudo ./restic-backup.sh --uninstall-scheduler` - Remove a schedule created by the wizard. - - `sudo ./restic-backup.sh --restore` - Start the interactive restore wizard. - - `sudo ./restic-backup.sh --forget` - Manually apply the retention policy and prune old data. - - `sudo ./restic-backup.sh --diff` - Show a summary of changes between the last two snapshots. - - `sudo ./restic-backup.sh --stats` - Display repository size, file counts, and stats. - - `sudo ./restic-backup.sh --unlock` - Forcibly remove stale locks from the repository. - - `sudo ./restic-backup.sh --snapshots` - List all available snapshots in the repository. - - `sudo ./restic-backup.sh --snapshots-delete` - Permanently delete specific snapshots. - - `sudo ./restic-backup.sh --init` - (One-time setup) Initialize the remote repository. - - `sudo ./restic-backup.sh --help` - Displays help and all the flags. - +### Run Modes + +- `sudo ./restic-backup.sh` - Run a standard backup silently (suitable for cron). +- `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 --fix-permissions --test` - Run tests and interactively auto-correct insecure file permissions. +- `sudo ./restic-backup.sh --install-scheduler` - Run the interactive wizard to set up an automated backup schedule (systemd/cron). +- `sudo ./restic-backup.sh --uninstall-scheduler` - Remove a schedule created by the wizard. +- `sudo ./restic-backup.sh --restore` - Start the interactive restore wizard. +- `sudo ./restic-backup.sh --forget` - Manually apply the retention policy and prune old data. +- `sudo ./restic-backup.sh --diff` - Show a summary of changes between the last two snapshots. +- `sudo ./restic-backup.sh --stats` - Display repository size, file counts, and stats. +- `sudo ./restic-backup.sh --unlock` - Forcibly remove stale locks from the repository. +- `sudo ./restic-backup.sh --snapshots` - List all available snapshots in the repository. +- `sudo ./restic-backup.sh --snapshots-delete` - Permanently delete specific snapshots. +- `sudo ./restic-backup.sh --init` - (One-time setup) Initialize the remote repository. +- `sudo ./restic-backup.sh --help` - Displays help and all the flags. > *Default log location: `/var/log/restic-backup.log`* @@ -52,15 +53,15 @@ This script automates encrypted, deduplicated backups of local directories to a The script uses specific exit codes for different failures to help with debugging automated runs. - - **Exit Code `1`:** A fatal configuration error, such as a missing `restic-backup.conf` file or required variable. - - **Exit Code `5`:** Lock contention; another instance of the script is already running. - - **Exit Code `10`:** A required command (like `restic` or `curl`) is not installed. - - **Exit Code `11`:** The `RESTIC_PASSWORD_FILE` cannot be found. - - **Exit Code `12`:** The script cannot connect to or access the Restic repository. - - **Exit Code `13`:** A source directory in `BACKUP_SOURCES` does not exist or is not readable. - - **Exit Code `14`:** The `EXCLUDE_FILE` is not readable. - - **Exit Code `15`:** The `LOG_FILE` is not writable. - - **Exit Code `20`:** The `restic init` command failed. +- **Exit Code `1`:** A fatal configuration error, such as a missing `restic-backup.conf` file or required variable. +- **Exit Code `5`:** Lock contention; another instance of the script is already running. +- **Exit Code `10`:** A required command (like `restic` or `curl`) is not installed. +- **Exit Code `11`:** The `RESTIC_PASSWORD_FILE` cannot be found. +- **Exit Code `12`:** The script cannot connect to or access the Restic repository. +- **Exit Code `13`:** A source directory in `BACKUP_SOURCES` does not exist or is not readable. +- **Exit Code `14`:** The `EXCLUDE_FILE` is not readable. +- **Exit Code `15`:** The `LOG_FILE` is not writable. +- **Exit Code `20`:** The `restic init` command failed. ----- @@ -68,7 +69,7 @@ The script uses specific exit codes for different failures to help with debuggin All files should be placed in a single directory (e.g., `/root/scripts/backup`). -``` +```bash /root/scripts/backup/ ├── restic-backup.sh (main script) ├── restic-backup.conf (settings and credentials) @@ -103,7 +104,7 @@ sudo apt-get update && sudo apt-get install -y restic jq gnupg curl bzip2 util-l sudo dnf install -y restic jq gnupg curl bzip2 util-linux coreutils less ``` -You could also download and install the latest version of `restic`. +You could also download and install the latest version of `restic`. **Note:** While `restic` can be installed from your system's package manager, it is often an older version. It is **recommended** to install it manually or allow the script's built-in auto-updater to fetch the latest [official version](https://github.com/restic/restic/releases) for you. @@ -138,12 +139,13 @@ sudo mv restic_* /usr/local/bin/restic | **`coreutils`** | Provides essential commands used throughout the script, such as `date`, `grep`, `sed`, `chmod`, `mv`, and `mktemp`. | | **`less`** | Paging through the list of files during an interactive restore (`--restore` mode). | +----- ### 2. Configure Passwordless SSH Login (Recommended) The most reliable way for the script to connect to a remote server is via an SSH config file. -1. **Generate a root SSH key** if one doesn't already exist: +1. **Generate a root SSH key** if one doesn't already exist: ```sh sudo ssh-keygen -t ed25519 @@ -151,18 +153,18 @@ The most reliable way for the script to connect to a remote server is via an SSH (Press Enter through all prompts). -2. **Add your public key** to the remote server's authorized keys. For a Hetzner Storage Box, you can paste the contents of `sudo cat /root/.ssh/id_ed25519.pub` into the control panel. +2. **Add your public key** to the remote server's authorized keys. For a Hetzner Storage Box, you can paste the contents of `sudo cat /root/.ssh/id_ed25519.pub` into the control panel. -3. **Create an SSH config file** to define an alias for your connection: +3. **Create an SSH config file** to define an alias for your connection: ```sh # Open the file in an editor sudo nano /root/.ssh/config ``` -4. **Add the following content**, adjusting the details for your server: +4. **Add the following content**, adjusting the details for your server: - ``` + ```bash Host storagebox HostName u123456.your-storagebox.de User u123456-sub4 @@ -172,7 +174,7 @@ The most reliable way for the script to connect to a remote server is via an SSH ServerAliveCountMax 240 ``` -5. **Set secure permissions** and test the connection: +5. **Set secure permissions** and test the connection: ```sh sudo chmod 600 /root/.ssh/config @@ -183,13 +185,13 @@ The most reliable way for the script to connect to a remote server is via an SSH ### 3. Place and Configure Files -1. Create your script directory: +1. Create your script directory: ```sh mkdir -p /root/scripts/backup && cd /root/scripts/backup ``` -2. Download the script, configuration, and excludes files from the repository: +2. Download the script, configuration, and excludes files from the repository: ```sh # Download the main script @@ -202,25 +204,70 @@ The most reliable way for the script to connect to a remote server is via an SSH curl -LO https://github.com/buildplan/restic-backup-script/raw/refs/heads/main/restic-excludes.txt ``` -3. **Make the script executable**: +3. **Make the script executable**: ```sh chmod +x restic-backup.sh ``` -4. **Set secure permissions** for your configuration file: +4. **Set secure permissions** for your configuration file: ```sh chmod 600 restic-backup.conf ``` -5. **Edit `restic-backup.conf` and `restic-excludes.txt`** to specify your repository path, source directories, notification settings, and exclusion patterns. +5. **Edit `restic-backup.conf` and `restic-excludes.txt`** to specify your repository path, source directories, notification settings, and exclusion patterns. + +### Configuration (`restic-backup.conf`) + +All script behavior is controlled by the `restic-backup.conf` file. Below is an overview of the key settings available. + +#### Core Settings + +- `RESTIC_REPOSITORY`: The connection string for your remote storage. +- `RESTIC_PASSWORD_FILE`: The absolute path to the file containing your repository's encryption password. +- `BACKUP_SOURCES`: A list of local directories to back up. Use Bash array syntax `("/path/one" "/path/two")` to handle spaces correctly. + +#### Retention Policy + +You can define how many snapshots to keep for various timeframes. The script will automatically remove older snapshots that fall outside these rules. + +- `KEEP_LAST`: Number of the most recent snapshots to keep. +- `KEEP_DAILY`: Number of daily snapshots to keep. +- `KEEP_WEEKLY`: Number of weekly snapshots to keep. +- `KEEP_MONTHLY`: Number of monthly snapshots to keep. +- `KEEP_YEARLY`: Number of yearly snapshots to keep. + +#### Notifications + +The script can send detailed status notifications to multiple services. Each can be enabled or disabled individually. + +- `NTFY_ENABLED`: Set to `true` to enable ntfy notifications. +- `DISCORD_ENABLED`: Set to `true` to enable Discord notifications. +- `SLACK_ENABLED`: Set to `true` to enable Slack notifications. +- `TEAMS_ENABLED`: Set to `true` to enable Microsoft Teams notifications. +- You must also provide the corresponding `_URL` and `_TOKEN` for each service you enable. + +#### Exclusions + +You have two ways to exclude files and directories from your backups: + +1. **`EXCLUDE_FILE`**: Point this to a text file (like `restic-excludes.txt`) containing one exclusion pattern per line. +2. **`EXCLUDE_PATTERNS`**: A space-separated list of patterns to exclude directly in the configuration file (e.g., `*.tmp *.log`). + +#### Performance and Maintenance + +- `LOW_PRIORITY`: Set to `true` to run the backup with lower CPU (`nice`) and I/O (`ionice`) priority, minimizing impact on other services. +- `CHECK_AFTER_BACKUP`: Set to `true` to automatically run a repository integrity check after each successful backup. +- `PRUNE_AFTER_FORGET`: Set to `true` to automatically prune the repository after applying the retention policy, which frees up storage space. + +----- ### 4. Initial Repository Setup Before the first backup, you need to create the repository password file and initialize the remote repository. -1. **Create the password file.** This stores the encryption key for your repository. **Guard this file carefully!** +1. **Create the password file.** This stores the encryption key for your repository. **Guard this file carefully!** ```sh # Replace 'your-very-secure-password' with a strong, unique password @@ -230,7 +277,7 @@ Before the first backup, you need to create the repository password file and ini sudo chmod 400 /root/.restic-password ``` -2. **Initialize the repository.** Run the script with the `--init` flag: +2. **Initialize the repository.** Run the script with the `--init` flag: ```sh # Navigate to your script directory @@ -244,12 +291,14 @@ Before the first backup, you need to create the repository password file and ini The easiest and most reliable way to schedule your backups is to use the script's built-in interactive wizard. It will guide you through creating and enabling either a modern `systemd timer` (recommended) or a traditional `cron job`. -1. Navigate to your script directory: +1. Navigate to your script directory: + ```sh cd /root/scripts/backup ``` -2. Run the scheduler installation wizard: +2. Run the scheduler installation wizard: + ```sh sudo ./restic-backup.sh --install-scheduler ``` @@ -262,13 +311,13 @@ If you prefer to manage the schedule manually instead of using the wizard, you c To run the backup automatically, edit the root crontab. -1. Open the crontab editor: +1. Open the crontab editor: ```sh sudo crontab -e ``` -2. Add the following lines to schedule your backups and maintenance. +2. Add the following lines to schedule your backups and maintenance. ```crontab # Define a safe PATH that includes the location of restic @@ -284,6 +333,7 @@ To run the backup automatically, edit the root crontab. 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).* *Redirecting output to `/dev/null` is recommended, as the script handles its own logging and notifications.* diff --git a/restic-backup.conf b/restic-backup.conf index f220ef1..0b2574e 100644 --- a/restic-backup.conf +++ b/restic-backup.conf @@ -63,6 +63,8 @@ LOG_RETENTION_DAYS="30" # Enable notifications NTFY_ENABLED=true DISCORD_ENABLED=false +SLACK_ENABLED=false +TEAMS_ENABLED=false # ntfy settings NTFY_TOKEN="xxxxxxxxxxxxxxxxxxx" @@ -76,6 +78,12 @@ NTFY_PRIORITY_FAILURE=4 # Discord webhook (if enabled) DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/your/webhook_url_here" +# Slack webhook (if enabled) +SLACK_WEBHOOK_URL="https://hooks.slack.com/services/your/webhook_url_here" + +# Microsoft Teams webhook (if enabled) +TEAMS_WEBHOOK_URL="https://your-tenant.webhook.office.com/webhookb2/your/webhook_url_here" + # --- Healthchecks.io --- # URL for the "dead man's switch" service to ping on successful completion. # Leave blank to disable. @@ -88,6 +96,10 @@ CHECK_AFTER_BACKUP=false # Prune repository after forget (removes unreferenced data) PRUNE_AFTER_FORGET=true +# Automatically fix insecure permissions on config/password files (600/400). +# Enable only for interactive maintenance runs, not in cron/systemd: +# AUTO_FIX_PERMS=false + # --- Exclusions --- # File containing exclude patterns (one per line) EXCLUDE_FILE="/etc/restic-excludes.txt" diff --git a/restic-backup.sh b/restic-backup.sh index 828f5f1..b735acd 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash # ================================================================= -# Restic Backup Script v0.32 - 2025.09.27 +# Restic Backup Script v0.33 - 2025.09.27 # ================================================================= set -euo pipefail umask 077 # --- Script Constants --- -SCRIPT_VERSION="0.32" +SCRIPT_VERSION="0.33" SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) CONFIG_FILE="${SCRIPT_DIR}/restic-backup.conf" LOCK_FILE="/tmp/restic-backup.lock" @@ -235,31 +235,46 @@ 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 "A comprehensive script for managing encrypted, deduplicated backups with restic." + echo "Encrypted, deduplicated backups with restic." echo echo -e "${C_BOLD}${C_YELLOW}USAGE:${C_RESET}" - echo -e " sudo $0 ${C_GREEN}[COMMAND]${C_RESET}" + echo -e " sudo $prog ${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." + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--fix-permissions" "Interactive only: auto-fix 600/400 on conf/secret." + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--help, -h" "Display this help message." echo echo -e "${C_BOLD}${C_YELLOW}COMMANDS:${C_RESET}" printf " ${C_GREEN}%-20s${C_RESET} %s\n" "[no command]" "Run a standard backup and apply the retention policy." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--init" "Initialize a new restic repository (one-time setup)." printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--diff" "Show a summary of changes between the last two snapshots." 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" "--stats" "Display repository size, file counts, and stats." - 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." - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--dry-run" "Preview backup changes without creating a new snapshot." - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--test" "Validate configuration, permissions, and SSH connectivity." - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--install-scheduler" "Install an automated backup schedule (systemd/cron)." - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--uninstall-scheduler" "Remove an existing automated backup schedule." - printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--help, -h" "Display this help message." + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--snapshots-delete" "Interactively select and permanently delete snapshots." + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--stats" "Display repository size and file counts." + printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--check" "Verify repository integrity (subset)." + 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" "--restore" "Interactive restore wizard." + 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" "--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 - echo -e "Use ${C_GREEN}--verbose${C_RESET} before any command for detailed live output (e.g., 'sudo $0 --verbose --diff')." + 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 } @@ -273,6 +288,14 @@ log_message() { fi } +handle_crash() { + local exit_code=$? + local line_num=$1 + log_message "FATAL: Script terminated unexpectedly on line $line_num with exit code $exit_code." + send_notification "Backup Crashed: $HOSTNAME" "x" \ + "${NTFY_PRIORITY_FAILURE}" "failure" "Backup script terminated unexpectedly on line $line_num." +} + build_backup_command() { local cmd=(restic) [ "${LOG_LEVEL:-1}" -le 0 ] && cmd+=(--quiet) @@ -446,6 +469,105 @@ send_discord() { "$DISCORD_WEBHOOK_URL" >/dev/null 2>>"$LOG_FILE" } +send_teams() { + local title="$1" + local status="$2" + local message="$3" + if [[ "${TEAMS_ENABLED:-false}" != "true" ]] || [ -z "${TEAMS_WEBHOOK_URL:-}" ]; then + return 0 + fi + local color + case "$status" in + success) color="good" ;; + warning) color="warning" ;; + failure) color="attention" ;; + *) color="default" ;; + esac + local escaped_title + escaped_title=$(echo "$title" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') + local escaped_message + 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 '{ + "type": "message", + "attachments": [{ + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "type": "AdaptiveCard", + "version": "1.4", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": [ + { + "type": "TextBlock", + "text": "%s", + "weight": "bolder", + "size": "large", + "wrap": true, + "color": "%s" + }, + { + "type": "TextBlock", + "text": "%s", + "wrap": true, + "separator": true + } + ], + "msteams": { "width": "full", "entities": [] } + } + }] + }' "$escaped_title" "$color" "$escaped_message" + curl -s --max-time 15 \ + -H "Content-Type: application/json" \ + -d "$json_payload" \ + "$TEAMS_WEBHOOK_URL" >/dev/null 2>>"$LOG_FILE" +} + +send_slack() { + local title="$1" + local status="$2" + local message="$3" + if [[ "${SLACK_ENABLED:-false}" != "true" ]] || [ -z "${SLACK_WEBHOOK_URL:-}" ]; then + return 0 + fi + local color + case "$status" in + success) color="#36a64f" ;; + warning) color="#ffa500" ;; + 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 json_payload + printf -v json_payload '{ + "attachments": [ + { + "color": "%s", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "%s" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "%s" + } + } + ] + } + ] + }' "$color" "$escaped_title" "$escaped_message" + curl -s --max-time 15 \ + -H "Content-Type: application/json" \ + -d "$json_payload" \ + "$SLACK_WEBHOOK_URL" >/dev/null 2>>"$LOG_FILE" +} + send_notification() { local title="$1" local tags="$2" @@ -454,6 +576,8 @@ send_notification() { local message="$5" send_ntfy "$title" "$tags" "$ntfy_priority" "$message" send_discord "$title" "$discord_status" "$message" + send_slack "$title" "$discord_status" "$message" + send_teams "$title" "$discord_status" "$message" } setup_environment() { @@ -514,13 +638,46 @@ run_preflight_checks() { fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi fi - # Configuration Files + + # --- Config Files Existence & Permissions Check --- if [[ "$verbosity" == "verbose" ]]; then echo -e "\n ${C_DIM}- Checking Configuration Files${C_RESET}"; fi + if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Secure permissions on config file (600)..."; fi + local perms + perms=$(stat -c %a "$CONFIG_FILE" 2>/dev/null) + if [[ "$perms" != "600" ]]; then + echo -e "[${C_YELLOW} WARN ${C_RESET}]" + echo -e "${C_YELLOW} ⚠️ Configuration file has insecure permissions ($perms), should be 600.${C_RESET}" + if [[ "${AUTO_FIX_PERMS}" == "true" ]]; then + if chmod 600 "$CONFIG_FILE"; then + echo -e "${C_GREEN} ✅ Automatically corrected permissions to 600.${C_RESET}" + else + echo -e "${C_RED} ❌ Failed to correct permissions.${C_RESET}" + fi + fi + else + if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi + fi + + # --- Password File Existence & Permissions Check --- 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" "11" fi - if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi + perms=$(stat -c %a "$RESTIC_PASSWORD_FILE" 2>/dev/null) + if [[ "$perms" != "400" ]]; then + echo -e "[${C_YELLOW} WARN ${C_RESET}]" + echo -e "${C_YELLOW} ⚠️ Password file has insecure permissions ($perms), should be 400.${C_RESET}" + if [[ "${AUTO_FIX_PERMS}" == "true" ]]; then + if chmod 400 "$RESTIC_PASSWORD_FILE"; then + echo -e "${C_GREEN} ✅ Automatically corrected permissions to 400.${C_RESET}" + else + echo -e "${C_RED} ❌ Failed to correct permissions.${C_RESET}" + fi + fi + else + if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi + fi + # --- Exclude File Check --- if [ -n "${EXCLUDE_FILE:-}" ]; then if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Exclude file ('$EXCLUDE_FILE')..."; fi if [ ! -r "$EXCLUDE_FILE" ]; then @@ -528,11 +685,14 @@ run_preflight_checks() { fi if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi fi + + # --- Log File Check --- 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}" "15" 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 @@ -658,7 +818,6 @@ run_install_scheduler() { local hour1=${time1%%:*} min1=${time1##*:} local hour2=${time2%%:*} min2=${time2##*:} printf -v systemd_schedule "*-*-* %s:%s:00\n*-*-* %s:%s:00" "$hour1" "$min1" "$hour2" "$min2" - systemd_schedule="*-*-* ${hour1}:${min1}:00\n*-*-* ${hour2}:${min2}:00" printf -v cron_schedule "%s %s * * *\n%s %s * * *" "$min1" "$hour1" "$min2" "$hour2" ;; 3) @@ -996,7 +1155,7 @@ run_restore() { read -p "Enter snapshot ID to restore (or 'latest'): " snapshot_id if [ -z "$snapshot_id" ]; then echo "No snapshot specified, exiting" - return 1 + 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 @@ -1007,13 +1166,13 @@ run_restore() { 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 + return 0 fi if [[ "$restore_dest" == "/" || "$restore_dest" == "/etc" || "$restore_dest" == "/usr" ]]; then read -p "${C_RED}WARNING: You are restoring to a critical system directory ('$restore_dest')${C_RESET}. This is highly unusual and could damage your system. Are you absolutely sure? (y/n): " confirm_dangerous_restore if [[ "${confirm_dangerous_restore,,}" != "y" ]]; then echo "Restore cancelled." - return 1 + return 0 fi fi local include_paths=() @@ -1101,7 +1260,7 @@ run_snapshots_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 + return 0 fi 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 @@ -1145,25 +1304,63 @@ run_snapshots_delete() { # MAIN SCRIPT EXECUTION # ================================================================= -check_for_script_update -check_and_install_restic -trap cleanup EXIT -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 +# 1. Parse flags. VERBOSE_MODE=false -if [[ "${1:-}" == "--verbose" ]]; then - VERBOSE_MODE=true - shift -fi +AUTO_FIX_PERMS=${AUTO_FIX_PERMS:-false} +while [[ $# -gt 0 ]]; do + case "$1" in + --verbose) + VERBOSE_MODE=true + shift + ;; + --fix-permissions) + if ! [ -t 1 ]; then + echo -e "${C_RED}ERROR: The --fix-permissions flag can only be used in an interactive session.${C_RESET}" >&2 + exit 1 + fi + AUTO_FIX_PERMS=true + shift + ;; + --) + shift + break + ;; + *) + break + ;; + esac +done + +# 2. Set traps. +trap 'handle_crash $LINENO' ERR +trap cleanup EXIT + +# 3. Acquire the 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 + +# 4. After lock, it's safe to run updates. +check_for_script_update +check_and_install_restic + +# 5. Prepare the environment and run final pre-flight checks. setup_environment rotate_log -# Handle different modes +# Handle the --fix-permissions and AUTO_FIX_PERMS config for non-interactive mode +if [[ "${AUTO_FIX_PERMS}" == "true" ]]; then + if ! [ -t 1 ]; then + log_message "AUTO_FIX_PERMS=true ignored in non-interactive mode for safety." + echo -e "${C_YELLOW}WARNING: AUTO_FIX_PERMS is enabled but ignored in non-interactive mode for safety.${C_RESET}" + AUTO_FIX_PERMS=false + fi +fi + +# 6. Execute the requested command. case "${1:-}" in --install-scheduler) run_install_scheduler diff --git a/restic-backup.sh.sha256 b/restic-backup.sh.sha256 index 900c231..7437989 100644 --- a/restic-backup.sh.sha256 +++ b/restic-backup.sh.sha256 @@ -1 +1 @@ -0d5c7c7bbd9a39cb27588b6b7f6bf33de8a6167f2c7482d3dff20d3710b5b2b1 restic-backup.sh +3834308a4d720a8a18e68f85f759817e2748919fe8bbd7eddbab35c52c850c8a restic-backup.sh \ No newline at end of file