summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--LICENSE21
-rw-r--r--Makefile504
-rw-r--r--README.md129
-rw-r--r--common.sh34
-rwxr-xr-xrestic_backup.sh61
-rwxr-xr-xrestic_check.sh43
-rwxr-xr-xsetup.sh370
-rw-r--r--systemd/restic-backup.service17
-rw-r--r--systemd/restic-backup.timer9
-rw-r--r--systemd/restic-check.service17
-rw-r--r--systemd/restic-check.timer9
-rw-r--r--tests/basic.bats39
-rwxr-xr-xuninstall_system.sh67
-rwxr-xr-xuninstall_user.sh59
-rwxr-xr-xupdate_gotify.sh71
16 files changed, 1456 insertions, 0 deletions
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=<user|system>"
+ 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" <<EOF
+/etc
+/home
+/root
+/var/lib/docker
+/opt
+EOF
+ else
+ cat >"$config_dir/paths" <<EOF
+$HOME
+EOF
+ fi
+ log "Created default paths file: $config_dir/paths"
+ else
+ log "Paths file already exists: $config_dir/paths"
+ fi
+
+ if [ ! -f "$config_dir/excludes" ]; then
+ cat >"$config_dir/excludes" <<EOF
+**/.cache
+**/node_modules
+*.tmp
+*.log
+**/.git/objects
+**/.npm
+**/.cargo/registry
+**/.local/share/Trash
+**/Downloads/*.iso
+**/Downloads/*.img
+**/.steam
+**/.wine
+EOF
+ log "Created default excludes file: $config_dir/excludes"
+ else
+ log "Excludes file already exists: $config_dir/excludes"
+ fi
+}
+
+# --- Create paths and excludes files (basic versions) ---
+log "Creating basic paths and excludes files..."
+create_default_config "$CONFIG_DIR" "$SCOPE"
+
+# --- Run basic configuration checks (moved after file creation) ---
+check_basic_config
+
+# --- Create environment file for Restic and Gotify ---
+log "Creating environment file: $ENV_FILE with Restic and Gotify settings"
+cat >"$ENV_FILE" <<EOF
+RESTIC_REPOSITORY="$RESTIC_REPO"
+RESTIC_PASSWORD_FILE="$PASS_FILE"
+CONFIG_DIR="$CONFIG_DIR"
+GOTIFY_URL="$GOTIFY_URL"
+GOTIFY_TOKEN="$GOTIFY_TOKEN"
+# Go garbage collector optimization for low-memory environments
+GOGC=20
+# Rclone throttling settings to prevent timeout issues during prune operations
+RCLONE_TRANSFERS=1
+RCLONE_CHECKERS=1
+RCLONE_TIMEOUT=7200s
+RCLONE_CONTIMEOUT=600s
+RCLONE_LOW_LEVEL_RETRIES=20
+RCLONE_BWLIMIT=1M
+# Rclone memory optimization settings
+RCLONE_BUFFER_SIZE=4M
+RCLONE_USE_MMAP=false
+RCLONE_VFS_CACHE_MODE=off
+RCLONE_MEGA_HARD_DELETE=false
+# Connection reliability settings for intermittent network issues
+RCLONE_RETRIES=10
+RCLONE_RETRIES_SLEEP=30s
+# Memory optimization for VPS environments
+RESTIC_CACHE_DIR=/tmp/restic-cache
+GOMAXPROCS=1
+EOF
+log "Setting permissions for $ENV_FILE to 600"
+chmod 600 "$ENV_FILE" || error "Failed to set permissions on environment file."
+
+# --- Install scripts and systemd units ---
+log "Installing Restic scripts and Systemd units..."
+# Copy scripts
+log "Copying restic_backup.sh to $INSTALL_DIR/"
+cp restic_backup.sh "$INSTALL_DIR/restic_backup.sh" || error "Failed to copy restic_backup.sh"
+log "Copying restic_check.sh to $INSTALL_DIR/"
+cp restic_check.sh "$INSTALL_DIR/restic_check.sh" || error "Failed to copy restic_check.sh"
+log "Copying common.sh to $INSTALL_DIR/"
+cp common.sh "$INSTALL_DIR/common.sh" || error "Failed to copy common.sh"
+log "Making scripts executable: $INSTALL_DIR/restic_backup.sh, $INSTALL_DIR/restic_check.sh, and common.sh"
+chmod +x "$INSTALL_DIR/restic_backup.sh" "$INSTALL_DIR/restic_check.sh" "$INSTALL_DIR/common.sh"
+
+# Prepare systemd service files
+# Use sed to replace the ExecStart path and add the environment variable
+log "Generating systemd service file: $SYSTEMD_DIR/restic-backup.service"
+sed -e "s|ExecStart=.*|ExecStart=$INSTALL_DIR/restic_backup.sh|" \
+ -e "s|# Environment variable will be set by setup.sh based on scope|Environment=\"RESTIC_ENV_FILE=$ENV_FILE\"|" \
+ systemd/restic-backup.service >"$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