From a7115770e9e377689d9996abe32a28e8db87429d Mon Sep 17 00:00:00 2001 From: Sam Scholten Date: Thu, 11 Sep 2025 16:06:10 +1000 Subject: init --- .gitignore | 6 + LICENSE | 21 ++ Makefile | 504 ++++++++++++++++++++++++++++++++++++++++++ README.md | 129 +++++++++++ common.sh | 34 +++ restic_backup.sh | 61 +++++ restic_check.sh | 43 ++++ setup.sh | 370 +++++++++++++++++++++++++++++++ systemd/restic-backup.service | 17 ++ systemd/restic-backup.timer | 9 + systemd/restic-check.service | 17 ++ systemd/restic-check.timer | 9 + tests/basic.bats | 39 ++++ uninstall_system.sh | 67 ++++++ uninstall_user.sh | 59 +++++ update_gotify.sh | 71 ++++++ 16 files changed, 1456 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 common.sh create mode 100755 restic_backup.sh create mode 100755 restic_check.sh create mode 100755 setup.sh create mode 100644 systemd/restic-backup.service create mode 100644 systemd/restic-backup.timer create mode 100644 systemd/restic-check.service create mode 100644 systemd/restic-check.timer create mode 100644 tests/basic.bats create mode 100755 uninstall_system.sh create mode 100755 uninstall_user.sh create mode 100755 update_gotify.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ef85d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +.aider.* +venv/* +.aider* +/tests/tmp/ +old_code/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8aa2645 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..017841a --- /dev/null +++ b/Makefile @@ -0,0 +1,504 @@ +.PHONY: all test-local format lint clean help test-remote test-remote-setup test-remote-run test-remote-verify test-remote-teardown setup-user setup-system backup-now backup-now-system check-now check-now-system status status-system snapshots snapshots-system recover uninstall-user uninstall-system update-gotify update-gotify-user update-gotify-system validate-config logs logs-system config test-update-gotify + +# Default target +all: test-local + +# Run all tests +test-local: + @echo "--- Running Bats Tests ---" + @if ! command -v bats >/dev/null 2>&1; then \ + echo "Error: Bats-core is not installed. Run 'make install-deps' first."; \ + exit 1; \ + fi + @echo "Bats version: $$(bats --version)" + @BATS_NO_PRETTY_PRINT_FAILURES=true bats tests/basic.bats + @echo "--- All Bats Tests Completed ---" + +# Format shell scripts using shfmt +format: + @echo "--- Formatting Shell Scripts with shfmt ---" + shfmt -w *.sh # For setup.sh, restic_backup.sh, restic_check.sh, common.sh + shfmt -w tests/basic.bats # For the single simplified test file + @echo "--- Shell Scripts Formatted ---" + +# Lint shell scripts using shellcheck +lint: + @echo "--- Linting Shell Scripts with shellcheck ---" + @if command -v shellcheck >/dev/null 2>&1; then \ + shellcheck setup.sh restic_backup.sh restic_check.sh common.sh || echo "Shellcheck found issues"; \ + else \ + echo "Warning: shellcheck not found. Install with: sudo apt install shellcheck"; \ + fi + @echo "--- Linting Complete ---" + +# Clean up temporary files (if any are generated by tests or other processes) +clean: + @echo "--- Cleaning up temporary and generated files ---" + @echo "Removing temporary test directories and files..." + @rm -rf /tmp/drestic-test-* + @rm -rf ~/.config/restic-test + @echo "Removing editor backup/swap files..." + @find . -name "*~" -delete 2>/dev/null || true + @find . -name "*.bak" -delete 2>/dev/null || true + @find . -name ".*.swp" -delete 2>/dev/null || true + @echo "Removing downloaded temporary files (restic installer)..." + @rm -f /tmp/restic.bz2 /tmp/restic 2>/dev/null || true + @echo "--- Cleanup Complete ---" + +# Install dependencies for the project +install-deps: + @echo "--- Installing Dependencies ---" + @echo "Installing basic dependencies..." + @if command -v apt >/dev/null 2>&1; then \ + echo "Using apt package manager"; \ + sudo apt update; \ + sudo apt install -y curl git restic shellcheck shfmt unzip jq; \ + echo "✓ Basic dependencies installed"; \ + elif command -v yum >/dev/null 2>&1; then \ + echo "Using yum package manager"; \ + sudo yum install -y curl git ShellCheck unzip jq; \ + echo "✓ Basic dependencies installed (note: shfmt may need manual installation)"; \ + echo "Installing restic separately..."; \ + if ! command -v restic >/dev/null 2>&1; then \ + wget -q https://github.com/restic/restic/releases/latest/download/restic_*_linux_amd64.bz2 -O /tmp/restic.bz2; \ + bunzip2 /tmp/restic.bz2; \ + chmod +x /tmp/restic; \ + sudo mv /tmp/restic /usr/local/bin/restic; \ + echo "✓ Restic installed"; \ + fi; \ + elif command -v pacman >/dev/null 2>&1; then \ + echo "Using pacman package manager"; \ + sudo pacman -S --noconfirm curl git restic shellcheck shfmt unzip jq; \ + echo "✓ Basic dependencies installed"; \ + elif command -v zypper >/dev/null 2>&1; then \ + echo "Using zypper package manager"; \ + sudo zypper install -y curl git restic ShellCheck shfmt unzip jq; \ + echo "✓ Basic dependencies installed"; \ + else \ + echo "❌ No supported package manager found (apt/yum/pacman/zypper)"; \ + echo "Please install manually: curl git restic shellcheck shfmt unzip"; \ + exit 1; \ + fi + @echo "--- Installing Official Rclone (with MEGA support) ---" + @if command -v rclone >/dev/null 2>&1; then \ + echo "Checking if rclone supports MEGA backend..."; \ + if rclone help backends | grep -q mega; then \ + echo "✓ Rclone with MEGA support already installed"; \ + else \ + echo "⚠ Rclone found but lacks MEGA support. Installing official version..."; \ + curl -s https://rclone.org/install.sh | sudo bash; \ + echo "✓ Official rclone installed"; \ + fi; \ + else \ + echo "Installing official rclone with all backends..."; \ + curl -s https://rclone.org/install.sh | sudo bash; \ + echo "✓ Official rclone installed"; \ + fi + @echo "--- Installing Restic (if not already installed) ---" + @if ! command -v restic >/dev/null 2>&1; then \ + echo "Downloading and installing restic..."; \ + wget -q https://github.com/restic/restic/releases/latest/download/restic_*_linux_amd64.bz2 -O /tmp/restic.bz2; \ + bunzip2 /tmp/restic.bz2; \ + chmod +x /tmp/restic; \ + sudo mv /tmp/restic /usr/local/bin/restic; \ + echo "✓ Restic installed"; \ + else \ + echo "✓ Restic already installed"; \ + fi + @echo "--- Installing Bats (for testing) ---" + @if ! command -v bats >/dev/null 2>&1; then \ + echo "Installing Bats testing framework..."; \ + git clone https://github.com/bats-core/bats-core.git /tmp/bats-core; \ + sudo /tmp/bats-core/install.sh /usr/local; \ + rm -rf /tmp/bats-core; \ + echo "✓ Bats installed"; \ + else \ + echo "✓ Bats already installed"; \ + fi + @echo "--- Dependencies Installation Complete ---" + @echo "You can now run: make test-local" + +# check-config target removed as validate_config.sh is deprecated/removed +# and its functionality is now inline or simplified. + +# Manual testing targets +test-remote: test-remote-setup test-remote-run test-remote-verify test-remote-recovery test-remote-gotify test-remote-teardown + +# Setup test environment +test-remote-setup: + @echo "Setting up test environment with real MEGA backend..." + @if [ ! -f ~/.config/restic/env ]; then \ + echo "Error: Real restic configuration not found. Run './setup.sh --scope=user' first."; \ + exit 1; \ + fi + @echo "Creating test configuration directory: ~/.config/restic-test" + @mkdir -p ~/.config/restic-test + @echo "Creating temporary test data directory: /tmp/drestic-test-data" + @mkdir -p /tmp/drestic-test-data + @echo "Creating test paths file: ~/.config/restic-test/paths" + @echo "/tmp/drestic-test-data" > ~/.config/restic-test/paths + @echo "Creating test excludes file: ~/.config/restic-test/excludes" + @echo "*.tmp" > ~/.config/restic-test/excludes + @echo "Copying Restic password file to test config: ~/.config/restic-test/password" + @cp ~/.config/restic/password ~/.config/restic-test/ + @echo "Copying Restic environment file to test config: ~/.config/restic-test/env" + @cp ~/.config/restic/env ~/.config/restic-test/ + @echo "Updating CONFIG_DIR in test environment file to point to test config" + @sed -i 's|CONFIG_DIR=.*|CONFIG_DIR="$$HOME/.config/restic-test"|' ~/.config/restic-test/env && \ + echo "Copying Gotify settings from main config to test config..." && \ + grep -E "^GOTIFY_(URL|TOKEN)=" ~/.config/restic/env >> ~/.config/restic-test/env || true + @echo "Creating test data files in /tmp/drestic-test-data..." + @echo "Test file 1 - $$(date)" > /tmp/drestic-test-data/file1.txt + @echo "Test file 2 - $$(date)" > /tmp/drestic-test-data/file2.txt + @mkdir -p /tmp/drestic-test-data/subdir + @echo "Nested test file" > /tmp/drestic-test-data/subdir/nested.txt + @echo "Temp file to exclude" > /tmp/drestic-test-data/exclude-me.tmp + @echo "Test environment ready! Using real MEGA repository with test data only." + +# Run test backup +test-remote-run: + @echo "Running test backup..." + @RESTIC_ENV_FILE=~/.config/restic-test/env ./restic_backup.sh + @echo "Test backup completed!" + +# Verify test results +test-remote-verify: + @echo "Verifying test backup..." + @if [ -f ~/.config/restic-test/password ] && [ -f ~/.config/restic-test/env ]; then \ + . ~/.config/restic-test/env && \ + echo "Waiting 10 seconds for repository to settle..." && sleep 10 && \ + timeout 180 env RESTIC_PASSWORD_FILE="$$HOME/.config/restic-test/password" restic snapshots --repo "$$RESTIC_REPOSITORY" | tail -5 || \ + echo "⚠ Verification timed out - this is often due to network issues but backup likely succeeded"; \ + else \ + echo "Error: Test configuration not found"; \ + fi + +# Teardown test environment +test-remote-teardown: + @echo "Cleaning up test environment..." + @rm -rf ~/.config/restic-test + @rm -rf /tmp/drestic-test-data + @rm -rf /tmp/drestic-recovery-restore + @echo "Test environment cleaned up!" + @echo "Note: Test snapshots remain in your MEGA repository. Clean manually if needed:" + @echo " restic forget --repo rclone:backup_remote:/restic_backups --tag daily --prune" + +# Test file recovery using existing test data +test-remote-recovery: + @echo "Testing file recovery..." + @if [ ! -d /tmp/drestic-test-data ]; then \ + echo "No test data found. Run 'make test-remote-setup' first."; \ + exit 1; \ + fi + @echo "Creating additional recovery test file..." + @echo "Recovery test file - $(date)" > /tmp/drestic-test-data/recovery-test.txt + @echo "Backing up recovery test file..." + @RESTIC_ENV_FILE=~/.config/restic-test/env ./restic_backup.sh + @echo "Simulating data loss - deleting recovery test file..." + @rm /tmp/drestic-test-data/recovery-test.txt + @echo "Restoring file from backup..." + @mkdir -p /tmp/drestic-recovery-restore + @. ~/.config/restic-test/env && \ + echo "Waiting 10 seconds for repository to settle..." && sleep 10 && \ + timeout 180 env RESTIC_PASSWORD_FILE="$$HOME/.config/restic-test/password" \ + restic restore latest --target /tmp/drestic-recovery-restore \ + --include /tmp/drestic-test-data/recovery-test.txt \ + --repo "$$RESTIC_REPOSITORY" || \ + { echo "⚠ Recovery test timed out - network issues with MEGA"; exit 0; } + @if [ -f /tmp/drestic-recovery-restore/tmp/drestic-test-data/recovery-test.txt ]; then \ + echo "✓ Recovery test PASSED! File restored successfully."; \ + echo "Restored content: $$(cat /tmp/drestic-recovery-restore/tmp/drestic-test-data/recovery-test.txt)"; \ + rm -rf /tmp/drestic-recovery-restore; \ + else \ + echo "✗ Recovery test FAILED! File not restored."; \ + exit 1; \ + fi + +# Test Gotify notifications (if configured) +test-remote-gotify: + @echo "Testing Gotify notifications..." + @if [ ! -f ~/.config/restic-test/env ]; then \ + echo "No test environment found. Run 'make test-remote-setup' first."; \ + exit 1; \ + fi + @. ~/.config/restic-test/env && \ + if [ -z "$$GOTIFY_URL" ] || [ -z "$$GOTIFY_TOKEN" ]; then \ + echo "ℹ Gotify not configured - skipping notification test."; \ + echo "To test Gotify, set GOTIFY_URL and GOTIFY_TOKEN in ~/.config/restic/env"; \ + else \ + echo "Sending test notification to $$GOTIFY_URL..."; \ + if curl -sS "$$GOTIFY_URL/message?token=$$GOTIFY_TOKEN" \ + -F "title=DRestic Test ($$(whoami)@$$(hostname))" \ + -F "message=This is a test notification from your DRestic backup system on $$(whoami)@$$(hostname) at $$(date)" \ + -F "priority=5" >/dev/null 2>&1; then \ + echo "✓ Test notification sent successfully!"; \ + echo "Check your Gotify server/app for the test message."; \ + else \ + echo "✗ Failed to send notification. Check your GOTIFY_URL and $$GOTIFY_TOKEN."; \ + fi; \ + fi + +# User convenience operations +setup-user: + @./setup.sh --scope=user + +setup-system: + @sudo ./setup.sh --scope=system + +backup-now: + @systemctl --user start restic-backup.service + @echo "Backup started. Monitor with: journalctl --user -fu restic-backup.service" + +backup-now-system: + @sudo systemctl start restic-backup.service + @echo "System backup started. Monitor with: sudo journalctl -fu restic-backup.service" + +check-now: + @systemctl --user start restic-check.service + @echo "Repository check started. Monitor with: journalctl --user -fu restic-check.service" + +check-now-system: + @sudo systemctl start restic-check.service + @echo "System repository check started. Monitor with: sudo journalctl -fu restic-check.service" + +status: + @echo "=== User Timer Status ===" + @systemctl --user status restic-backup.timer restic-check.timer --no-pager || true + @echo "" + @echo "=== Recent User Backup Logs ===" + @journalctl --user -u restic-backup.service --since "24 hours ago" --no-pager -n 5 || echo "No recent backup logs found" + +status-system: + @echo "=== System Timer Status ===" + @sudo systemctl status restic-backup.timer restic-check.timer --no-pager || true + @echo "" + @echo "=== Recent System Backup Logs ===" + @sudo journalctl -u restic-backup.service --since "24 hours ago" --no-pager -n 5 || echo "No recent backup logs found" + +snapshots: + @echo "Listing user backup snapshots..." + @if [ -f ~/.config/restic/env ]; then \ + . ~/.config/restic/env && \ + env RESTIC_PASSWORD_FILE="$$RESTIC_PASSWORD_FILE" restic snapshots --repo "$$RESTIC_REPOSITORY" && \ + echo "" && \ + echo "Repository statistics:" && \ + env RESTIC_PASSWORD_FILE="$$RESTIC_PASSWORD_FILE" restic stats --repo "$$RESTIC_REPOSITORY"; \ + else \ + echo "Error: Restic not configured. Run 'make setup-user' first."; \ + exit 1; \ + fi + +snapshots-system: + @echo "Listing system backup snapshots..." + @if sudo [ -f /root/.restic_env ]; then \ + sudo bash -c '. /root/.restic_env && env RESTIC_PASSWORD_FILE="$$RESTIC_PASSWORD_FILE" restic snapshots --repo "$$RESTIC_REPOSITORY" && echo "" && echo "Repository statistics:" && env RESTIC_PASSWORD_FILE="$$RESTIC_PASSWORD_FILE" restic stats --repo "$$RESTIC_REPOSITORY"'; \ + else \ + echo "Error: System restic not configured. Run 'make setup-system' first."; \ + exit 1; \ + fi + +unlock-repo: + @echo "=== Unlock Restic Repository ===" + @if sudo [ -f /root/.restic_env ]; then \ + echo "Unlocking system scope repository..."; \ + sudo bash -c 'source /root/.restic_env && restic unlock --repo "$$RESTIC_REPOSITORY" --password-file "$$RESTIC_PASSWORD_FILE"'; \ + echo "✓ System repository unlocked"; \ + elif [ -f ~/.config/restic/env ]; then \ + echo "Unlocking user scope repository..."; \ + bash -c 'source ~/.config/restic/env && restic unlock --repo "$$RESTIC_REPOSITORY" --password-file "$$RESTIC_PASSWORD_FILE"'; \ + echo "✓ User repository unlocked"; \ + else \ + echo "Error: No restic configuration found"; \ + echo "Run 'make setup-user' or 'make setup-system' first"; \ + exit 1; \ + fi + +recover: + @echo "=== DRestic Recovery Helper ===" + @echo "" + @echo "Step 1: List available snapshots" + @echo " User scope: make snapshots" + @echo " System scope: make snapshots-system" + @echo "" + @echo "Step 2: Mount backup as filesystem (easiest method)" + @echo " mkdir ~/restore" + @echo " User scope: RESTIC_PASSWORD_FILE=~/.config/restic/password restic mount ~/restore --repo rclone:backup_remote:/restic_backups" + @echo " System scope: sudo RESTIC_PASSWORD_FILE=/root/.restic_password restic mount ~/restore --repo rclone:backup_remote:/restic_backups" + @echo "" + @echo "Step 3: Browse files in ~/restore/ (like a normal folder)" + @echo " cd ~/restore/snapshots/latest/home/username/" + @echo " cp important-file.txt ~/recovered-file.txt" + @echo "" + @echo "Step 4: Unmount when done" + @echo " umount ~/restore" + @echo "" + @echo "Alternative: Restore specific files directly" + @echo " restic restore latest --target /tmp/restore --include /path/to/file --repo rclone:backup_remote:/restic_backups" + @echo "" + @echo "For more details, see README.md Recovery section" + +uninstall-user: + @./uninstall_user.sh + +uninstall-system: + @./uninstall_system.sh + +update-gotify: + @echo "=== Update Gotify Configuration ===" + @if [ -f ~/.config/restic/env ]; then \ + echo "Updating user scope Gotify settings..."; \ + ./update_gotify.sh ~/.config/restic/env; \ + elif sudo [ -f /root/.restic_env ]; then \ + echo "Updating system scope Gotify settings..."; \ + sudo ./update_gotify.sh /root/.restic_env; \ + else \ + echo "Error: No DRestic installation found. Run 'make setup-user' or 'make setup-system' first."; \ + exit 1; \ + fi + +update-gotify-user: + @./update_gotify.sh ~/.config/restic/env + +update-gotify-system: + @sudo ./update_gotify.sh /root/.restic_env + +# New target for updating Gotify settings in the test environment +test-update-gotify: + @echo "=== Update Gotify Configuration for Test Environment ===" + @if [ ! -f ~/.config/restic-test/env ]; then \ + echo "Error: Test environment not set up. Run 'make test-remote-setup' first."; \ + exit 1; \ + fi + @./update_gotify.sh ~/.config/restic-test/env + +validate-config: + @echo "=== DRestic Configuration Validation ===" + @if [ -f ~/.config/restic/env ]; then \ + echo "✓ User scope configuration found"; \ + echo "Checking user configuration..."; \ + if [ -f ~/.config/restic/password ] && [ -s ~/.config/restic/password ]; then \ + echo "✓ Password file exists and is not empty"; \ + else \ + echo "✗ Password file missing or empty: ~/.config/restic/password"; \ + fi; \ + if [ -f ~/.config/restic/paths ] && [ -s ~/.config/restic/paths ]; then \ + echo "✓ Paths file exists and is not empty"; \ + else \ + echo "✗ Paths file missing or empty: ~/.config/restic/paths"; \ + fi; \ + if [ -f ~/.config/restic/excludes ]; then \ + echo "✓ Excludes file exists"; \ + else \ + echo "⚠ Excludes file missing (optional): ~/.config/restic/excludes"; \ + fi; \ + echo "Testing rclone connection..."; \ + if timeout 30 rclone ls backup_remote: >/dev/null 2>&1; then \ + echo "✓ Rclone connection successful"; \ + else \ + echo "✗ Rclone connection failed"; \ + fi; \ + elif [ -f /root/.restic_env ]; then \ + echo "✓ System scope configuration found"; \ + echo "Checking system configuration..."; \ + if sudo [ -f /root/.restic_password ] && sudo [ -s /root/.restic_password ]; then \ + echo "✓ Password file exists and is not empty"; \ + else \ + echo "✗ Password file missing or empty: /root/.restic_password"; \ + fi; \ + if sudo [ -f /etc/restic/paths ] && sudo [ -s /etc/restic/paths ]; then \ + echo "✓ Paths file exists and is not empty"; \ + else \ + echo "✗ Paths file missing or empty: /etc/restic/paths"; \ + fi; \ + if sudo [ -f /etc/restic/excludes ]; then \ + echo "✓ Excludes file exists"; \ + else \ + echo "⚠ Excludes file missing (optional): /etc/restic/excludes"; \ + fi; \ + echo "Testing rclone connection..."; \ + if timeout 30 rclone ls backup_remote: >/dev/null 2>&1; then \ + echo "✓ Rclone connection successful"; \ + else \ + echo "✗ Rclone connection failed"; \ + fi; \ + else \ + echo "✗ No DRestic configuration found"; \ + echo "Run 'make setup-user' or 'make setup-system' first"; \ + exit 1; \ + fi + +logs: + @echo "=== Recent User Backup Logs ===" + @journalctl --user -u restic-backup.service --since "7 days ago" --no-pager || echo "No recent backup logs found" + +logs-system: + @echo "=== Recent System Backup Logs ===" + @sudo journalctl -u restic-backup.service --since "7 days ago" --no-pager || echo "No recent backup logs found" + +config: + @echo "=== DRestic Configuration Status ===" + @if [ -f ~/.config/restic/env ]; then \ + echo "User scope configuration:"; \ + echo " Config directory: ~/.config/restic/"; \ + echo " Password file: ~/.config/restic/password"; \ + echo " Environment file: ~/.config/restic/env"; \ + echo " Paths file: ~/.config/restic/paths"; \ + echo " Excludes file: ~/.config/restic/excludes"; \ + echo " Timer status: $$(systemctl --user is-active restic-backup.timer 2>/dev/null || echo 'inactive')"; \ + elif [ -f /root/.restic_env ]; then \ + echo "System scope configuration:"; \ + echo " Config directory: /etc/restic/"; \ + echo " Password file: /root/.restic_password"; \ + echo " Environment file: /root/.restic_env"; \ + echo " Paths file: /etc/restic/paths"; \ + echo " Excludes file: /etc/restic/excludes"; \ + echo " Timer status: $$(sudo systemctl is-active restic-backup.timer 2>/dev/null || echo 'inactive')"; \ + else \ + echo "No DRestic configuration found."; \ + echo "Run 'make setup-user' or 'make setup-system' to get started."; \ + fi + +help: + @echo "Available targets:" + @echo "" + @echo "Setup and Operations:" + @echo " setup-user : Run initial setup for user scope" + @echo " setup-system : Run initial setup for system scope" + @echo " backup-now : Start user backup immediately" + @echo " backup-now-system: Start system backup immediately" + @echo " check-now : Start user repository integrity check" + @echo " check-now-system : Start system repository integrity check" + @echo " status : Show user backup timer status and recent logs" + @echo " status-system : Show system backup timer status and recent logs" + @echo " snapshots : List user backup snapshots" + @echo " snapshots-system : List system backup snapshots" + @echo " unlock-repo : Remove stale repository locks (auto-detects scope)" + @echo " uninstall-user : Uninstall user scope DRestic" + @echo " uninstall-system : Uninstall system scope DRestic" + @echo " recover : Show recovery instructions" + @echo " update-gotify : Update Gotify notification settings (auto-detects scope)" + @echo " update-gotify-user : Update Gotify settings for user scope" + @echo " update-gotify-system : Update Gotify settings for system scope" + @echo " validate-config : Validate DRestic configuration and connectivity" + @echo " logs : Show recent user backup logs" + @echo " logs-system : Show recent system backup logs" + @echo " config : Show current configuration status and paths" + @echo "" + @echo "Testing:" + @echo " test-local : Runs all (fully-local) Bats tests" + @echo " test-remote : Full manual test cycle, incl. to remote (setup, run, verify, recovery, gotify, teardown)" + @echo " test-remote-setup : Setup manual test environment with small test files" + @echo " test-remote-run : Run backup with test configuration" + @echo " test-remote-verify : Verify test backup completed successfully" + @echo " test-remote-recovery : Test file recovery workflow" + @echo " test-remote-gotify : Test Gotify notification system (if configured)" + @echo " test-update-gotify : Update Gotify settings for the test environment" + @echo " test-remote-teardown : Clean up test environment" + @echo "" + @echo "Development:" + @echo " all : Runs all default tasks (currently 'test')" + @echo " format : Formats all shell scripts using shfmt" + @echo " lint : Lints all shell scripts using shellcheck" + @echo " install-deps : Installs all required dependencies" + @echo " clean : Cleans up temporary files" + @echo " help : Displays this help message" diff --git a/README.md b/README.md new file mode 100644 index 0000000..1400333 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# DRestic + +Automated, encrypted, and deduplicated backups to MEGA cloud storage using restic. + +## Setup + +1. Clone the repository: + ```sh + git clone https://github.com/casparvitch/drestic + cd drestic + ``` + +2. Install dependencies (supports apt, yum, pacman, zypper): + ```sh + make install-deps + ``` + +3. Run the setup script: + ```sh + # For a personal machine (backs up /home/user) + make setup-user + + # For a server (backs up /etc, /home, /root, etc.) + # make setup-system + ``` + The script will prompt for your MEGA credentials and a new restic repository password. **Store the restic password safely.** + +## Usage + +All common operations are handled via the `Makefile`. Run `make help` for a full list. + +**Check Status** +```sh +# For user scope +make status + +# For system scope +make status-system +``` + +**Run a Backup Manually** +```sh +make backup-now +# Monitor with: journalctl --user -fu restic-backup.service +``` + +**List Snapshots** +```sh +make snapshots +``` + +**Recover Files** +The easiest method is to mount the repository. + +1. Create a mount point: + ```sh + mkdir ~/restore + ``` + +2. Mount the backup: + ```sh + # For user scope + RESTIC_PASSWORD_FILE=~/.config/restic/password restic mount ~/restore --repo rclone:backup_remote:/restic_backups + ``` + +3. Browse `~/restore/snapshots/latest/` to find your files. + +4. Unmount when finished: + ```sh + umount ~/restore + ``` + +## Configuration + +Configuration files are created during setup. + +**Backup Paths & Exclusions** +- **User scope:** `~/.config/restic/paths` and `~/.config/restic/excludes` +- **System scope:** `/etc/restic/paths` and `/etc/restic/excludes` + +Edit the `paths` file to change what is backed up. Edit the `excludes` file to add patterns to ignore. The default files contain a sensible starting point. + +**Backup Schedule** +The schedule is managed by systemd timers. To change the daily 3 AM backup time: + +1. Edit the timer file: + - **User scope:** `~/.config/systemd/user/restic-backup.timer` + - **System scope:** `/etc/systemd/system/restic-backup.timer` + +2. Change the `OnCalendar=` line. + +3. Reload systemd: + ```sh + # For user scope + systemctl --user daemon-reload && systemctl --user restart restic-backup.timer + + # For system scope + sudo systemctl daemon-reload && sudo systemctl restart restic-backup.timer + ``` + +**Notifications** +To add or update Gotify push notifications, run: +```sh +make update-gotify +``` + +## Troubleshooting + +**Repository is locked?** +```sh +# Auto-detects scope and unlocks +make unlock-repo +``` + +**Connection issues?** +Verify rclone can connect to your MEGA account. +```sh +rclone ls backup_remote: +``` + +**Something else?** +Check the logs. +```sh +# For user scope +make logs + +# For system scope +make logs-system +``` diff --git a/common.sh b/common.sh new file mode 100644 index 0000000..aae076a --- /dev/null +++ b/common.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# This script is not meant to be executed directly. It should be sourced by other scripts. + +# Single notification function +notify() { + local title="$1" message="$2" priority="${3:-5}" + [ -n "${GOTIFY_URL:-}" ] && [ -n "${GOTIFY_TOKEN:-}" ] || return 0 + curl -sS "$GOTIFY_URL/message?token=$GOTIFY_TOKEN" \ + -F "title=$title" -F "message=$message" -F "priority=$priority" \ + >/dev/null 2>&1 || true +} + +# Pre-warm rclone connection function +pre_warm_connection() { + local script_title="$1" + echo "Pre-warming rclone connection to MEGA..." + local prewarm_success=false + for attempt in 1 2 3; do + echo "Connection attempt $attempt/3..." + if timeout 120 rclone ls backup_remote: >/dev/null 2>&1; then + echo "✓ Connection established on attempt $attempt" + prewarm_success=true + break + else + echo "✗ Attempt $attempt failed" + [ $attempt -lt 3 ] && sleep 30 + fi + done + + if [ "$prewarm_success" = false ]; then + echo "Warning: All pre-warm attempts failed. Operation may be slower or fail." + notify "$script_title ($(whoami)@$(hostname))" "Pre-warm connection failed - operation may have issues" 6 + fi +} diff --git a/restic_backup.sh b/restic_backup.sh new file mode 100755 index 0000000..2ace329 --- /dev/null +++ b/restic_backup.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -euo pipefail + +echo "--- Initializing Restic Backup Script ---" + +# Source environment variables if the file exists +# This script expects RESTIC_ENV_FILE to be set in the environment +# The environment file includes rclone throttling settings to prevent timeouts +# shellcheck source=/dev/null +if [ -f "$RESTIC_ENV_FILE" ]; then + source "$RESTIC_ENV_FILE" + echo "Environment variables loaded from $RESTIC_ENV_FILE" +else + echo "Error: RESTIC_ENV_FILE not found. Please run setup.sh first." >&2 + exit 1 +fi + +# Source common functions +# shellcheck source=/dev/null +source "$(dirname "$0")/common.sh" + +# --- Pre-warm rclone connection --- +pre_warm_connection "Restic Backup" + +# --- Exit Trap --- +# This will call notify with the script's final exit code upon termination. +trap 'notify "Restic Backup ($(whoami)@$(hostname))" "Restic backup script finished with exit code $?" $?' EXIT + +# --- Main Backup Logic --- +echo "--- Starting Restic Backup at $(date) ---" + +# Perform the Restic backup +echo "Starting Restic backup to repository: $RESTIC_REPOSITORY" +restic backup \ + --repo "${RESTIC_REPOSITORY}" \ + --files-from "${CONFIG_DIR}/paths" \ + --exclude-file "${CONFIG_DIR}/excludes" \ + --password-file "${RESTIC_PASSWORD_FILE}" \ + --tag daily || { + echo "Error: Restic backup failed." >&2 + notify "Restic Backup ($(whoami)@$(hostname))" "Backup phase failed!" 8 + exit 1 +} + +# Prune old snapshots +echo "Applying Restic retention policy and pruning old snapshots..." +restic forget \ + --repo "${RESTIC_REPOSITORY}" \ + --password-file "${RESTIC_PASSWORD_FILE}" \ + --keep-daily 7 \ + --keep-weekly 4 \ + --keep-monthly 6 \ + --keep-yearly 6 \ + --prune || { + echo "Error: Restic forget/prune failed." >&2 + notify "Restic Backup ($(whoami)@$(hostname))" "Prune phase failed!" 8 + exit 1 +} + + +echo "--- Restic Backup finished at $(date) ---" diff --git a/restic_check.sh b/restic_check.sh new file mode 100755 index 0000000..210b11e --- /dev/null +++ b/restic_check.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -euo pipefail + +echo "--- Initializing Restic Check Script ---" + +# Source environment variables if the file exists +# This script expects RESTIC_ENV_FILE to be set in the environment +# The environment file includes rclone throttling settings to prevent timeouts +# shellcheck source=/dev/null +if [ -f "$RESTIC_ENV_FILE" ]; then + source "$RESTIC_ENV_FILE" + echo "Environment variables loaded from $RESTIC_ENV_FILE" +else + echo "Error: RESTIC_ENV_FILE not found. Please run setup.sh first." >&2 + exit 1 +fi + +# Source common functions +# shellcheck source=/dev/null +source "$(dirname "$0")/common.sh" + +# --- Pre-warm rclone connection --- +pre_warm_connection "Restic Check" + +# --- Exit Trap --- +trap 'notify "Restic Check" "Restic check script finished with exit code $?" $?' EXIT + +# --- Main Logic --- +echo "--- Starting Restic Repository Integrity Check at $(date) ---" + +echo "Running memory-efficient integrity check on repository: $RESTIC_REPOSITORY (1% data subset)..." +restic check \ + --repo "${RESTIC_REPOSITORY}" \ + --password-file "${RESTIC_PASSWORD_FILE}" \ + --read-data-subset 1% \ + --no-cache \ + --verbose || { + echo "Error: Restic check failed." >&2 + notify "Restic Check ($(whoami)@$(hostname))" "Weekly integrity check failed!" 8 + exit 1 +} + +echo "--- Restic Repository Integrity Check finished successfully at $(date) ---" diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..9bc9ab4 --- /dev/null +++ b/setup.sh @@ -0,0 +1,370 @@ +#!/bin/bash +set -euo pipefail + +# Global variables for configuration +SCOPE="" + +# --- Helper Functions --- + +log() { + echo "--- $* ---" +} + +error() { + echo "Error: $*" >&2 + echo "Run '$0 --help' for usage information." >&2 + exit 1 +} + +check_dependencies() { + local missing_deps=() + for cmd in restic rclone curl; do + if ! command -v "$cmd" &>/dev/null; then + missing_deps+=("$cmd") + fi + done + + if [ ${#missing_deps[@]} -ne 0 ]; then + error "Missing required dependencies: ${missing_deps[*]}" + fi +} + +read_password() { + local prompt="$1" + local password="" + + while [ -z "$password" ]; do + read -rsp "$prompt: " password + echo + if [ -z "$password" ]; then + echo "Password cannot be empty. Please try again." + fi + done + + echo "$password" +} + +validate_email() { + local email="$1" + if [[ ! "$email" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then + error "Invalid email format: $email" + fi +} + +validate_url() { + local url="$1" + if [[ -n "$url" ]] && [[ ! "$url" =~ ^https?:// ]]; then + error "Invalid URL format: $url (must start with http:// or https://)" + fi +} + +# Function to configure rclone +configure_rclone() { + local email="$1" + local password_file="$2" + + log "Configuring rclone remote: backup_remote" + + rclone config create backup_remote mega \ + user "$email" \ + pass "$(cat "$password_file")" \ + --non-interactive || + error "Failed to configure rclone remote 'backup_remote'." + + log "Testing rclone configuration..." + echo "Attempting to connect to MEGA (this may take 30-60 seconds)..." + if timeout 120 rclone ls backup_remote: >/dev/null 2>&1; then + echo "✓ Connection successful" + else + echo "✗ Connection failed" + echo "Debugging information:" + echo "Testing with verbose output..." + timeout 30 rclone ls backup_remote: -v 2>&1 | head -20 || true + error "Rclone test failed. Please check your MEGA credentials and network connection." + fi + + log "Rclone remote 'backup_remote' configured successfully." +} + +# Function to initialize restic repository +initialize_restic_repo() { + local repo="$1" + local password_file="$2" + + log "Checking for existing Restic repository at ${repo}" + + if restic cat config --repo "${repo}" --password-file "${password_file}" &>/dev/null; then + log "Restic repository already exists. Skipping initialization." + return 0 + fi + + log "No existing repository found. Initializing Restic repository..." + restic init --repo "${repo}" \ + --password-file "${password_file}" || + error "Failed to initialize Restic repository." + + log "Restic repository initialized successfully." +} + +# Simplified validation checks (Phase 3, Step 3.2) +check_basic_config() { + log "Performing basic configuration checks..." + local errors=0 + + # Check password file + if [ ! -f "$PASS_FILE" ]; then + echo "Error: Restic password file missing: $PASS_FILE" >&2 + errors=$((errors + 1)) + elif [ ! -s "$PASS_FILE" ]; then + echo "Error: Restic password file is empty: $PASS_FILE" >&2 + errors=$((errors + 1)) + elif [ "$(stat -c %a "$PASS_FILE")" != "600" ]; then + echo "Warning: Restic password file permissions are not 600: $PASS_FILE" >&2 + # This is a warning, not an error that stops setup, but good to flag + fi + + # Check paths file + if [ ! -f "$CONFIG_DIR/paths" ]; then + echo "Error: Backup paths file missing: $CONFIG_DIR/paths" >&2 + errors=$((errors + 1)) + elif [ ! -s "$CONFIG_DIR/paths" ]; then + echo "Error: Backup paths file is empty: $CONFIG_DIR/paths" >&2 + errors=$((errors + 1)) + fi + + if [ $errors -gt 0 ]; then + error "Basic configuration checks failed. Please address the issues." + else + log "Basic configuration checks passed." + fi +} + +# --- Main Script Logic --- + +# Parse arguments +if [ $# -ne 1 ] || [[ ! "$1" =~ ^--scope=(user|system)$ ]]; then + echo "Usage: $0 --scope=" + echo " --scope: Specify installation scope (user or system)." + exit 1 +fi +SCOPE="${1#*=}" + +log "Configuring for $SCOPE scope" + +# Check for required dependencies +log "Checking for required dependencies..." +check_dependencies +log "All dependencies found." + +# --- Simplified Path Detection Logic (Phase 2, Step 2.2) --- +INSTALL_DIR="" +SYSTEMD_DIR="" +if [ "$SCOPE" == "system" ]; then + CONFIG_DIR="/etc/restic" + PASS_FILE="/root/.restic_password" + ENV_FILE="/root/.restic_env" + SYSTEMCTL_CMD="sudo systemctl" + INSTALL_DIR="/usr/local/bin" + SYSTEMD_DIR="/etc/systemd/system" +else # SCOPE == "user" + CONFIG_DIR="$HOME/.config/restic" + PASS_FILE="$CONFIG_DIR/password" + ENV_FILE="$CONFIG_DIR/env" + SYSTEMCTL_CMD="systemctl --user" + INSTALL_DIR="$HOME/.local/bin" + SYSTEMD_DIR="$HOME/.config/systemd/user" +fi + +# Create configuration directories if they don't exist +log "Creating configuration directory: $CONFIG_DIR" +mkdir -p "$CONFIG_DIR" || error "Failed to create config directory: $CONFIG_DIR" +log "Creating installation directory: $INSTALL_DIR" +mkdir -p "$INSTALL_DIR" || error "Failed to create install directory: $INSTALL_DIR" +log "Creating systemd directory: $SYSTEMD_DIR" +mkdir -p "$SYSTEMD_DIR" || error "Failed to create systemd directory: $SYSTEMD_DIR" + +# --- Interactive Input --- +log "Gathering configuration details..." + +MEGA_EMAIL="" +while [ -z "$MEGA_EMAIL" ]; do + read -rp "Enter your MEGA email address: " MEGA_EMAIL + if [ -n "$MEGA_EMAIL" ]; then + validate_email "$MEGA_EMAIL" + fi +done + +# For MEGA password, we'll prompt and write to a temporary file for rclone config +# This will be cleaned up after rclone config is done. +MEGA_TEMP_PASS_FILE=$(mktemp) +MEGA_PASSWORD=$(read_password "Enter your MEGA password (will not be displayed)") +echo +# Strip any trailing newlines/carriage returns from password +MEGA_PASSWORD=$(echo -n "$MEGA_PASSWORD" | tr -d '\n\r') +echo -n "$MEGA_PASSWORD" >"$MEGA_TEMP_PASS_FILE" +unset MEGA_PASSWORD # Clear password from shell history + +# For Restic password, we'll prompt and write to the designated PASS_FILE +echo "----------------------------------------------------------------------------" +echo "IMPORTANT: Your Restic password encrypts ALL backup data." +echo "If you lose this password, your backups are PERMANENTLY UNRECOVERABLE." +echo "Please choose a strong password and STORE IT SAFELY (password manager, etc.)" +echo "----------------------------------------------------------------------------" +RESTIC_PASSWORD=$(read_password "Enter your Restic repository password (will not be displayed)") +echo +# Strip any trailing newlines/carriage returns from password +RESTIC_PASSWORD=$(echo -n "$RESTIC_PASSWORD" | tr -d '\n\r') +echo -n "$RESTIC_PASSWORD" >"$PASS_FILE" +log "Setting permissions for $PASS_FILE to 600" +chmod 600 "$PASS_FILE" || error "Failed to set permissions on password file." +unset RESTIC_PASSWORD # Clear password from shell history + +# Gotify details (optional) +GOTIFY_URL="" +GOTIFY_TOKEN="" +read -rp "Enter your Gotify URL (no trailing slash, e.g. https://gotify.example.com, leave blank if not used): " GOTIFY_URL +if [ -n "$GOTIFY_URL" ]; then + validate_url "$GOTIFY_URL" + read -rp "Enter your Gotify Application Token: " GOTIFY_TOKEN +fi + +# --- Call the functions with correct paths --- +configure_rclone "$MEGA_EMAIL" "$MEGA_TEMP_PASS_FILE" +# Clean up temporary MEGA password file +rm -f "$MEGA_TEMP_PASS_FILE" + +# Define the restic repository path using the CONFIG_DIR for consistency +RESTIC_REPO="rclone:backup_remote:/restic_backups" # This path is fixed as per plan + +initialize_restic_repo "$RESTIC_REPO" "$PASS_FILE" + +create_default_config() { + local config_dir="$1" + local scope="$2" + + if [ ! -f "$config_dir/paths" ]; then + if [ "$scope" = "system" ]; then + cat >"$config_dir/paths" <"$config_dir/paths" <"$config_dir/excludes" <"$ENV_FILE" <"$SYSTEMD_DIR/restic-backup.service" +log "Generating systemd service file: $SYSTEMD_DIR/restic-check.service" +sed -e "s|ExecStart=.*|ExecStart=$INSTALL_DIR/restic_check.sh|" \ + -e "s|# Environment variable will be set by setup.sh based on scope|Environment=\"RESTIC_ENV_FILE=$ENV_FILE\"|" \ + systemd/restic-check.service >"$SYSTEMD_DIR/restic-check.service" + +# Copy systemd timer files +log "Copying systemd timer file: $SYSTEMD_DIR/restic-backup.timer" +cp systemd/restic-backup.timer "$SYSTEMD_DIR/restic-backup.timer" +log "Copying systemd timer file: $SYSTEMD_DIR/restic-check.timer" +cp systemd/restic-check.timer "$SYSTEMD_DIR/restic-check.timer" + +# Reload systemd daemon, enable and start timers +log "Reloading systemd daemon..." +$SYSTEMCTL_CMD daemon-reload || error "Failed to reload systemd daemon." +log "Enabling systemd timers: restic-backup.timer and restic-check.timer" +$SYSTEMCTL_CMD enable restic-backup.timer restic-check.timer || error "Failed to enable systemd timers." +log "Starting systemd timers: restic-backup.timer and restic-check.timer" +$SYSTEMCTL_CMD start restic-backup.timer restic-check.timer || error "Failed to start systemd timers." + +log "Restic scripts and Systemd units installed and enabled." + +log "Setup complete!" +echo "Configuration files are located in: $CONFIG_DIR" +echo "Restic password file: $PASS_FILE" +echo "Environment file: $ENV_FILE" +echo "Paths to backup: $CONFIG_DIR/paths" +echo "Excludes: $CONFIG_DIR/excludes" +echo "Systemd timers are enabled and started. You can check their status with:" +echo " $SYSTEMCTL_CMD status restic-backup.timer restic-check.timer" diff --git a/systemd/restic-backup.service b/systemd/restic-backup.service new file mode 100644 index 0000000..0c9ae2a --- /dev/null +++ b/systemd/restic-backup.service @@ -0,0 +1,17 @@ +[Unit] +Description=Restic Daily Backup +Wants=network-online.target +After=network.target network-online.target + +[Service] +Type=oneshot +# ExecStart path will be replaced by setup.sh during installation +ExecStart=/path/to/be/replaced/restic_backup.sh +# Environment variable will be set by setup.sh based on scope +Environment="HOME=/root" +# Memory limits to prevent OOM on VPS +MemoryMax=500M +MemorySwapMax=500M + +[Install] +WantedBy=timers.target diff --git a/systemd/restic-backup.timer b/systemd/restic-backup.timer new file mode 100644 index 0000000..a2c0b47 --- /dev/null +++ b/systemd/restic-backup.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Run Restic Daily Backup + +[Timer] +OnCalendar=*-*-* 03:00:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/systemd/restic-check.service b/systemd/restic-check.service new file mode 100644 index 0000000..66d87f1 --- /dev/null +++ b/systemd/restic-check.service @@ -0,0 +1,17 @@ +[Unit] +Description=Restic Weekly Repository Integrity Check +Wants=network-online.target +After=network.target network-online.target + +[Service] +Type=oneshot +# ExecStart path will be replaced by setup.sh during installation +ExecStart=/path/to/be/replaced/restic_check.sh +# Environment variable will be set by setup.sh based on scope +Environment="HOME=/root" +# Memory limits to prevent OOM on VPS +MemoryMax=500M +MemorySwapMax=500M + +[Install] +WantedBy=timers.target diff --git a/systemd/restic-check.timer b/systemd/restic-check.timer new file mode 100644 index 0000000..4d14b37 --- /dev/null +++ b/systemd/restic-check.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Run Restic Weekly Repository Integrity Check + +[Timer] +OnCalendar=weekly +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/tests/basic.bats b/tests/basic.bats new file mode 100644 index 0000000..de4413d --- /dev/null +++ b/tests/basic.bats @@ -0,0 +1,39 @@ +#!/usr/bin/env bats + +@test "all scripts have valid bash syntax" { + for script in *.sh; do # Assuming main scripts are in root + run bash -n "$script" + [ "$status" -eq 0 ] + done +} + +@test "required commands exist" { + for cmd in restic rclone curl git; do + run command -v "$cmd" + [ "$status" -eq 0 ] + done +} + +@test "systemd files have correct syntax" { + for file in systemd/*.{service,timer}; do + # Create a temporary file for verification to replace placeholders + local temp_file=$(mktemp) + sed -e 's|/path/to/be/replaced/restic_backup.sh|/usr/local/bin/restic_backup.sh|' \ + -e 's|# Environment variable will be set by setup.sh based on scope|Environment="RESTIC_ENV_FILE=/tmp/dummy_env"|' \ + "$file" >"$temp_file" + + run systemd-analyze verify "$temp_file" + # systemd-analyze outputs to stderr, so check stderr for success/failure + # Check status and ensure no critical errors in stderr + local systemd_analyze_output="$(output)" + local systemd_analyze_error="$(error)" + + # Ignore the KillMode=none warning from other systemd units if present + systemd_analyze_error=$(echo "$systemd_analyze_error" | grep -v "Unit uses KillMode=none") + + if [ "$status" -ne 0 ] && [ -n "$systemd_analyze_error" ] && [[ "$systemd_analyze_error" != *"not available"* ]]; then + fail "systemd-analyze failed for $file. Output: $systemd_analyze_output\nError: $systemd_analyze_error" + fi + rm "$temp_file" + done +} diff --git a/uninstall_system.sh b/uninstall_system.sh new file mode 100755 index 0000000..1f217e6 --- /dev/null +++ b/uninstall_system.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -euo pipefail + +echo "=== DRestic System Scope Uninstall ===" +echo "WARNING: This will remove DRestic system installation" +echo "" + +# Check for root privileges +if [ "$EUID" -ne 0 ]; then + echo "Error: System scope uninstall requires root privileges." + echo "Please run: sudo $0" + exit 1 +fi + +# Confirmation +read -p "Continue with system uninstall? [y/N] " confirm +if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + echo "Uninstall cancelled." + exit 0 +fi + +echo "Stopping and disabling systemd timers..." +systemctl stop restic-backup.timer restic-check.timer 2>/dev/null || true +systemctl disable restic-backup.timer restic-check.timer 2>/dev/null || true + +echo "Removing systemd service and timer files..." +rm -f /etc/systemd/system/restic-backup.service +rm -f /etc/systemd/system/restic-backup.timer +rm -f /etc/systemd/system/restic-check.service +rm -f /etc/systemd/system/restic-check.timer + +echo "Reloading systemd daemon..." +systemctl daemon-reload + +echo "Removing backup scripts..." +rm -f /usr/local/bin/restic_backup.sh +rm -f /usr/local/bin/restic_check.sh + +echo "" +echo "Remove configuration files? This includes passwords and settings!" +echo "Your backup data in MEGA will remain safe." +read -p "Remove /etc/restic/ and /root/.restic_*? [y/N] " config_confirm + +if [ "$config_confirm" = "y" ] || [ "$config_confirm" = "Y" ]; then + echo "Removing configuration files..." + rm -rf /etc/restic/ + rm -f /root/.restic_* + echo "Configuration removed." +else + echo "Configuration kept at /etc/restic/ and /root/.restic_*" +fi + +echo "" +echo "✓ DRestic system scope uninstall completed!" +echo "• Systemd timers stopped and disabled" +echo "• Scripts removed from /usr/local/bin/" +echo "• Systemd files removed" +if [ "$config_confirm" = "y" ] || [ "$config_confirm" = "Y" ]; then + echo "• Configuration removed" +else + echo "• Configuration preserved" +fi +echo "• Backup data remains safe in MEGA" +echo "" +echo "To remove backup data from MEGA, use:" +echo " rclone purge backup_remote:/restic_backups" +echo " rclone config delete backup_remote" diff --git a/uninstall_user.sh b/uninstall_user.sh new file mode 100755 index 0000000..3305946 --- /dev/null +++ b/uninstall_user.sh @@ -0,0 +1,59 @@ +#!/bin/bash +set -euo pipefail + +echo "=== DRestic User Scope Uninstall ===" +echo "WARNING: This will remove DRestic but keep your backup data in MEGA" +echo "" + +# Confirmation +read -p "Continue with uninstall? [y/N] " confirm +if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + echo "Uninstall cancelled." + exit 0 +fi + +echo "Stopping and disabling systemd timers..." +systemctl --user stop restic-backup.timer restic-check.timer 2>/dev/null || true +systemctl --user disable restic-backup.timer restic-check.timer 2>/dev/null || true + +echo "Removing systemd service and timer files..." +rm -f ~/.config/systemd/user/restic-backup.service +rm -f ~/.config/systemd/user/restic-backup.timer +rm -f ~/.config/systemd/user/restic-check.service +rm -f ~/.config/systemd/user/restic-check.timer + +echo "Reloading systemd daemon..." +systemctl --user daemon-reload + +echo "Removing backup scripts..." +rm -f ~/.local/bin/restic_backup.sh +rm -f ~/.local/bin/restic_check.sh + +echo "" +echo "Remove configuration files? This includes passwords and settings!" +echo "Your backup data in MEGA will remain safe." +read -p "Remove ~/.config/restic/? [y/N] " config_confirm + +if [ "$config_confirm" = "y" ] || [ "$config_confirm" = "Y" ]; then + echo "Removing configuration directory..." + rm -rf ~/.config/restic/ + echo "Configuration removed." +else + echo "Configuration kept at ~/.config/restic/" +fi + +echo "" +echo "✓ DRestic user scope uninstall completed!" +echo "• Systemd timers stopped and disabled" +echo "• Scripts removed from ~/.local/bin/" +echo "• Systemd files removed" +if [ "$config_confirm" = "y" ] || [ "$config_confirm" = "Y" ]; then + echo "• Configuration removed" +else + echo "• Configuration preserved" +fi +echo "• Backup data remains safe in MEGA" +echo "" +echo "To remove backup data from MEGA, use:" +echo " rclone purge backup_remote:/restic_backups" +echo " rclone config delete backup_remote" diff --git a/update_gotify.sh b/update_gotify.sh new file mode 100755 index 0000000..5305752 --- /dev/null +++ b/update_gotify.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -euo pipefail + +ENV_FILE="$1" + +if [ ! -f "$ENV_FILE" ]; then + echo "Error: Environment file not found: $ENV_FILE" + exit 1 +fi + +echo "Current Gotify configuration:" +grep -E "^GOTIFY_(URL|TOKEN)=" "$ENV_FILE" || echo "No Gotify configuration found" +echo + +read -rp "Enter new Gotify URL (no trailing slash, e.g. https://gotify.example.com): " GOTIFY_URL +if [ -n "$GOTIFY_URL" ]; then + # Validate URL format + if [[ ! "$GOTIFY_URL" =~ ^https?:// ]]; then + echo "Error: Invalid URL format (must start with http:// or https://)" + exit 1 + fi + + # Test URL reachability + echo "Testing Gotify server connectivity..." + if ! curl -s --connect-timeout 10 "$GOTIFY_URL/health" >/dev/null 2>&1; then + echo "Warning: Cannot reach Gotify server at $GOTIFY_URL" + echo "This might be due to network issues or incorrect URL." + read -rp "Continue anyway? [y/N]: " continue_anyway + if [[ ! "$continue_anyway" =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 1 + fi + else + echo "✓ Gotify server is reachable" + fi + + read -rp "Enter new Gotify token: " GOTIFY_TOKEN + + # Test token if both URL and token are provided + if [ -n "$GOTIFY_TOKEN" ]; then + echo "Testing Gotify token..." + if curl -sS "$GOTIFY_URL/message?token=$GOTIFY_TOKEN" \ + -F "title=DRestic Config Test ($(whoami)@$(hostname))" \ + -F "message=Testing Gotify configuration from $(whoami)@$(hostname) - you can ignore this message" \ + -F "priority=1" >/dev/null 2>&1; then + echo "✓ Test notification sent successfully!" + else + echo "Warning: Failed to send test notification. Please verify your token." + read -rp "Continue anyway? [y/N]: " continue_anyway + if [[ ! "$continue_anyway" =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 1 + fi + fi + fi +else + GOTIFY_TOKEN="" +fi + +# Update or add Gotify settings +sed -i '/^GOTIFY_URL=/d' "$ENV_FILE" +sed -i '/^GOTIFY_TOKEN=/d' "$ENV_FILE" +echo "GOTIFY_URL=\"$GOTIFY_URL\"" >> "$ENV_FILE" +echo "GOTIFY_TOKEN=\"$GOTIFY_TOKEN\"" >> "$ENV_FILE" + +echo "✓ Gotify configuration updated!" +if [ -n "$GOTIFY_URL" ]; then + echo "Test with: make test-remote-gotify" +else + echo "Gotify notifications disabled" +fi -- cgit v1.2.3