Initial commit: Add quantumrick backup system

- Automated backup system using Restic
- Systemd timer integration for scheduled backups
- Service management tools (start/stop/restore)
- Multi-service support with template-based configuration
- Backup and restore functionality with test/production modes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Nicolas Duhamel 2025-06-23 17:05:19 +02:00
commit 75a1e66ed2
9 changed files with 1162 additions and 0 deletions

54
backup/gen-conf.sh Normal file
View File

@ -0,0 +1,54 @@
#!/bin/bash
# Script to generate Restic configuration with secure password
# Location: /home/citadel/services/paperless/generate-restic-config.sh
set -e
CONFIG_FILE="/home/citadel/backup/restic.conf"
REPO_PATH="/mnt/data/backup/quantumrick"
echo "=== Generating Restic Configuration ==="
# Check if config already exists
if [ -f "$CONFIG_FILE" ]; then
echo "Configuration file already exists at $CONFIG_FILE"
read -p "Do you want to overwrite it? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
fi
# Generate secure password (32 characters)
echo "Generating secure password..."
RESTIC_PASSWORD=$(openssl rand -base64 32)
# Create configuration file
echo "Creating configuration file..."
cat > "$CONFIG_FILE" << EOF
# Restic configuration for Paperless NGX backups
# Generated on $(date)
# Repository path
export RESTIC_REPOSITORY="$REPO_PATH"
# Repository password
export RESTIC_PASSWORD="$RESTIC_PASSWORD"
# Cache directory (optional)
export RESTIC_CACHE_DIR="/tmp/restic-cache"
EOF
# Set secure permissions
chmod 600 "$CONFIG_FILE"
echo "✅ Configuration file created at $CONFIG_FILE"
echo "🔒 Password generated and saved securely"
echo ""
echo "⚠️ IMPORTANT: Save this password somewhere safe!"
echo " If you lose it, you won't be able to restore your backups!"
echo ""
echo "Password: $RESTIC_PASSWORD"
echo ""

51
backup/init-restic.sh Normal file
View File

@ -0,0 +1,51 @@
#!/bin/bash
# Script to initialize Restic repository for Paperless NGX backups
# Location: /home/citadel/services/paperless/init-restic-repo.sh
set -e
REPO_PATH="/mnt/data/backup/quantumrick"
CONFIG_FILE="/home/citadel/backup/restic.conf"
echo "=== Initializing Restic Repository ==="
# Check if repository directory exists
if [ ! -d "$(dirname "$REPO_PATH")" ]; then
echo "Creating backup directory..."
sudo mkdir -p "$(dirname "$REPO_PATH")"
sudo chown $(whoami):$(whoami) "$(dirname "$REPO_PATH")"
fi
# Check if config file exists
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: Configuration file $CONFIG_FILE not found!"
echo "Please run generate-restic-config.sh first to create the configuration."
exit 1
fi
# Source the configuration
source "$CONFIG_FILE"
# Check if repository is already initialized
if restic -r "$REPO_PATH" cat config &>/dev/null; then
echo "Repository already initialized at $REPO_PATH"
exit 0
fi
echo "Initializing new Restic repository at $REPO_PATH"
# Initialize the repository
restic init -r "$REPO_PATH"
if [ $? -eq 0 ]; then
echo "✅ Repository successfully initialized!"
echo "Repository path: $REPO_PATH"
echo "Configuration: $CONFIG_FILE"
else
echo "❌ Failed to initialize repository"
exit 1
fi
echo "=== Repository Information ==="
restic -r "$REPO_PATH" stats

138
backup/install-service Executable file
View File

@ -0,0 +1,138 @@
#!/bin/bash
# Generic script to install systemd timer for any service backup
# Location: /home/citadel/backup/install-service-timer.sh
# Usage: sudo ./install-service.sh <service_name> [schedule]
set -e
# Check arguments
if [ $# -lt 1 ]; then
echo "Usage: $0 <service_name> [schedule]"
echo ""
echo "Examples:"
echo " $0 paperless # Daily at 3:00 AM (default)"
echo " $0 nextcloud \"*-*-* 02:30:00\" # Daily at 2:30 AM"
echo " $0 gitlab \"Mon *-*-* 04:00:00\" # Weekly on Monday at 4:00 AM"
echo ""
echo "Available services with backup.sh:"
find /home/citadel/services -name "backup.sh" -printf " %h\n" | sed 's|/home/citadel/services/||' 2>/dev/null || echo " (none found)"
exit 1
fi
SERVICE_NAME="$1"
SCHEDULE="${2:-*-*-* 03:00:00}" # Default: daily at 3:00 AM
SERVICE_DIR="/home/citadel/services/$SERVICE_NAME"
BACKUP_SCRIPT="$SERVICE_DIR/backup.sh"
BACKUP_DIR="/home/citadel/backup"
# Systemd template files
SERVICE_TEMPLATE="service-backup@.service"
TIMER_TEMPLATE="service-backup@.timer"
SYSTEMD_DIR="/etc/systemd/system"
# Instance names
SERVICE_INSTANCE="service-backup@$SERVICE_NAME.service"
TIMER_INSTANCE="service-backup@$SERVICE_NAME.timer"
echo "=== Installing Systemd Timer for $SERVICE_NAME Backup ==="
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "This script needs to be run with sudo for systemd operations"
echo "Usage: sudo $0 $SERVICE_NAME"
exit 1
fi
# Check if service directory exists
if [ ! -d "$SERVICE_DIR" ]; then
echo "ERROR: Service directory not found: $SERVICE_DIR"
exit 1
fi
# Check if backup script exists
if [ ! -f "$BACKUP_SCRIPT" ]; then
echo "ERROR: Backup script not found: $BACKUP_SCRIPT"
echo "Please create a backup.sh script in the service directory"
exit 1
fi
# Make backup script executable
chmod +x "$BACKUP_SCRIPT"
# Check if template files exist
if [ ! -f "$BACKUP_DIR/$SERVICE_TEMPLATE" ]; then
echo "ERROR: Service template not found: $BACKUP_DIR/$SERVICE_TEMPLATE"
exit 1
fi
if [ ! -f "$BACKUP_DIR/$TIMER_TEMPLATE" ]; then
echo "ERROR: Timer template not found: $BACKUP_DIR/$TIMER_TEMPLATE"
exit 1
fi
# Stop and disable existing timer if it exists
if systemctl is-active --quiet "$TIMER_INSTANCE" 2>/dev/null; then
echo "Stopping existing timer..."
systemctl stop "$TIMER_INSTANCE"
fi
if systemctl is-enabled --quiet "$TIMER_INSTANCE" 2>/dev/null; then
echo "Disabling existing timer..."
systemctl disable "$TIMER_INSTANCE"
fi
# Copy template files if they don't exist in systemd directory
if [ ! -f "$SYSTEMD_DIR/$SERVICE_TEMPLATE" ]; then
echo "Installing service template..."
cp "$BACKUP_DIR/$SERVICE_TEMPLATE" "$SYSTEMD_DIR/"
chmod 644 "$SYSTEMD_DIR/$SERVICE_TEMPLATE"
fi
if [ ! -f "$SYSTEMD_DIR/$TIMER_TEMPLATE" ]; then
echo "Installing timer template..."
cp "$BACKUP_DIR/$TIMER_TEMPLATE" "$SYSTEMD_DIR/"
chmod 644 "$SYSTEMD_DIR/$TIMER_TEMPLATE"
fi
# Create custom timer with specific schedule if different from default
if [ "$SCHEDULE" != "*-*-* 03:00:00" ]; then
echo "Creating custom timer with schedule: $SCHEDULE"
sed "s|OnCalendar=\*-\*-\* 03:00:00|OnCalendar=$SCHEDULE|" \
"$BACKUP_DIR/$TIMER_TEMPLATE" > "$SYSTEMD_DIR/$TIMER_INSTANCE"
chmod 644 "$SYSTEMD_DIR/$TIMER_INSTANCE"
fi
# Reload systemd
echo "Reloading systemd daemon..."
systemctl daemon-reload
# Enable and start timer
echo "Enabling and starting timer for $SERVICE_NAME..."
systemctl enable "service-backup@$SERVICE_NAME.timer"
systemctl start "service-backup@$SERVICE_NAME.timer"
# Create log file with proper permissions
LOG_FILE="/var/log/$SERVICE_NAME-backup.log"
touch "$LOG_FILE"
chown citadel:citadel "$LOG_FILE"
echo ""
echo "✅ Systemd timer installed successfully for $SERVICE_NAME!"
echo ""
echo "📋 Useful commands:"
echo " Check timer status: systemctl status service-backup@$SERVICE_NAME.timer"
echo " View timer schedule: systemctl list-timers service-backup@$SERVICE_NAME.timer"
echo " Run backup manually: systemctl start service-backup@$SERVICE_NAME.service"
echo " View backup logs: journalctl -u service-backup@$SERVICE_NAME.service"
echo " View recent logs: journalctl -u service-backup@$SERVICE_NAME.service -f"
echo " Stop timer: systemctl stop service-backup@$SERVICE_NAME.timer"
echo " Disable timer: systemctl disable service-backup@$SERVICE_NAME.timer"
echo ""
echo "📅 Schedule: $SCHEDULE (with up to 5min random delay)"
echo "📝 Logs: Available via journalctl and $LOG_FILE"
echo ""
echo "🔍 Current timer status:"
systemctl status "service-backup@$SERVICE_NAME.timer" --no-pager -l || true

156
backup/list-snapshots Executable file
View File

@ -0,0 +1,156 @@
#!/bin/bash
# Script to list available snapshots from Restic repository
# Location: /home/citadel/backup/list-snapshots
# Usage: ./list-snapshots [service_name]
set -e
# Configuration
BACKUP_DIR="/home/citadel/backup"
CONFIG_FILE="$BACKUP_DIR/restic.conf"
# Colors for output
BLUE='\033[0;34m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
show_help() {
echo "Usage: $0 [service_name]"
echo ""
echo "List available snapshots from Restic repository"
echo ""
echo "Examples:"
echo " $0 # List all snapshots grouped by service"
echo " $0 paperless # List snapshots for paperless service only"
echo ""
}
log() {
echo -e "${BLUE}[INFO]${NC} $1"
}
list_all_snapshots() {
log "All available snapshots:"
echo ""
local snapshots=$(restic snapshots --json 2>/dev/null)
if [ -z "$snapshots" ] || [ "$snapshots" = "null" ] || [ "$snapshots" = "[]" ]; then
echo "No snapshots found in repository."
return
fi
# Filter out timestamp tags (like paperless-20250609-115655) and retention tags
local services=$(echo "$snapshots" | jq -r '.[].tags[]' | grep -v -E '^(daily|weekly|monthly|yearly)$' | grep -v -E '^.*-[0-9]{8}-[0-9]{6}$' | sort -u)
for service in $services; do
echo -e "${GREEN}=== $service ===${NC}"
# Display snapshots for this service
echo "$snapshots" | jq -r ".[] | select(.tags[] == \"$service\") | \"\(.short_id) \(.time[:19]) \(.summary.total_files_processed) \((.summary.total_bytes_processed / 1024 / 1024 / 1024 * 100 | floor) / 100)\"" | \
awk 'BEGIN {printf "%-10s %-20s %-10s %s\n", "ID", "DATE", "FILES", "SIZE(GB)"; print "-------------------------------------------------------"} {printf "%-10s %-20s %-10s %.2f\n", $1, $2, $3, $4}'
echo ""
done
}
list_service_snapshots() {
local service="$1"
log "Snapshots for service: $service"
echo ""
local snapshots=$(restic snapshots --tag "$service" --json 2>/dev/null)
if [ -z "$snapshots" ] || [ "$snapshots" = "null" ] || [ "$snapshots" = "[]" ]; then
echo "No snapshots found for service: $service"
return
fi
# Display snapshots in a readable format
echo "$snapshots" | jq -r '.[] | "\(.short_id) \(.time[:19]) \(.tags | join(",")) \(.summary.total_files_processed) \((.summary.total_bytes_processed / 1024 / 1024 / 1024 * 100 | floor) / 100)"' | \
awk 'BEGIN {printf "%-10s %-20s %-35s %-10s %s\n", "ID", "DATE", "TAGS", "FILES", "SIZE(GB)"; print "-------------------------------------------------------------------------------------"} {printf "%-10s %-20s %-35s %-10s %.2f\n", $1, $2, $3, $4, $5}'
echo ""
# Show summary
local count=$(echo "$snapshots" | jq length)
local total_size=$(echo "$snapshots" | jq -r '.[].summary.total_bytes_processed' | awk '{sum+=$1} END {printf "%.2f", sum/1024/1024/1024}')
local oldest=$(echo "$snapshots" | jq -r '.[0].time[:19]' 2>/dev/null)
local newest=$(echo "$snapshots" | jq -r '.[-1].time[:19]' 2>/dev/null)
echo -e "${YELLOW}Summary:${NC}"
echo " Total snapshots: $count"
echo " Total size: ${total_size}GB"
if [ "$oldest" != "$newest" ]; then
echo " Date range: $oldest to $newest"
else
echo " Date: $oldest"
fi
}
show_repository_info() {
log "Repository information:"
echo ""
# Repository stats
restic stats --mode raw-data 2>/dev/null | head -10
echo ""
log "Repository status:"
restic check --read-data-subset=1% 2>/dev/null && echo "✅ Repository is healthy" || echo "⚠️ Repository check failed or not available"
}
# Main script logic
main() {
local service="${1:-}"
if [ "$service" = "--help" ] || [ "$service" = "-h" ] || [ "$service" = "help" ]; then
show_help
exit 0
fi
# Check if we're in the backup directory or config exists
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: Configuration file not found: $CONFIG_FILE"
echo "Make sure you're running this from the backup directory or have valid restic.conf"
exit 1
fi
# Source Restic configuration
source "$CONFIG_FILE"
# Test repository access
if ! restic snapshots >/dev/null 2>&1; then
echo "Error: Cannot access Restic repository"
echo "Check your configuration and repository password"
exit 1
fi
# List snapshots
if [ -n "$service" ]; then
list_service_snapshots "$service"
else
list_all_snapshots
fi
# Show repository info
echo ""
show_repository_info
}
# Check for required tools
if ! command -v restic &> /dev/null; then
echo "Error: restic is not installed or not in PATH"
exit 1
fi
if ! command -v jq &> /dev/null; then
echo "Error: jq is not installed (required for JSON parsing)"
exit 1
fi
# Run main function
main "$@"

242
backup/manage Executable file
View File

@ -0,0 +1,242 @@
#!/bin/bash
# Script to manage backup timers for all services
# Location: /home/citadel/backup/manage
set -e
BACKUP_DIR="/home/citadel/backup"
SERVICES_DIR="/home/citadel/services"
show_help() {
echo "Usage: $0 <command> [service_name]"
echo ""
echo "Backup Commands:"
echo " list List all backup timers and their status"
echo " status [service] Show detailed status for service (or all)"
echo " start <service> Start backup timer for service"
echo " stop <service> Stop backup timer for service"
echo " restart <service> Restart backup timer for service"
echo " enable <service> Enable backup timer for service"
echo " disable <service> Disable backup timer for service"
echo " run <service> Run backup manually for service"
echo " logs <service> Show logs for service backup"
echo " install <service> Install timer for service"
echo " remove <service> Remove timer for service"
echo " available List services with backup.sh available"
echo ""
echo "Restore Commands:"
echo " snapshots [service] List available snapshots"
echo " restore <service> Restore service (test mode)"
echo " restore-prod <service> Restore service (production mode)"
echo " cleanup-test [service] Clean up test restore instances"
echo ""
echo "Examples:"
echo " $0 list"
echo " $0 status paperless"
echo " $0 run paperless"
echo " $0 logs paperless"
echo " $0 snapshots paperless"
echo " $0 restore paperless"
echo " $0 cleanup-test"
}
list_timers() {
echo "=== Backup Timers Status ==="
echo ""
# Get all service-backup timers
local timer_output=$(systemctl list-timers "service-backup@*.timer" --all --no-legend 2>/dev/null || true)
if [ -z "$timer_output" ]; then
echo "No backup timers found."
echo ""
echo "Available services with backup.sh:"
find "$SERVICES_DIR" -name "backup.sh" -printf " %h\n" | sed "s|$SERVICES_DIR/||" 2>/dev/null || echo " (none found)"
return
fi
printf "%-20s %-10s %-15s %s\n" "SERVICE" "STATUS" "ENABLED" "NEXT RUN"
printf "%-20s %-10s %-15s %s\n" "-------" "------" "-------" "--------"
# Parse each line from systemctl list-timers
# Format: NEXT(1-4) LEFT(5) LAST(6-9) PASSED(10) UNIT(11) ACTIVATES(12)
echo "$timer_output" | while read -r line; do
# Extract timer name (11th field)
local timer=$(echo "$line" | awk '{print $11}')
# Extract service name from timer name
local service_name=$(echo "$timer" | sed 's/service-backup@\(.*\)\.timer/\1/')
# Skip if service name is empty
if [ -z "$service_name" ]; then
continue
fi
# Get next run (first 4 fields for date/time)
local next_run=$(echo "$line" | awk '{print $1, $2, $3, $4}')
# Get status and enabled state
local status=$(systemctl is-active "$timer" 2>/dev/null || echo "inactive")
local enabled=$(systemctl is-enabled "$timer" 2>/dev/null || echo "disabled")
printf "%-20s %-10s %-15s %s\n" "$service_name" "$status" "$enabled" "$next_run"
done
}
show_status() {
local service="$1"
if [ -n "$service" ]; then
echo "=== Status for $service ==="
systemctl status "service-backup@$service.timer" --no-pager -l
echo ""
echo "Next scheduled runs:"
systemctl list-timers "service-backup@$service.timer" --no-legend
else
echo "=== All Backup Timers Status ==="
systemctl list-timers "service-backup@*.timer" --no-legend
fi
}
manage_timer() {
local action="$1"
local service="$2"
if [ -z "$service" ]; then
echo "ERROR: Service name required for $action"
exit 1
fi
local timer="service-backup@$service.timer"
local service_unit="service-backup@$service.service"
case $action in
start)
systemctl start "$timer"
echo "✅ Started timer for $service"
;;
stop)
systemctl stop "$timer"
echo "✅ Stopped timer for $service"
;;
restart)
systemctl restart "$timer"
echo "✅ Restarted timer for $service"
;;
enable)
systemctl enable "$timer"
echo "✅ Enabled timer for $service"
;;
disable)
systemctl disable "$timer"
echo "✅ Disabled timer for $service"
;;
run)
echo "Running backup for $service..."
systemctl start "$service_unit"
echo "✅ Backup started for $service"
echo "Monitor with: journalctl -u $service_unit -f"
;;
logs)
journalctl -u "$service_unit" --no-pager -l
;;
remove)
if systemctl is-active --quiet "$timer" 2>/dev/null; then
systemctl stop "$timer"
fi
if systemctl is-enabled --quiet "$timer" 2>/dev/null; then
systemctl disable "$timer"
fi
echo "✅ Removed timer for $service"
;;
esac
}
install_timer() {
local service="$1"
if [ -z "$service" ]; then
echo "ERROR: Service name required for install"
exit 1
fi
if [ "$EUID" -ne 0 ]; then
echo "ERROR: Install requires sudo privileges"
echo "Usage: sudo $0 install $service"
exit 1
fi
"$BACKUP_DIR/install" "$service"
}
list_available() {
echo "=== Services with backup.sh available ==="
echo ""
local found=false
for service_dir in "$SERVICES_DIR"/*; do
if [ -d "$service_dir" ] && [ -f "$service_dir/backup.sh" ]; then
local service_name=$(basename "$service_dir")
local timer_status="not installed"
if systemctl is-enabled --quiet "service-backup@$service_name.timer" 2>/dev/null; then
timer_status="installed"
fi
printf " %-20s (timer: %s)\n" "$service_name" "$timer_status"
found=true
fi
done
if [ "$found" = false ]; then
echo "No services with backup.sh found in $SERVICES_DIR"
fi
}
# Main script logic
case "${1:-}" in
list)
list_timers
;;
status)
show_status "$2"
;;
start|stop|restart|enable|disable|run|logs|remove)
if [ "$1" = "remove" ] || [ "$1" = "enable" ] || [ "$1" = "disable" ] || [ "$1" = "start" ] || [ "$1" = "stop" ] || [ "$1" = "restart" ]; then
if [ "$EUID" -ne 0 ]; then
echo "ERROR: $1 requires sudo privileges"
echo "Usage: sudo $0 $1 $2"
exit 1
fi
fi
manage_timer "$1" "$2"
;;
install)
install_timer "$2"
;;
available)
list_available
;;
snapshots)
"$BACKUP_DIR/list-snapshots" "$2"
;;
restore)
"$BACKUP_DIR/restore" "$2" --test
;;
restore-prod)
"$BACKUP_DIR/restore" "$2" --production
;;
cleanup-test)
"$BACKUP_DIR/cleanup-test" "$2"
;;
help|--help|-h)
show_help
;;
*)
echo "ERROR: Unknown command '${1:-}'"
echo ""
show_help
exit 1
;;
esac

11
backup/restic.conf Normal file
View File

@ -0,0 +1,11 @@
# Restic configuration for Paperless NGX backups
# Generated on Mon Jun 9 11:45:41 CEST 2025
# Repository path
export RESTIC_REPOSITORY="/mnt/data/backup/quantumrick"
# Repository password
export RESTIC_PASSWORD="CHANGE-ME"
# Cache directory (optional)
export RESTIC_CACHE_DIR="/tmp/restic-cache"

478
backup/restore Executable file
View File

@ -0,0 +1,478 @@
#!/bin/bash
# Restic backup restore script
# Location: /home/citadel/backup/restore
# Usage: ./restore <service_name> [--test|--production]
set -euo pipefail # Strict mode: exit on error, undefined vars, pipe failures
# Configuration
readonly BACKUP_DIR="/home/citadel/backup"
readonly SERVICES_DIR="/home/citadel/services"
readonly CONFIG_FILE="$BACKUP_DIR/restic.conf"
readonly SCRIPT_NAME=$(basename "$0")
readonly VERSION="2.1.0"
# Global variables
USER_CHOICE=""
# Colors
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m'
# Logging functions
log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
# Check dependencies
check_dependencies() {
local deps=("restic" "jq" "awk" "grep")
local missing=()
for dep in "${deps[@]}"; do
if ! command -v "$dep" &>/dev/null; then
missing+=("$dep")
fi
done
if [ ${#missing[@]} -ne 0 ]; then
error "Missing dependencies: ${missing[*]}"
fi
}
# Show help
show_help() {
cat << EOF
Usage: $SCRIPT_NAME <service_name> [OPTIONS]
Options:
--test Restore to test instance (default)
--production Restore to production instance
-h, --help Show this help message
-v, --version Show version
Examples:
$SCRIPT_NAME paperless # Restore in test mode (default)
$SCRIPT_NAME paperless --test # Restore to test instance
$SCRIPT_NAME paperless --production # Restore to production (requires confirmation)
Available services:
EOF
if [ -f "$CONFIG_FILE" ]; then
# Secure validation of config file
if grep -qE '^[^#]*export\s+RESTIC_' "$CONFIG_FILE"; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
# List services from snapshots
if restic snapshots &>/dev/null; then
restic snapshots --json 2>/dev/null | \
jq -r '.[].tags[]' 2>/dev/null | \
grep -v -E '^(daily|weekly|monthly|yearly)$' | \
sort -u | sed 's/^/ /' || echo " (cannot list services)"
else
echo " (cannot access repository)"
fi
else
error "Invalid configuration file format"
fi
else
echo " (no config found)"
fi
}
# Check prerequisites
check_prerequisites() {
# Check configuration file
if [ ! -f "$CONFIG_FILE" ]; then
error "Config file not found: $CONFIG_FILE"
fi
# Check config file permissions
if [ ! -r "$CONFIG_FILE" ]; then
error "Cannot read config file: $CONFIG_FILE"
fi
# Secure validation before source
if ! grep -qE '^[^#]*export\s+RESTIC_' "$CONFIG_FILE"; then
error "Invalid configuration file format"
fi
# Source config
# shellcheck source=/dev/null
source "$CONFIG_FILE"
# Check required variables
if [ -z "${RESTIC_REPOSITORY:-}" ]; then
error "RESTIC_REPOSITORY not defined in config"
fi
# Test restic access
if ! restic snapshots &>/dev/null; then
error "Cannot access Restic repository"
fi
log "Prerequisites OK"
}
# Show available snapshots
show_snapshots() {
local service="$1"
# Validate service name
if [[ ! "$service" =~ ^[a-zA-Z0-9_-]+$ ]]; then
error "Invalid service name format"
fi
echo ""
log "Available snapshots for $service:"
echo ""
# Get snapshots with error handling
local snapshots_json
if ! snapshots_json=$(restic snapshots --tag "$service" --json 2>/dev/null); then
error "Failed to retrieve snapshots"
fi
if [ -z "$snapshots_json" ] || [ "$snapshots_json" = "[]" ]; then
warning "No snapshots found for service: $service"
return 1
fi
# Format and display snapshots
echo "$snapshots_json" | jq -r '.[] | "\(.short_id) \(.time[:19]) \(.summary.total_files_processed // "?") files"' | \
awk 'BEGIN {printf "%-12s %-20s %s\n", "ID", "DATE", "FILES"; print "----------------------------------------"}
{printf "%-12s %-20s %s\n", $1, $2, $3" "$4}'
echo ""
return 0
}
# Get user choice
get_user_choice() {
local max_attempts=3
local attempt=0
while [ $attempt -lt $max_attempts ]; do
echo ""
echo "Enter snapshot ID (or 'latest' for most recent, 'q' to quit):"
echo -n "> "
read -r USER_CHOICE
echo ""
# Basic validation
if [ "$USER_CHOICE" = "q" ] || [ "$USER_CHOICE" = "quit" ]; then
log "Operation cancelled by user"
exit 0
fi
if [ -n "$USER_CHOICE" ]; then
# Format validation (alphanumeric or "latest")
if [[ "$USER_CHOICE" =~ ^[a-zA-Z0-9]+$ ]] || [ "$USER_CHOICE" = "latest" ]; then
return 0
else
warning "Invalid snapshot ID format. Please use alphanumeric characters only."
fi
else
warning "No selection made"
fi
((attempt++))
done
error "Maximum attempts exceeded"
}
# Resolve snapshot ID
resolve_snapshot_id() {
local service="$1"
local choice="$2"
if [ "$choice" = "latest" ]; then
local latest_id
latest_id=$(restic snapshots --tag "$service" --json 2>/dev/null | jq -r '.[-1].short_id // empty')
if [ -z "$latest_id" ]; then
error "No snapshots found for service: $service"
fi
echo "$latest_id"
else
echo "$choice"
fi
}
# Verify snapshot
verify_snapshot() {
local service="$1"
local snapshot_id="$2"
log "Verifying snapshot $snapshot_id for service $service"
# Get snapshot info
local snapshot_info
snapshot_info=$(restic snapshots --tag "$service" --json 2>/dev/null | \
jq -r ".[] | select(.short_id == \"$snapshot_id\")")
if [ -z "$snapshot_info" ]; then
error "Snapshot $snapshot_id not found for service $service"
fi
# Extract date
local date
date=$(echo "$snapshot_info" | jq -r '.time[:19]')
# Check integrity (optional but recommended)
if restic check --read-data-subset=1/10 "$snapshot_id" &>/dev/null; then
success "Found snapshot $snapshot_id from $date (integrity check passed)"
else
warning "Found snapshot $snapshot_id from $date (integrity check skipped)"
fi
}
# Check service capabilities
check_service_capabilities() {
local service="$1"
local service_dir="$SERVICES_DIR/$service"
echo ""
log "Checking service capabilities for $service"
# Check service directory exists
if [ ! -d "$service_dir" ]; then
error "Service directory not found: $service_dir"
fi
# Check for custom restore script
local custom_restore="$service_dir/restore"
local has_custom_restore=false
if [ -f "$custom_restore" ] && [ -x "$custom_restore" ]; then
has_custom_restore=true
success "Found custom restore script: $custom_restore"
else
log "No custom restore script found - will extract to temporary directory"
fi
echo "$has_custom_restore"
}
# Create secure temporary directory
create_secure_temp_dir() {
local prefix="${1:-restore}"
local temp_dir
# Use mktemp to create a secure temporary directory
temp_dir=$(mktemp -d "/tmp/${prefix}-$(date +%Y%m%d-%H%M%S)-XXXXXX")
if [ ! -d "$temp_dir" ]; then
error "Failed to create temporary directory"
fi
# Set restrictive permissions
chmod 700 "$temp_dir"
echo "$temp_dir"
}
# Extract snapshot to temporary directory
extract_snapshot_to_temp() {
local snapshot_id="$1"
local temp_dir
temp_dir=$(create_secure_temp_dir "restore")
log "Extracting snapshot $snapshot_id to temporary directory"
log "Target directory: $temp_dir"
# Extract with error handling
if ! restic restore "$snapshot_id" --target "$temp_dir" 2>&1 | tee -a "$temp_dir/restore.log"; then
error "Failed to extract snapshot"
fi
success "Snapshot extracted to: $temp_dir"
echo ""
echo "📁 Contents of the restore:"
ls -la "$temp_dir" | head -20
echo ""
echo "💡 Manual restore instructions:"
echo " 1. Review the extracted data in: $temp_dir"
echo " 2. Copy the required files to their destination"
echo " 3. Verify permissions and ownership"
echo " 4. Remove the temporary directory: rm -rf $temp_dir"
echo ""
echo "⚠️ The directory will remain until you delete it manually"
}
# Run custom restore
run_custom_restore() {
local service="$1"
local snapshot_id="$2"
local mode="$3"
local service_dir="$SERVICES_DIR/$service"
local custom_restore="$service_dir/restore"
log "Running custom restore script for $service"
log "Script: $custom_restore"
log "Snapshot: $snapshot_id"
log "Mode: $mode"
# Create restore log
local log_file="/tmp/restore-${service}-$(date +%Y%m%d-%H%M%S).log"
# Change directory with verification
if ! cd "$service_dir"; then
error "Cannot change to service directory: $service_dir"
fi
# Execute restore script with log capture
if "$custom_restore" "$snapshot_id" "--$mode" 2>&1 | tee "$log_file"; then
success "Custom restore script completed"
log "Restore log saved to: $log_file"
else
error "Custom restore script failed. Check log: $log_file"
fi
}
# Main function
main() {
# Check dependencies
check_dependencies
# Parse arguments
if [ $# -eq 0 ]; then
show_help
exit 0
fi
local service=""
local mode="test" # Default mode: test (safer)
# Parse arguments
while [ $# -gt 0 ]; do
case "$1" in
--test)
mode="test"
shift
;;
--production)
mode="production"
shift
;;
-h|--help)
show_help
exit 0
;;
-v|--version)
echo "$SCRIPT_NAME version $VERSION"
exit 0
;;
-*)
error "Unknown option: $1"
;;
*)
if [ -z "$service" ]; then
service="$1"
else
error "Too many arguments"
fi
shift
;;
esac
done
# Check service is specified
if [ -z "$service" ]; then
error "Service name required"
fi
# Validate service name format
if [[ ! "$service" =~ ^[a-zA-Z0-9_-]+$ ]]; then
error "Invalid service name format. Use only alphanumeric characters, hyphens, and underscores."
fi
# Check service directory exists
if [ ! -d "$SERVICES_DIR/$service" ]; then
error "Service directory not found: $SERVICES_DIR/$service"
fi
echo "=== Restore $service ==="
log "Mode: $mode"
# Step 1: Check prerequisites
check_prerequisites
# Step 2: Check service capabilities
local has_custom_restore
has_custom_restore=$(check_service_capabilities "$service")
# Step 3: Show snapshots
if ! show_snapshots "$service"; then
exit 1
fi
# Step 4: Get user choice
get_user_choice
local choice="$USER_CHOICE"
# Step 5: Resolve and verify snapshot
local snapshot_id
snapshot_id=$(resolve_snapshot_id "$service" "$choice")
verify_snapshot "$service" "$snapshot_id"
# Step 6: Display restore plan
echo ""
log "Restore plan:"
echo " Service: $service"
echo " Snapshot: $snapshot_id"
echo " Mode: $mode"
if [ "$has_custom_restore" = "true" ]; then
echo " Method: Custom restore script"
else
echo " Method: Extract to temporary directory"
fi
# Step 7: Final confirmation
echo ""
if [ "$mode" = "test" ]; then
read -p "Proceed with TEST restore? [y/N]: " -r confirm
if [[ ! $confirm =~ ^[Yy]$ ]]; then
echo "Cancelled."
exit 0
fi
else
echo -e "${RED}⚠️ WARNING: This is a PRODUCTION restore!${NC}"
echo -e "${RED}This operation may overwrite existing data.${NC}"
echo ""
read -p "Type 'yes-production' to proceed: " -r confirm
if [ "$confirm" != "yes-production" ]; then
echo "Cancelled."
exit 0
fi
fi
# Step 8: Execute restore
echo ""
if [ "$has_custom_restore" = "true" ]; then
run_custom_restore "$service" "$snapshot_id" "$mode"
else
# No custom script, extract to temp for both test and production
extract_snapshot_to_temp "$snapshot_id"
fi
echo ""
success "Restore operation completed!"
}
# Trap to clean up on interruption
trap 'echo -e "\n${RED}Interrupted${NC}"; exit 130' INT TERM
# Entry point
main "$@"

View File

@ -0,0 +1,18 @@
[Unit]
Description=Backup Service for %i
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
User=citadel
Group=citadel
WorkingDirectory=/home/citadel/services/%i
ExecStart=/home/citadel/services/%i/backup.sh
StandardOutput=journal
StandardError=journal
Environment=PATH=/usr/local/bin:/usr/bin:/bin
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,14 @@
[Unit]
Description=Daily Backup Timer for %i
Requires=service-backup@%i.service
[Timer]
# Run daily at 3:00 AM
OnCalendar=*-*-* 03:00:00
# If system was down during scheduled time, run on next boot
Persistent=true
# Add randomization to avoid conflicts with other services
RandomizedDelaySec=300
[Install]
WantedBy=timers.target