#!/usr/bin/env bash set -Eeuo pipefail usage() { cat <<'EOF' Usage: backup-upload-s3.sh Environment: DEPLOY_ROOT Deployment directory. Defaults to ~/qlockify-deployment. S3_BACKUP_BUCKET S3 bucket name. S3_BACKUP_PREFIX S3 object prefix. Defaults to qlockify. S3_BACKUP_REGION S3 region. Defaults to us-east-1. S3_BACKUP_ENDPOINT_URL Optional S3-compatible endpoint URL. S3_BACKUP_ACCESS_KEY_ID S3 access key. S3_BACKUP_SECRET_ACCESS_KEY S3 secret key. BACKUP_ENCRYPTION_PASSPHRASE Passphrase used to encrypt backup archives. BACKUP_LOCAL_KEEP_LATEST Set to 1 to keep latest encrypted archive locally. EOF } log() { printf '[backup-s3] %s\n' "$*" } fail() { printf '[backup-s3] %s\n' "$*" >&2 exit 1 } require_var() { local name="$1" [[ -n "${!name:-}" ]] || fail "$name is required" } load_env() { local env_path="$1" [[ -f "$env_path" ]] || fail "Deployment env file not found: $env_path" set -a # shellcheck disable=SC1090 . "$env_path" set +a } aws_s3() { if [[ -n "${S3_BACKUP_ENDPOINT_URL:-}" ]]; then aws --endpoint-url "$S3_BACKUP_ENDPOINT_URL" s3 "$@" else aws s3 "$@" fi } normalize_prefix() { local value="${1:-qlockify}" value="${value#/}" value="${value%/}" printf '%s' "$value" } if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then usage exit 0 fi command -v aws >/dev/null 2>&1 || fail "aws CLI is required" command -v openssl >/dev/null 2>&1 || fail "openssl is required" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEPLOY_ROOT="${DEPLOY_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" DEPLOY_ENV="$DEPLOY_ROOT/.env" BACKUP_SCRIPT="$SCRIPT_DIR/backup.sh" [[ -x "$BACKUP_SCRIPT" ]] || fail "Backup script is not executable: $BACKUP_SCRIPT" load_env "$DEPLOY_ENV" S3_BACKUP_PREFIX="$(normalize_prefix "${S3_BACKUP_PREFIX:-qlockify}")" S3_BACKUP_REGION="${S3_BACKUP_REGION:-us-east-1}" require_var S3_BACKUP_BUCKET require_var S3_BACKUP_ACCESS_KEY_ID require_var S3_BACKUP_SECRET_ACCESS_KEY require_var BACKUP_ENCRYPTION_PASSPHRASE export AWS_ACCESS_KEY_ID="$S3_BACKUP_ACCESS_KEY_ID" export AWS_SECRET_ACCESS_KEY="$S3_BACKUP_SECRET_ACCESS_KEY" export AWS_DEFAULT_REGION="$S3_BACKUP_REGION" export AWS_EC2_METADATA_DISABLED=true WORK_DIR="$(mktemp -d)" trap 'rm -rf "$WORK_DIR"' EXIT PLAIN_DIR="$WORK_DIR/plain" mkdir -p "$PLAIN_DIR" log "Creating local backup archive" DEPLOY_ROOT="$DEPLOY_ROOT" "$BACKUP_SCRIPT" "$PLAIN_DIR" shopt -s nullglob archives=("$PLAIN_DIR"/*.tar.gz) shopt -u nullglob [[ "${#archives[@]}" -eq 1 ]] || fail "Expected exactly one backup archive, found ${#archives[@]}" ARCHIVE_PATH="${archives[0]}" ARCHIVE_NAME="$(basename "$ARCHIVE_PATH")" ENCRYPTED_NAME="$ARCHIVE_NAME.enc" ENCRYPTED_PATH="$WORK_DIR/$ENCRYPTED_NAME" S3_URI="s3://$S3_BACKUP_BUCKET/$S3_BACKUP_PREFIX/$ENCRYPTED_NAME" log "Encrypting backup archive" openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000 \ -in "$ARCHIVE_PATH" \ -out "$ENCRYPTED_PATH" \ -pass env:BACKUP_ENCRYPTION_PASSPHRASE rm -f "$ARCHIVE_PATH" log "Uploading encrypted backup to $S3_URI" aws_s3 cp "$ENCRYPTED_PATH" "$S3_URI" --only-show-errors if [[ "${BACKUP_LOCAL_KEEP_LATEST:-1}" == "1" ]]; then LATEST_DIR="$DEPLOY_ROOT/backups/latest" mkdir -p "$LATEST_DIR" find "$LATEST_DIR" -type f -name '*.tar.gz.enc' -delete cp "$ENCRYPTED_PATH" "$LATEST_DIR/$ENCRYPTED_NAME" log "Kept latest encrypted backup locally: $LATEST_DIR/$ENCRYPTED_NAME" fi log "S3 backup upload completed: $S3_URI"