258 lines
6.7 KiB
Bash
258 lines
6.7 KiB
Bash
#!/bin/bash
|
|
#
|
|
# Backup script for example service
|
|
# Stops the service, creates a backup, uploads to remote storage, and restarts the service
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
# shellcheck source=example_server-env
|
|
. "${HOME}"/"${USER}"-env
|
|
|
|
#======================================
|
|
# Constants
|
|
#======================================
|
|
readonly SERVICE_NAME="Example"
|
|
readonly PRIORITY_INFO=2
|
|
readonly PRIORITY_WARNING=3
|
|
readonly PRIORITY_ERROR=4
|
|
readonly EXIT_SUCCESS=0
|
|
readonly EXIT_VALIDATION_ERROR=1
|
|
readonly EXIT_DOCKER_ERROR=2
|
|
readonly EXIT_BACKUP_ERROR=3
|
|
readonly EXIT_UPLOAD_ERROR=4
|
|
|
|
#======================================
|
|
# Global Variables
|
|
#======================================
|
|
backupDir="/tmp/${USER}-backup"
|
|
composeFile="${HOME}/${USER}-compose.yaml"
|
|
logFile="${HOME}/backup_logs/$(date +%y_%m).log"
|
|
serviceRestartFailed=false
|
|
|
|
#======================================
|
|
# Helper Functions
|
|
#======================================
|
|
|
|
# Log a message with timestamp
|
|
# Output to stderr to avoid buffering issues with stdout
|
|
log() {
|
|
local level="$1"
|
|
shift
|
|
local message="$*"
|
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - [${level}] ${message}" >&2
|
|
}
|
|
|
|
# Log info message
|
|
log_info() {
|
|
log "INFO" "$@"
|
|
}
|
|
|
|
# Log warning message
|
|
log_warn() {
|
|
log "WARN" "$@"
|
|
}
|
|
|
|
# Log error message
|
|
log_error() {
|
|
log "ERROR" "$@"
|
|
}
|
|
|
|
# Cleanup function - ensures temporary files are removed
|
|
# shellcheck disable=SC2329
|
|
cleanup() {
|
|
local exit_code=$?
|
|
|
|
if [[ -d "${backupDir}" ]]; then
|
|
log_info "Cleaning up backup directory"
|
|
rm -rf "${backupDir}"
|
|
fi
|
|
|
|
if [[ ${exit_code} -ne 0 ]]; then
|
|
log_error "Backup failed with exit code ${exit_code}"
|
|
fi
|
|
}
|
|
|
|
# Set trap to ensure cleanup on exit
|
|
trap cleanup EXIT
|
|
|
|
# Send ntfy notification via curl
|
|
# Arguments: priority, tags, message
|
|
send_notification() {
|
|
local priority="$1"
|
|
local tags="$2"
|
|
local message="$3"
|
|
|
|
if ! curl -Ss \
|
|
-H "Title: ${SERVICE_NAME}" \
|
|
-H "Priority: ${priority}" \
|
|
-H "Tags: ${tags}" \
|
|
-d "${message}" \
|
|
"${NOTIF_URL}"; then
|
|
log_warn "Failed to send notification"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Validate required environment variables
|
|
validate_environment() {
|
|
log_info "Validating environment variables"
|
|
|
|
: "${USER:?USER environment variable is required}"
|
|
: "${VOLUME_PATH:?VOLUME_PATH environment variable is required}"
|
|
: "${BUCKET_PATH:?BUCKET_PATH environment variable is required}"
|
|
: "${NOTIF_URL:?NOTIF_URL environment variable is required}"
|
|
}
|
|
|
|
# Validate required dependencies and paths
|
|
validate_dependencies() {
|
|
log_info "Validating dependencies and paths"
|
|
|
|
# Check for rclone
|
|
if ! command -v rclone &>/dev/null; then
|
|
log_error "rclone command not found"
|
|
send_notification "${PRIORITY_ERROR}" "x,backup" "Backup failed: rclone not found"
|
|
return "${EXIT_VALIDATION_ERROR}"
|
|
fi
|
|
|
|
# Check docker compose file
|
|
if [[ ! -f "${composeFile}" ]]; then
|
|
log_error "Docker compose file not found: ${composeFile}"
|
|
send_notification "${PRIORITY_ERROR}" "x,backup" "Backup failed: compose file not found"
|
|
return "${EXIT_VALIDATION_ERROR}"
|
|
fi
|
|
|
|
# Check volume path
|
|
if [[ ! -d "${VOLUME_PATH}" ]]; then
|
|
log_error "Volume path not found: ${VOLUME_PATH}"
|
|
send_notification "${PRIORITY_ERROR}" "x,backup" "Backup failed: volume path not found"
|
|
return "${EXIT_VALIDATION_ERROR}"
|
|
fi
|
|
|
|
return "${EXIT_SUCCESS}"
|
|
}
|
|
|
|
# Stop docker compose services
|
|
stop_services() {
|
|
log_info "Stopping docker compose services"
|
|
|
|
if ! sudo docker compose -f "${composeFile}" stop; then
|
|
log_error "Failed to stop docker compose services"
|
|
send_notification "${PRIORITY_ERROR}" "x,backup" "Backup failed: could not stop services"
|
|
return "${EXIT_DOCKER_ERROR}"
|
|
fi
|
|
|
|
return "${EXIT_SUCCESS}"
|
|
}
|
|
|
|
# Start docker compose services
|
|
start_services() {
|
|
log_info "Starting docker compose services"
|
|
|
|
if ! sudo docker compose -f "${composeFile}" start; then
|
|
log_warn "Failed to start docker compose services"
|
|
serviceRestartFailed=true
|
|
return 1
|
|
fi
|
|
|
|
return "${EXIT_SUCCESS}"
|
|
}
|
|
|
|
# Create backup of volume data
|
|
create_backup() {
|
|
log_info "Creating backup directory"
|
|
mkdir -p "${backupDir}"
|
|
|
|
log_info "Copying files from ${VOLUME_PATH}"
|
|
if ! cp -pr "${VOLUME_PATH}"/* "${backupDir}"/; then
|
|
log_error "Failed to copy files to backup directory"
|
|
return "${EXIT_BACKUP_ERROR}"
|
|
fi
|
|
|
|
# Verify backup has content
|
|
if [[ -z "$(ls -A "${backupDir}")" ]]; then
|
|
log_error "Backup directory is empty - no files to backup"
|
|
return "${EXIT_BACKUP_ERROR}"
|
|
fi
|
|
|
|
log_info "Backup created successfully"
|
|
return "${EXIT_SUCCESS}"
|
|
}
|
|
|
|
# Upload backup to remote storage
|
|
upload_backup() {
|
|
local backup_size
|
|
|
|
log_info "Uploading backup to ${BUCKET_PATH}"
|
|
|
|
if ! rclone copy "${backupDir}" "${BUCKET_PATH}" -v --retries 3; then
|
|
log_error "Failed to upload backup to remote storage"
|
|
return "${EXIT_UPLOAD_ERROR}"
|
|
fi
|
|
|
|
# Calculate and report backup size
|
|
backup_size=$(du -sh "${backupDir}" | cut -f1)
|
|
log_info "Backup uploaded successfully (${backup_size})"
|
|
|
|
echo "${backup_size}"
|
|
return "${EXIT_SUCCESS}"
|
|
}
|
|
|
|
#======================================
|
|
# Main Execution
|
|
#======================================
|
|
main() {
|
|
log_info "Starting ${SERVICE_NAME} backup"
|
|
|
|
# Validate environment
|
|
validate_environment || exit "${EXIT_VALIDATION_ERROR}"
|
|
|
|
# Validate dependencies and paths
|
|
validate_dependencies || exit "$?"
|
|
|
|
# Stop services to ensure data consistency
|
|
stop_services || exit "$?"
|
|
|
|
# Create backup
|
|
if ! create_backup; then
|
|
start_services # Attempt to restart services even if backup failed
|
|
send_notification "${PRIORITY_ERROR}" "x,backup" "Backup failed: could not copy files"
|
|
exit "${EXIT_BACKUP_ERROR}"
|
|
fi
|
|
|
|
# Restart services immediately to minimize downtime
|
|
start_services
|
|
|
|
# Upload backup to remote storage
|
|
local backup_size
|
|
if ! backup_size=$(upload_backup); then
|
|
if [[ "${serviceRestartFailed}" == "true" ]]; then
|
|
send_notification "${PRIORITY_ERROR}" "x,backup" "Backup upload failed AND services failed to restart"
|
|
else
|
|
send_notification "${PRIORITY_WARNING}" "warning,backup" "Backup not completed: upload failed"
|
|
fi
|
|
exit "${EXIT_UPLOAD_ERROR}"
|
|
fi
|
|
|
|
# Send success notification
|
|
if [[ "${serviceRestartFailed}" == "true" ]]; then
|
|
send_notification "${PRIORITY_WARNING}" "warning,backup" "Backup completed (${backup_size}) but services failed to restart"
|
|
log_warn "Backup completed but services failed to restart"
|
|
exit "${EXIT_DOCKER_ERROR}"
|
|
else
|
|
send_notification "${PRIORITY_INFO}" "heavy_check_mark,backup" "Backup completed (${backup_size})"
|
|
log_info "Backup completed successfully"
|
|
exit "${EXIT_SUCCESS}"
|
|
fi
|
|
}
|
|
|
|
# Setup logging directory
|
|
mkdir -p "${HOME}"/backup_logs
|
|
|
|
# Execute main function with output redirected to log file
|
|
{
|
|
main
|
|
} &>>"${logFile}"
|