#!/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 "$@"