#!/usr/bin/env bash set -Eeuo pipefail APP_DIR="${APP_DIR:-/opt/proxpanel}" SECRET_FILE="${SECRET_FILE:-$APP_DIR/.backup.env}" BACKUP_ROOT="${BACKUP_ROOT:-/opt/proxpanel-backups/daily}" usage() { cat <<'EOF' Usage: APP_DIR=/opt/proxpanel /opt/proxpanel/infra/deploy/db-backup-replicate-offsite.sh [--backup-dir ] Reads replication settings from /opt/proxpanel/.backup.env. Required keys for S3-compatible replication: OFFSITE_BACKUP_ENABLED=true OFFSITE_S3_BUCKET= OFFSITE_S3_ACCESS_KEY_ID= OFFSITE_S3_SECRET_ACCESS_KEY= Optional: OFFSITE_S3_REGION=us-east-1 OFFSITE_S3_PREFIX=proxpanel/db OFFSITE_S3_ENDPOINT_URL=https://s3.us-west-1.wasabisys.com OFFSITE_S3_SSE=AES256 OFFSITE_REPLICA_RETENTION_DAYS=30 EOF } log() { printf '[%s] %s\n' "$(date -u +'%Y-%m-%d %H:%M:%S UTC')" "$*" } die() { printf '[ERROR] %s\n' "$*" >&2 exit 1 } require_file() { [[ -f "$1" ]] || die "Missing required file: $1" } require_command() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" } find_latest_backup_dir() { find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1 } prune_remote_retention() { local bucket="$1" local prefix="$2" local endpoint_flag=() if [[ -n "${OFFSITE_S3_ENDPOINT_URL:-}" ]]; then endpoint_flag=(--endpoint-url "$OFFSITE_S3_ENDPOINT_URL") fi local days="${OFFSITE_REPLICA_RETENTION_DAYS:-}" if [[ -z "$days" || ! "$days" =~ ^[0-9]+$ || "$days" -le 0 ]]; then return fi local cutoff cutoff="$(date -u -d "-${days} days" +%Y%m%d-%H%M%S)" log "Applying offsite retention policy (${days} days; cutoff=${cutoff})" local listing prefixes old_prefix listing="$(aws "${endpoint_flag[@]}" s3 ls "s3://${bucket}/${prefix}/" || true)" prefixes="$(printf '%s\n' "$listing" | awk '/PRE [0-9]{8}-[0-9]{6}\// {print $2}' | tr -d '/')" while IFS= read -r old_prefix; do [[ -n "$old_prefix" ]] || continue if [[ "$old_prefix" < "$cutoff" ]]; then log "Pruning remote backup prefix ${old_prefix}" aws "${endpoint_flag[@]}" s3 rm "s3://${bucket}/${prefix}/${old_prefix}/" --recursive >/dev/null fi done <<<"$prefixes" } main() { local forced_backup_dir="" while [[ $# -gt 0 ]]; do case "$1" in --backup-dir) forced_backup_dir="${2:-}" shift 2 ;; -h|--help) usage exit 0 ;; *) die "Unknown argument: $1" ;; esac done require_file "$SECRET_FILE" # shellcheck disable=SC1090 source "$SECRET_FILE" if [[ "${OFFSITE_BACKUP_ENABLED:-false}" != "true" ]]; then log "Offsite replication disabled (OFFSITE_BACKUP_ENABLED != true)." exit 0 fi require_command aws [[ -n "${OFFSITE_S3_BUCKET:-}" ]] || die "OFFSITE_S3_BUCKET is required." [[ -n "${OFFSITE_S3_ACCESS_KEY_ID:-}" ]] || die "OFFSITE_S3_ACCESS_KEY_ID is required." [[ -n "${OFFSITE_S3_SECRET_ACCESS_KEY:-}" ]] || die "OFFSITE_S3_SECRET_ACCESS_KEY is required." export AWS_ACCESS_KEY_ID="$OFFSITE_S3_ACCESS_KEY_ID" export AWS_SECRET_ACCESS_KEY="$OFFSITE_S3_SECRET_ACCESS_KEY" if [[ -n "${OFFSITE_S3_SESSION_TOKEN:-}" ]]; then export AWS_SESSION_TOKEN="$OFFSITE_S3_SESSION_TOKEN" fi export AWS_DEFAULT_REGION="${OFFSITE_S3_REGION:-us-east-1}" local backup_dir if [[ -n "$forced_backup_dir" ]]; then backup_dir="$forced_backup_dir" else backup_dir="$(find_latest_backup_dir)" fi [[ -n "$backup_dir" && -d "$backup_dir" ]] || die "Unable to locate backup directory." local encrypted_file checksum_file stamp prefix endpoint_flag sse_flag destination encrypted_file="${backup_dir}/proxpanel.sql.enc" checksum_file="${encrypted_file}.sha256" require_file "$encrypted_file" require_file "$checksum_file" stamp="$(basename "$backup_dir")" prefix="${OFFSITE_S3_PREFIX:-proxpanel/db}" destination="s3://${OFFSITE_S3_BUCKET}/${prefix}/${stamp}/" endpoint_flag=() if [[ -n "${OFFSITE_S3_ENDPOINT_URL:-}" ]]; then endpoint_flag=(--endpoint-url "$OFFSITE_S3_ENDPOINT_URL") fi sse_flag=() if [[ -n "${OFFSITE_S3_SSE:-}" ]]; then sse_flag=(--sse "$OFFSITE_S3_SSE") fi log "Replicating encrypted backup to ${destination}" aws "${endpoint_flag[@]}" s3 cp "$encrypted_file" "${destination}proxpanel.sql.enc" "${sse_flag[@]}" --only-show-errors aws "${endpoint_flag[@]}" s3 cp "$checksum_file" "${destination}proxpanel.sql.enc.sha256" "${sse_flag[@]}" --only-show-errors log "Verifying offsite object presence" aws "${endpoint_flag[@]}" s3 ls "${destination}proxpanel.sql.enc" >/dev/null aws "${endpoint_flag[@]}" s3 ls "${destination}proxpanel.sql.enc.sha256" >/dev/null prune_remote_retention "$OFFSITE_S3_BUCKET" "$prefix" log "Offsite replication completed successfully." } main "$@"