From 35503d4a94776e8a11eae076f44f5adea716b8e0 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 11:09:24 +0100 Subject: [PATCH 01/14] add scheduler option to the script --- restic-backup.sh | 255 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 249 insertions(+), 6 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 0ad85bb..ccb2d33 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash # ================================================================= -# Restic Backup Script v0.30 - 2025.09.26 +# Restic Backup Script v0.31 - 2025.09.27 # ================================================================= set -euo pipefail umask 077 # --- Script Constants --- -SCRIPT_VERSION="0.30" +SCRIPT_VERSION="0.31" SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) CONFIG_FILE="${SCRIPT_DIR}/restic-backup.conf" LOCK_FILE="/tmp/restic-backup.lock" @@ -255,6 +255,8 @@ display_help() { 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." echo echo -e "Use ${C_GREEN}--verbose${C_RESET} before any command for detailed live output (e.g., 'sudo $0 --verbose --diff')." @@ -580,7 +582,7 @@ rotate_log() { 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) + log_size=$(stat -f%z "$LOG_FILE" 2>/dev/null || stat -c%s "$LOG_FILE" 2>/dev/null || wc -c < "$LOG_FILE" 2>/dev/null || echo 0) else log_size=0 fi @@ -606,6 +608,238 @@ run_with_priority() { fi } +run_install_scheduler() { + echo -e "${C_BOLD}--- Backup Schedule Installation Wizard ---${C_RESET}" + if [[ $EUID -ne 0 ]]; then + echo -e "${C_RED}ERROR: This operation requires root privileges.${C_RESET}" >&2 + exit 1 + fi + local script_path + script_path=$(realpath "$0") + echo -e "\n${C_YELLOW}Which scheduling system would you like to use?${C_RESET}" + echo " 1) ${C_GREEN}systemd timer${C_RESET} (Modern, recommended, more flexible logging)" + echo " 2) ${C_CYAN}crontab${C_RESET} (Classic, simple, universally available)" + local scheduler_choice + read -p "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 " 1) ${C_GREEN}Once daily${C_RESET}" + echo " 2) ${C_GREEN}Twice daily${C_RESET} (e.g., every 12 hours)" + echo " 3) ${C_CYAN}Custom schedule${C_RESET} (provide your own expression)" + local schedule_choice + read -p "Enter your choice [1]: " schedule_choice + schedule_choice=${schedule_choice:-1} + + local systemd_schedule cron_schedule + case "$schedule_choice" in + 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 + 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 + local hour=${daily_time%%:*} minute=${daily_time##*:} + systemd_schedule="*-*-* ${hour}:${minute}:00" + cron_schedule="${minute} ${hour} * * *" + ;; + 2) + local time1 time2 + while true; do + read -p "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 + 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 + local hour1=${time1%%:*} min1=${time1##*:} + local hour2=${time2%%:*} min2=${time2##*:} + systemd_schedule="*-*-* ${hour1}:${min1}:00,${hour2}:${min2}:00" + cron_schedule="${min1} ${hour1} * * *\n${min2} ${hour2} * * *" + ;; + 3) + if [[ "$scheduler_choice" == "1" ]]; then + read -p "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 + if echo "$cron_schedule" | grep -qE '^([0-9*,/-]+\s){4}[0-9*,/-]+$'; then + break + else + echo -e "${C_RED}Invalid format. A cron expression must have 5 fields separated by spaces, using only valid characters (0-9,*,/,-).${C_RESET}" + fi + done + fi + ;; + *) + echo -e "${C_RED}Invalid choice. Aborting.${C_RESET}" >&2; return 1 ;; + esac + echo -e "\n${C_BOLD}--- Summary ---${C_RESET}" + echo -e " ${C_DIM}Script Path:${C_RESET} $script_path" + echo -e " ${C_DIM}Config File:${C_RESET} $CONFIG_FILE" + if [[ "$scheduler_choice" == "1" ]]; then + echo -e " ${C_DIM}Scheduler:${C_RESET} systemd timer" + echo -e " ${C_DIM}Schedule:${C_RESET} $systemd_schedule" + echo + read -p "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 + if [[ "${confirm,,}" != "y" ]]; then echo "Aborted."; return 1; fi + install_crontab "$script_path" "$cron_schedule" "$LOG_FILE" + fi +} + +install_systemd_timer() { + local script_path="$1" + local schedule="$2" + local config_file="$3" + local service_file="/etc/systemd/system/restic-backup.service" + 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 + if [[ "${confirm,,}" != "y" ]]; then echo "Aborted."; return 1; fi + fi + echo "Creating systemd service file: $service_file" + cat > "$service_file" << EOF +[Unit] +Description=Restic Backup Service +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +EnvironmentFile=$config_file +ExecStart=$script_path +User=root +Group=root +EOF + + echo "Creating systemd timer file: $timer_file" + cat > "$timer_file" << EOF +[Unit] +Description=Run Restic Backup on a schedule + +[Timer] +OnCalendar=$schedule +Persistent=true + +[Install] +WantedBy=timers.target +EOF + + echo "Reloading systemd daemon, enabling and starting timer..." + if systemctl daemon-reload && systemctl enable --now restic-backup.timer; then + echo -e "${C_GREEN}✅ systemd timer installed and activated successfully.${C_RESET}" + echo -e "\n${C_BOLD}--- Verifying Status ---${C_RESET}" + systemctl status restic-backup.timer --no-pager + else + echo -e "${C_RED}❌ Failed to install or start systemd timer.${C_RESET}" >&2 + return 1 + fi +} + +install_crontab() { + local script_path="$1" + local schedule="$2" + local log_file="$3" + local cron_file="/etc/cron.d/restic-backup" + + # --- Path 1: File exists (Append mode) --- + if [ -f "$cron_file" ]; then + 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 + if [[ "${confirm,,}" != "y" ]]; then + echo "Aborted." + return 1 + fi + + echo "Appending new schedule(s)..." + local new_jobs_added=0 + # Loop to append ONLY the new job lines, checking for duplicates + while IFS= read -r schedule_line; do + if [ -n "$schedule_line" ]; then + local full_command_line="$schedule_line root $script_path" + if grep -qF "$full_command_line" "$cron_file"; then + echo -e "${C_DIM}Skipping duplicate schedule: $schedule_line${C_RESET}" + else + echo "$full_command_line >> \"$log_file\" 2>&1" >> "$cron_file" + ((new_jobs_added++)) + fi + fi + done <<< "$schedule" + + if [ "$new_jobs_added" -eq 0 ]; then + echo -e "${C_YELLOW}No new unique schedules were added.${C_RESET}" + fi + else + echo "Creating new cron job file: $cron_file" + cat > "$cron_file" << EOF +# Restic Backup Job installed by restic-backup.sh +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin + +EOF + + while IFS= read -r schedule_line; do + if [ -n "$schedule_line" ]; then + echo "$schedule_line root $script_path >> \"$log_file\" 2>&1" >> "$cron_file" + fi + done <<< "$schedule" + fi + + chmod 644 "$cron_file" + echo -e "${C_GREEN}✅ Cron job file updated successfully.${C_RESET}" + echo -e "\n${C_BOLD}--- Current Cron File Content ---${C_RESET}" + cat "$cron_file" +} + +run_uninstall_scheduler() { + echo -e "${C_BOLD}--- Backup Schedule Uninstallation ---${C_RESET}" + if [[ $EUID -ne 0 ]]; then + echo -e "${C_RED}ERROR: This operation requires root privileges.${C_RESET}" >&2 + exit 1 + fi + local service_file="/etc/systemd/system/restic-backup.service" + local timer_file="/etc/systemd/system/restic-backup.timer" + local cron_file="/etc/cron.d/restic-backup" + local found=false + + if [ -f "$timer_file" ]; then + found=true + echo "Found systemd timer. Stopping and disabling..." + systemctl stop restic-backup.timer + systemctl disable restic-backup.timer + rm -f "$timer_file" "$service_file" + systemctl daemon-reload + echo -e "${C_GREEN}✅ systemd timer and service files removed.${C_RESET}" + fi + + if [ -f "$cron_file" ]; then + found=true + echo "Found cron file. Removing..." + rm -f "$cron_file" + echo -e "${C_GREEN}✅ Cron file removed.${C_RESET}" + fi + + if [ "$found" = false ]; then + echo -e "${C_YELLOW}No scheduled backup tasks found to uninstall.${C_RESET}" + fi +} + # ================================================================= # MAIN OPERATIONS # ================================================================= @@ -826,7 +1060,7 @@ run_restore() { # Set file ownership logic if [[ "$restore_dest" == /home/* ]]; then local dest_user - dest_user=$(echo "$restore_dest" | cut -d/ -f3) + dest_user=$(stat -c %U "$(dirname "$restore_dest")") if [[ -n "$dest_user" ]] && id -u "$dest_user" &>/dev/null; then echo -e "${C_CYAN}ℹ️ Home directory detected. Setting ownership of restored files to '$dest_user'...${C_RESET}" if chown -R "${dest_user}:${dest_user}" "$restore_dest"; then @@ -922,6 +1156,12 @@ rotate_log # Handle different modes case "${1:-}" in + --install-scheduler) + run_install_scheduler + ;; + --uninstall-scheduler) + run_uninstall_scheduler + ;; --init) run_preflight_checks "init" "quiet" init_repository @@ -997,9 +1237,12 @@ case "${1:-}" in # --- 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" + if ! curl -fsS -m 15 --retry 3 "${HEALTHCHECKS_URL}" >/dev/null 2>>"$LOG_FILE"; then + log_message "WARNING: Healthchecks.io ping failed." + send_notification "Healthchecks Ping Failed: $HOSTNAME" "warning" \ + "${NTFY_PRIORITY_WARNING}" "warning" "Failed to ping Healthchecks.io after successful backup." + fi fi ;; esac -echo -e "${C_BOLD}--- Backup Script Completed ---${C_RESET}" From 2efdb3d3dae4572cf556446298e1ff009b2cc644 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 11:13:12 +0100 Subject: [PATCH 02/14] message to show script completion --- restic-backup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/restic-backup.sh b/restic-backup.sh index ccb2d33..19cdf75 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -1246,3 +1246,4 @@ case "${1:-}" in ;; esac +echo -e "${C_BOLD}--- Backup Script Completed ---${C_RESET}" \ No newline at end of file From fa876d166295a9953677b44ebbf6d7f704c24c3a Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 11:26:00 +0100 Subject: [PATCH 03/14] improved run_restore function --- restic-backup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restic-backup.sh b/restic-backup.sh index 19cdf75..d833763 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -1060,7 +1060,7 @@ run_restore() { # Set file ownership logic if [[ "$restore_dest" == /home/* ]]; then local dest_user - dest_user=$(stat -c %U "$(dirname "$restore_dest")") + dest_user=$(stat -c %U "$(dirname "$restore_dest")" 2>/dev/null || echo "${restore_dest#/home/}" | cut -d/ -f1) if [[ -n "$dest_user" ]] && id -u "$dest_user" &>/dev/null; then echo -e "${C_CYAN}ℹ️ Home directory detected. Setting ownership of restored files to '$dest_user'...${C_RESET}" if chown -R "${dest_user}:${dest_user}" "$restore_dest"; then From 33bfc1d367dca351ab7a1c1f7254e4787dab1fea Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 12:16:43 +0100 Subject: [PATCH 04/14] update new feature --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ece3261..9b2528e 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ This script automates encrypted, deduplicated backups of local directories to a - `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. @@ -238,7 +240,25 @@ Before the first backup, you need to create the repository password file and ini sudo ./restic-backup.sh --init ``` -### 5. Set up a Cron Job +### 5. Set up an Automated Schedule (Recommended) + +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: + ```sh + cd /root/scripts/backup + ``` + +2. Run the scheduler installation wizard: + ```sh + sudo ./restic-backup.sh --install-scheduler + ``` + +Follow the on-screen prompts to choose your preferred scheduling system and frequency. The script will handle creating all the necessary files and enabling the service for you. + +#### Manual Cron Job Setup + +If you prefer to manage the schedule manually instead of using the wizard, you can edit the root crontab directly. To run the backup automatically, edit the root crontab. From 0e2af57b2efdf6776a486959558096a40f8a458c Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 12:58:20 +0100 Subject: [PATCH 05/14] sha256 for v0.31 --- 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 f73db47..f203db9 100644 --- a/restic-backup.sh.sha256 +++ b/restic-backup.sh.sha256 @@ -1 +1 @@ -f46058f5e1dfb2cc158e30183d01d62a225b71459b4b702a9ec57113cb76f743 restic-backup.sh +a9a15d5722f0ceceffcceff2fb53d472948d24c729be908aa81e06bd09cf37f8 restic-backup.sh From 92596ead38fc9033da49ced27775813d07a8eeba Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 13:08:46 +0100 Subject: [PATCH 06/14] add about.md --- .github/about.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/about.md diff --git a/.github/about.md b/.github/about.md new file mode 100644 index 0000000..f9900e9 --- /dev/null +++ b/.github/about.md @@ -0,0 +1,45 @@ +# About restic-backup-script + +## Project Overview +This repository provides a Bash script (`restic-backup.sh`) and configuration (`restic-backup.conf`) for automated, encrypted backups using [restic](https://restic.net/). It targets VPS environments, backing up local directories to remote SFTP storage (e.g., Hetzner Storage Box) with client-side encryption, deduplication, and snapshot management. + +## Key Files & Structure +- `restic-backup.sh`: Main script. Handles backup, restore, integrity checks, scheduling, notifications, and self-updating. +- `restic-backup.conf`: Central config. All operational parameters (sources, retention, logging, notifications, etc.) are set here. +- `restic-excludes.txt`: Patterns for files/directories to exclude from backups. +- `how-to/`: Step-by-step guides for restoring, migrating, troubleshooting, and best practices. + +## Essential Workflows +- **Run Modes**: Script supports multiple flags (`--restore`, `--check`, `--install-scheduler`, etc.). See `README.md` for full list and usage examples. +- **Configuration**: All settings (sources, repository, retention, notifications) are managed in `restic-backup.conf`. Use Bash array syntax for `BACKUP_SOURCES`. +- **Exclusions**: Update `restic-excludes.txt` or `EXCLUDE_PATTERNS` in config for custom exclusions. +- **Scheduling**: Use `--install-scheduler` to set up systemd/cron jobs interactively. +- **Self-Update**: Script can auto-update itself and restic binary if run interactively. +- **Notifications**: Integrates with ntfy and Discord for backup status alerts. +- **Healthchecks**: Optional integration for cron job monitoring via Healthchecks.io. + +## Patterns & Conventions +- **Root Privileges**: Script enforces root execution (auto re-invokes with sudo). +- **Locking**: Prevents concurrent runs via `/tmp/restic-backup.lock`. +- **Logging**: Rotates logs at `/var/log/restic-backup.log` based on config. +- **Error Handling**: Uses `set -euo pipefail` for strict error management. +- **Color Output**: Uses ANSI colors for interactive output, disables for non-TTY. +- **Repository URL**: Uses SSH config alias for SFTP (e.g., `sftp:storagebox:/home/vps`). +- **Restore Safety**: Guides recommend restoring to temp directories before overwriting live data. + +## Integration Points +- **restic**: Main dependency. Script checks, installs, and verifies restic binary. +- **SFTP/SSH**: Repository access via SSH config alias. Ensure `/root/.ssh/config` is set up. +- **ntfy/Discord**: Notification endpoints configured in `restic-backup.conf`. +- **Healthchecks.io**: Optional dead man's switch for scheduled jobs. + +## Examples +- To run a backup: `sudo ./restic-backup.sh` +- To restore: `sudo ./restic-backup.sh --restore` (interactive wizard) +- To check integrity: `sudo ./restic-backup.sh --check` +- To install scheduler: `sudo ./restic-backup.sh --install-scheduler` + +## References +- See `README.md` for feature overview and usage. +- See `how-to/` for guides on restore, migration, troubleshooting, and best practices. +- See `restic-backup.conf` for all config options and conventions. From 352704692f43b9a7f8b6abc6b294d5cc0c1374f8 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 13:34:24 +0100 Subject: [PATCH 07/14] fix ANSI escape --- restic-backup.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index d833763..0cd0f99 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -617,15 +617,15 @@ run_install_scheduler() { local script_path script_path=$(realpath "$0") echo -e "\n${C_YELLOW}Which scheduling system would you like to use?${C_RESET}" - echo " 1) ${C_GREEN}systemd timer${C_RESET} (Modern, recommended, more flexible logging)" - echo " 2) ${C_CYAN}crontab${C_RESET} (Classic, simple, universally available)" + 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 scheduler_choice=${scheduler_choice:-1} echo -e "\n${C_YELLOW}How often would you like the backup to run?${C_RESET}" - echo " 1) ${C_GREEN}Once daily${C_RESET}" - echo " 2) ${C_GREEN}Twice daily${C_RESET} (e.g., every 12 hours)" - echo " 3) ${C_CYAN}Custom schedule${C_RESET} (provide your own expression)" + 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 schedule_choice=${schedule_choice:-1} From 5f0c86ef4bbe65f530fcda8eec8c470bbc749fac Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 13:45:08 +0100 Subject: [PATCH 08/14] fix systemd and cron schedule generation to handle multiple times correctly --- restic-backup.sh | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 0cd0f99..1cd35d6 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -657,7 +657,7 @@ run_install_scheduler() { done local hour1=${time1%%:*} min1=${time1##*:} local hour2=${time2%%:*} min2=${time2##*:} - systemd_schedule="*-*-* ${hour1}:${min1}:00,${hour2}:${min2}:00" + systemd_schedule="*-*-* ${hour1}:${min1}:00\n*-*-* ${hour2}:${min2}:00" cron_schedule="${min1} ${hour1} * * *\n${min2} ${hour2} * * *" ;; 3) @@ -685,7 +685,7 @@ run_install_scheduler() { echo -e " ${C_DIM}Config File:${C_RESET} $CONFIG_FILE" if [[ "$scheduler_choice" == "1" ]]; then echo -e " ${C_DIM}Scheduler:${C_RESET} systemd timer" - echo -e " ${C_DIM}Schedule:${C_RESET} $systemd_schedule" + printf " ${C_DIM}Schedule:%b\n%s${C_RESET}\n" "${C_RESET}" "$systemd_schedule" echo read -p "Proceed with installation? (y/n): " confirm if [[ "${confirm,,}" != "y" ]]; then echo "Aborted."; return 1; fi @@ -732,8 +732,14 @@ EOF Description=Run Restic Backup on a schedule [Timer] -OnCalendar=$schedule Persistent=true +EOF + while IFS= read -r schedule_line; do + if [ -n "$schedule_line" ]; then + echo "OnCalendar=$schedule_line" >> "$timer_file" + fi + done <<< "$schedule" + cat >> "$timer_file" << EOF [Install] WantedBy=timers.target @@ -743,7 +749,7 @@ EOF if systemctl daemon-reload && systemctl enable --now restic-backup.timer; then echo -e "${C_GREEN}✅ systemd timer installed and activated successfully.${C_RESET}" echo -e "\n${C_BOLD}--- Verifying Status ---${C_RESET}" - systemctl status restic-backup.timer --no-pager + systemctl list-timers restic-backup.timer else echo -e "${C_RED}❌ Failed to install or start systemd timer.${C_RESET}" >&2 return 1 @@ -755,8 +761,6 @@ install_crontab() { local schedule="$2" local log_file="$3" local cron_file="/etc/cron.d/restic-backup" - - # --- Path 1: File exists (Append mode) --- if [ -f "$cron_file" ]; then echo -e "${C_YELLOW}Existing cron file found at $cron_file:${C_RESET}" cat "$cron_file" @@ -766,10 +770,8 @@ install_crontab() { echo "Aborted." return 1 fi - echo "Appending new schedule(s)..." local new_jobs_added=0 - # Loop to append ONLY the new job lines, checking for duplicates while IFS= read -r schedule_line; do if [ -n "$schedule_line" ]; then local full_command_line="$schedule_line root $script_path" @@ -781,7 +783,6 @@ install_crontab() { fi fi done <<< "$schedule" - if [ "$new_jobs_added" -eq 0 ]; then echo -e "${C_YELLOW}No new unique schedules were added.${C_RESET}" fi From d6e2bb444cc7793632df6ad9dd680515da3f1825 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 13:47:18 +0100 Subject: [PATCH 09/14] sha256 for v0.31 --- 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 f203db9..e2921b9 100644 --- a/restic-backup.sh.sha256 +++ b/restic-backup.sh.sha256 @@ -1 +1 @@ -a9a15d5722f0ceceffcceff2fb53d472948d24c729be908aa81e06bd09cf37f8 restic-backup.sh +35e9ecfe5b9bdd6b373332ba12c5fc7ee0baeb4d1549d046c96d57919fbbe46e restic-backup.sh From 0b22ce99868968d2ce01ff05cb9f9338ccaaf830 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 13:57:33 +0100 Subject: [PATCH 10/14] improved healthcheck.io integration --- restic-backup.sh | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/restic-backup.sh b/restic-backup.sh index 1cd35d6..2ed750f 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -1227,22 +1227,41 @@ case "${1:-}" in fi run_preflight_checks "backup" "quiet" log_message "=== Starting backup run ===" - if run_backup; then + + local backup_exit_code=0 + if ! run_backup; then + backup_exit_code=1 + fi + + if [ "$backup_exit_code" -eq 0 ]; then run_forget if [ "${CHECK_AFTER_BACKUP:-false}" = "true" ]; then run_check fi fi + log_message "=== Backup run completed ===" - # --- Ping Healthchecks.io on success --- - if [[ -n "${HEALTHCHECKS_URL:-}" ]]; then + # --- Ping Healthchecks.io (Success or Failure) --- + if [ "$backup_exit_code" -eq 0 ] && [[ -n "${HEALTHCHECKS_URL:-}" ]]; then log_message "Pinging Healthchecks.io to signal successful run." if ! curl -fsS -m 15 --retry 3 "${HEALTHCHECKS_URL}" >/dev/null 2>>"$LOG_FILE"; then - log_message "WARNING: Healthchecks.io ping failed." + log_message "WARNING: Healthchecks.io success ping failed." send_notification "Healthchecks Ping Failed: $HOSTNAME" "warning" \ "${NTFY_PRIORITY_WARNING}" "warning" "Failed to ping Healthchecks.io after successful backup." fi + elif [ "$backup_exit_code" -ne 0 ] && [[ -n "${HEALTHCHECKS_URL:-}" ]]; then + log_message "Pinging Healthchecks.io with failure signal." + if ! curl -fsS -m 15 --retry 3 "${HEALTHCHECKS_URL}/fail" >/dev/null 2>>"$LOG_FILE"; then + log_message "WARNING: Healthchecks.io failure ping failed." + send_notification "Healthchecks Ping Failed: $HOSTNAME" "warning" \ + "${NTFY_PRIORITY_WARNING}" "warning" "Failed to ping Healthchecks.io /fail endpoint after backup failure." + fi + fi + + # Exit with the correct code to signal success or failure to the scheduler + if [ "$backup_exit_code" -ne 0 ]; then + exit "$backup_exit_code" fi ;; esac From 42f3b07d0c7c1db84c078110d76c676feeed47fc Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 14:02:38 +0100 Subject: [PATCH 11/14] shellcheck fixed --- restic-backup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restic-backup.sh b/restic-backup.sh index 2ed750f..cd5e8cb 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -1228,7 +1228,7 @@ case "${1:-}" in run_preflight_checks "backup" "quiet" log_message "=== Starting backup run ===" - local backup_exit_code=0 + backup_exit_code=0 if ! run_backup; then backup_exit_code=1 fi From 2d5d951b49b964b59853eb39b86b51310ea2109e Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 14:15:47 +0100 Subject: [PATCH 12/14] fix systemd timer format --- restic-backup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/restic-backup.sh b/restic-backup.sh index cd5e8cb..85a8ff8 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -657,6 +657,7 @@ run_install_scheduler() { done 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" cron_schedule="${min1} ${hour1} * * *\n${min2} ${hour2} * * *" ;; From d36b51fd3d73ba25ab9754ef9459926fcd2a6fe4 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 14:29:19 +0100 Subject: [PATCH 13/14] fix cron schedule formatting --- restic-backup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restic-backup.sh b/restic-backup.sh index 85a8ff8..94131e7 100644 --- a/restic-backup.sh +++ b/restic-backup.sh @@ -659,7 +659,7 @@ run_install_scheduler() { 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" - cron_schedule="${min1} ${hour1} * * *\n${min2} ${hour2} * * *" + printf -v cron_schedule "%s %s * * *\n%s %s * * *" "$min1" "$hour1" "$min2" "$hour2" ;; 3) if [[ "$scheduler_choice" == "1" ]]; then From 5683344dfccac4f29bb00e0dbc67cf050ac05df7 Mon Sep 17 00:00:00 2001 From: buildplan Date: Sat, 27 Sep 2025 14:40:27 +0100 Subject: [PATCH 14/14] sha256 v0.31 --- 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 e2921b9..1a68428 100644 --- a/restic-backup.sh.sha256 +++ b/restic-backup.sh.sha256 @@ -1 +1 @@ -35e9ecfe5b9bdd6b373332ba12c5fc7ee0baeb4d1549d046c96d57919fbbe46e restic-backup.sh +9a870262d8c680ea1afaf2bef175088f75c599de7fa06f39974f22d6e89e4c8d restic-backup.sh