#!/bin/bash set -euo pipefail ####################################### # Determine backup mode from the environment only. # Valid values: "directory" or "postgres". # Default to "directory" if not provided. ####################################### BACKUP_MODE="${BACKUP_MODE:-directory}" ####################################### # Check for required external commands. ####################################### REQUIRED_CMDS=(restic curl jq) if [ "$BACKUP_MODE" = "postgres" ]; then REQUIRED_CMDS+=(pg_dump) fi for cmd in "${REQUIRED_CMDS[@]}"; do if ! command -v "$cmd" &>/dev/null; then echo "Error: Required command '$cmd' is not installed." >&2 exit 1 fi done ####################################### # Validate common required environment variables. ####################################### # Gotify notification settings (optional). # Set ENABLE_GOTIFY to "true" to enable notifications, any other value or unset disables them. ENABLE_GOTIFY="${ENABLE_GOTIFY:-true}" if [ "$ENABLE_GOTIFY" = "true" ]; then : "${GOTIFYHOST:?Environment variable GOTIFYHOST is not set (required when ENABLE_GOTIFY=true)}" : "${GOTIFYTOKEN:?Environment variable GOTIFYTOKEN is not set (required when ENABLE_GOTIFY=true)}" : "${GOTIFYTOPIC:?Environment variable GOTIFYTOPIC is not set (required when ENABLE_GOTIFY=true)}" else log "Gotify notifications disabled. Backup status will be logged to console only." fi # Restic encryption password. : "${RESTIC_PASSWORD:?Environment variable RESTIC_PASSWORD is not set}" # Use the repository URI directly from the environment. # Example: export RESTIC_REPOSITORY="rest:http://your-rest-server:8000/backup" : "${RESTIC_REPOSITORY:?Environment variable RESTIC_REPOSITORY is not set}" ####################################### # Validate mode-specific environment variables. ####################################### case "$BACKUP_MODE" in directory) : "${SOURCEDIR:?Environment variable SOURCEDIR is not set (required for directory backup mode)}" ;; postgres) : "${PGHOST:?Environment variable PGHOST is not set (required for PostgreSQL backup mode)}" : "${PGDATABASE:?Environment variable PGDATABASE is not set (required for PostgreSQL backup mode)}" : "${PGUSER:?Environment variable PGUSER is not set (required for PostgreSQL backup mode)}" # Optional: default PGPORT to 5432. : "${PGPORT:=5432}" if [ -z "${PGPASSWORD:-}" ]; then echo "Warning: Environment variable PGPASSWORD is not set. pg_dump may fail if authentication is required." fi ;; *) echo "Error: Unknown backup mode '$BACKUP_MODE'. Valid modes are 'directory' and 'postgres'." >&2 exit 1 ;; esac ####################################### # Build the Gotify URL (only if Gotify is enabled). ####################################### if [ "$ENABLE_GOTIFY" = "true" ]; then GOTIFYURL="${GOTIFYHOST}/message?token=${GOTIFYTOKEN}" fi ####################################### # Date format for logging. ####################################### LOG_DATE_FORMAT="%Y-%m-%dT%T" ####################################### # Log a message with a timestamp. # Arguments: # Message to log. ####################################### log() { echo "$(date +"$LOG_DATE_FORMAT") - $*" } ####################################### # Send a notification via Gotify. # Arguments: # message: The message to send. ####################################### send_notification() { local message="$1" # Only send notification if Gotify is enabled if [ "$ENABLE_GOTIFY" != "true" ]; then log "$message" return 0 fi if ! curl -s -X POST "$GOTIFYURL" -F "title=${GOTIFYTOPIC}" -F "message=${message}" >/dev/null; then log "Warning: Failed to send notification with message: ${message}" fi } ####################################### # Run the backup using restic. # The --no-cache flag disables local caching. # Arguments: # $1 - The source directory to back up. ####################################### run_restic_backup() { local source_dir="$1" cd "${source_dir}" log "Starting backup of '${source_dir}' to repository ${RESTIC_REPOSITORY}" # Capture both stdout and stderr in a variable backup_output=$(restic -r "${RESTIC_REPOSITORY}" backup --no-cache --json --verbose . 2>&1) # Optionally, also print the output to the console: echo "$backup_output" # Parse the JSON lines output for the summary message summary=$(echo "$backup_output" | jq -r 'select(.message_type=="summary") | "Snapshot " + (.snapshot_id // "none") + ": " + "files new: " + (.files_new|tostring) + ", files changed: " + (.files_changed|tostring) + ", data added: " + (.data_added|tostring) + " bytes in " + (.total_duration|tostring) + " sec"') # Check exit code of restic backup (assuming restic exits non-zero on error) if [ $? -eq 0 ]; then msg="Backup successful. $summary" log "$msg" send_notification "$msg" else exit_code=$? msg="Backup failed with error code ${exit_code}. $backup_output" log "$msg" send_notification "$msg" exit "$exit_code" fi } ####################################### # Backup a directory (regular mode). ####################################### backup_directory() { run_restic_backup "${SOURCEDIR}" } ####################################### # Backup a PostgreSQL database. # Dumps the database to a temporary directory and then backs it up. ####################################### backup_postgres() { log "Starting PostgreSQL backup for database '${PGDATABASE}' on host '${PGHOST}'" # Create a temporary directory for the database dump. TEMP_BACKUP_DIR=$(mktemp -d) log "Created temporary directory: ${TEMP_BACKUP_DIR}" local dump_file="${TEMP_BACKUP_DIR}/dump.sql" log "Dumping PostgreSQL database to ${dump_file}..." if pg_dump -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" ${PG_DUMP_ARGS:-} "${PGDATABASE}" > "${dump_file}"; then log "Database dump created successfully." else local exit_code=$? local msg="PostgreSQL dump failed with error code ${exit_code}" log "$msg" send_notification "$msg" exit "$exit_code" fi # Back up the directory containing the dump. run_restic_backup "${TEMP_BACKUP_DIR}" } ####################################### # Cleanup temporary resources. ####################################### cleanup() { if [ -n "${TEMP_BACKUP_DIR:-}" ] && [ -d "${TEMP_BACKUP_DIR}" ]; then rm -rf "${TEMP_BACKUP_DIR}" log "Removed temporary directory ${TEMP_BACKUP_DIR}" fi } trap cleanup EXIT ####################################### # Main routine. ####################################### main() { case "$BACKUP_MODE" in directory) backup_directory ;; postgres) backup_postgres ;; esac } # Trap termination signals to log and exit cleanly. trap 'log "Script interrupted. Exiting."; exit 1' SIGINT SIGTERM main