All checks were successful
CD / Check changes (push) Successful in 14s
CD / Create tag (push) Successful in 11s
CD / Build and push (amd64) (push) Successful in 26s
CD / Build and push (arm64) (push) Successful in 1m14s
CI / Build Docker image (pull_request) Successful in 18s
CD / Create manifest (push) Successful in 25s
Reviewed-on: #12 Co-authored-by: Timo Behrendt <t.behrendt@t00n.de> Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
337 lines
12 KiB
Bash
337 lines
12 KiB
Bash
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
#######################################
|
|
# 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") - $*"
|
|
}
|
|
|
|
#######################################
|
|
# Determine operation mode from the environment only.
|
|
# Valid values: "backup" or "restore".
|
|
# Default to "backup" if not provided.
|
|
#######################################
|
|
OPERATION_MODE="${OPERATION_MODE:-backup}"
|
|
|
|
#######################################
|
|
# 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
|
|
if [ "$OPERATION_MODE" = "backup" ]; then
|
|
REQUIRED_CMDS+=(pg_dump)
|
|
elif [ "$OPERATION_MODE" = "restore" ]; then
|
|
REQUIRED_CMDS+=(psql)
|
|
fi
|
|
fi
|
|
|
|
for cmd in "${REQUIRED_CMDS[@]}"; do
|
|
if ! command -v "$cmd" &>/dev/null; then
|
|
log "Error: Required command '$cmd' is not installed."
|
|
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 operation mode.
|
|
#######################################
|
|
case "$OPERATION_MODE" in
|
|
backup|restore)
|
|
;;
|
|
*)
|
|
echo "Error: Unknown operation mode '$OPERATION_MODE'. Valid modes are 'backup' and 'restore'." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
#######################################
|
|
# Validate mode-specific environment variables.
|
|
#######################################
|
|
case "$BACKUP_MODE" in
|
|
directory)
|
|
if [ "$OPERATION_MODE" = "backup" ]; then
|
|
: "${SOURCEDIR:?Environment variable SOURCEDIR is not set (required for directory backup mode)}"
|
|
elif [ "$OPERATION_MODE" = "restore" ]; then
|
|
: "${RESTOREDIR:?Environment variable RESTOREDIR is not set (required for directory restore mode)}"
|
|
fi
|
|
;;
|
|
postgres)
|
|
: "${PGHOST:?Environment variable PGHOST is not set (required for PostgreSQL mode)}"
|
|
: "${PGDATABASE:?Environment variable PGDATABASE is not set (required for PostgreSQL mode)}"
|
|
: "${PGUSER:?Environment variable PGUSER is not set (required for PostgreSQL mode)}"
|
|
# Optional: default PGPORT to 5432.
|
|
: "${PGPORT:=5432}"
|
|
if [ -z "${PGPASSWORD:-}" ]; then
|
|
if [ "$OPERATION_MODE" = "backup" ]; then
|
|
echo "Warning: Environment variable PGPASSWORD is not set. pg_dump may fail if authentication is required."
|
|
elif [ "$OPERATION_MODE" = "restore" ]; then
|
|
echo "Warning: Environment variable PGPASSWORD is not set. psql may fail if authentication is required."
|
|
fi
|
|
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
|
|
|
|
#######################################
|
|
# 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}"
|
|
}
|
|
|
|
#######################################
|
|
# Run the restore using restic.
|
|
# Arguments:
|
|
# $1 - The target directory to restore to.
|
|
# $2 - Optional snapshot ID to restore (defaults to latest).
|
|
#######################################
|
|
run_restic_restore() {
|
|
local target_dir="$1"
|
|
local snapshot_id="$2"
|
|
|
|
log "Starting restore from repository ${RESTIC_REPOSITORY} to '${target_dir}'"
|
|
log "Using snapshot: ${snapshot_id}"
|
|
|
|
# Create target directory if it doesn't exist
|
|
mkdir -p "${target_dir}"
|
|
|
|
# Capture both stdout and stderr in a variable
|
|
restore_output=$(restic -r "${RESTIC_REPOSITORY}" restore "${snapshot_id}" --target "${target_dir}" --no-cache --json --verbose 2>&1)
|
|
# Optionally, also print the output to the console:
|
|
echo "$restore_output"
|
|
|
|
# Parse the JSON lines output for the summary message
|
|
summary=$(echo "$restore_output" | jq -r 'select(.message_type=="summary") | "Restore completed: " + (.files_restored|tostring) + " files restored, " + (.bytes_restored|tostring) + " bytes in " + (.total_duration|tostring) + " sec"' 2>/dev/null || echo "Restore completed")
|
|
|
|
# Check exit code of restic restore
|
|
if [ $? -eq 0 ]; then
|
|
msg="Restore successful. $summary"
|
|
log "$msg"
|
|
send_notification "$msg"
|
|
else
|
|
exit_code=$?
|
|
msg="Restore failed with error code ${exit_code}. $restore_output"
|
|
log "$msg"
|
|
send_notification "$msg"
|
|
exit "$exit_code"
|
|
fi
|
|
}
|
|
|
|
#######################################
|
|
# Restore a directory (regular mode).
|
|
#######################################
|
|
restore_directory() {
|
|
local snapshot_id="${RESTORE_SNAPSHOT_ID:-latest}"
|
|
run_restic_restore "${RESTOREDIR}" "${snapshot_id}"
|
|
}
|
|
|
|
#######################################
|
|
# Restore a PostgreSQL database.
|
|
# Restores the database dump from the backup and applies it to the database.
|
|
#######################################
|
|
restore_postgres() {
|
|
local snapshot_id="${RESTORE_SNAPSHOT_ID:-latest}"
|
|
log "Starting PostgreSQL restore for database '${PGDATABASE}' on host '${PGHOST}'"
|
|
|
|
# Create a temporary directory for the restore.
|
|
TEMP_RESTORE_DIR=$(mktemp -d)
|
|
log "Created temporary directory: ${TEMP_RESTORE_DIR}"
|
|
|
|
# Restore the backup to the temporary directory
|
|
run_restic_restore "${TEMP_RESTORE_DIR}" "${snapshot_id}"
|
|
|
|
local dump_file="${TEMP_RESTORE_DIR}/dump.sql"
|
|
if [ ! -f "${dump_file}" ]; then
|
|
local msg="PostgreSQL restore failed. Database dump file not found at ${dump_file}"
|
|
log "$msg"
|
|
send_notification "$msg"
|
|
exit 1
|
|
fi
|
|
|
|
log "Restoring PostgreSQL database from ${dump_file}..."
|
|
if psql -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" -d "${PGDATABASE}" ${PSQL_ARGS:-} < "${dump_file}"; then
|
|
local msg="PostgreSQL database restored successfully"
|
|
log "$msg"
|
|
send_notification "$msg"
|
|
else
|
|
local exit_code=$?
|
|
local msg="PostgreSQL restore failed with error code ${exit_code}"
|
|
log "$msg"
|
|
send_notification "$msg"
|
|
exit "$exit_code"
|
|
fi
|
|
}
|
|
|
|
#######################################
|
|
# 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
|
|
if [ -n "${TEMP_RESTORE_DIR:-}" ] && [ -d "${TEMP_RESTORE_DIR}" ]; then
|
|
rm -rf "${TEMP_RESTORE_DIR}"
|
|
log "Removed temporary directory ${TEMP_RESTORE_DIR}"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
#######################################
|
|
# Main routine.
|
|
#######################################
|
|
main() {
|
|
case "$OPERATION_MODE" in
|
|
backup)
|
|
case "$BACKUP_MODE" in
|
|
directory)
|
|
backup_directory
|
|
;;
|
|
postgres)
|
|
backup_postgres
|
|
;;
|
|
esac
|
|
;;
|
|
restore)
|
|
case "$BACKUP_MODE" in
|
|
directory)
|
|
restore_directory
|
|
;;
|
|
postgres)
|
|
restore_postgres
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Trap termination signals to log and exit cleanly.
|
|
trap 'log "Script interrupted. Exiting."; exit 1' SIGINT SIGTERM
|
|
|
|
main
|