Files
2026-07-13 12:30:54 +08:00

117 lines
3.3 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml"
if [ ! -f "$COMPOSE_FILE" ]; then
COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
fi
BACKUP_DIR="${BACKUP_DIR:-${SCRIPT_DIR}/backups}"
BACKUP_PREFIX="${POSTGRES_BACKUP_PREFIX:-tianji-postgres}"
KEEP_LOCAL_COPY="${KEEP_LOCAL_COPY:-false}"
KEEP_PARTIAL_DUMP="${KEEP_PARTIAL_DUMP:-false}"
R2_REGION="${R2_REGION:-auto}"
R2_PREFIX="${R2_PREFIX:-tianji}"
R2_ENDPOINT="${R2_ENDPOINT:-}"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2
}
abort() {
log "ERROR: $*"
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || abort "Command '$1' is required but not found in PATH."
}
require_env() {
local var_name="$1"
local value="${!var_name:-}"
[ -n "$value" ] || abort "Environment variable '$var_name' is required."
}
[ -f "$COMPOSE_FILE" ] || abort "docker-compose.yml not found next to the script or one directory above."
require_command docker
DOCKER_COMPOSE_CMD="docker compose"
if ! docker compose version >/dev/null 2>&1; then
if command -v docker-compose >/dev/null 2>&1; then
DOCKER_COMPOSE_CMD="docker-compose"
else
abort "Neither 'docker compose' nor 'docker-compose' command is available."
fi
fi
if ! $DOCKER_COMPOSE_CMD -f "$COMPOSE_FILE" config --services | grep -qx 'postgres'; then
abort "Service 'postgres' not found in docker-compose.yml."
fi
POSTGRES_CONTAINER_ID="$($DOCKER_COMPOSE_CMD -f "$COMPOSE_FILE" ps -q postgres || true)"
if [ -z "$POSTGRES_CONTAINER_ID" ]; then
log "Postgres container is not running. Bringing it up..."
$DOCKER_COMPOSE_CMD -f "$COMPOSE_FILE" up -d postgres >/dev/null
POSTGRES_CONTAINER_ID="$($DOCKER_COMPOSE_CMD -f "$COMPOSE_FILE" ps -q postgres || true)"
[ -n "$POSTGRES_CONTAINER_ID" ] || abort "Failed to start postgres service."
fi
mkdir -p "$BACKUP_DIR"
TIMESTAMP="$(date '+%Y%m%d_%H%M%S')"
BACKUP_FILENAME="${BACKUP_PREFIX}_${TIMESTAMP}.dump"
LOCAL_BACKUP_PATH="${BACKUP_DIR}/${BACKUP_FILENAME}"
log "Creating Postgres dump at ${LOCAL_BACKUP_PATH}"
cleanup_partial_dump() {
if [ -f "$LOCAL_BACKUP_PATH" ] && [ "$KEEP_PARTIAL_DUMP" != "true" ]; then
rm -f "$LOCAL_BACKUP_PATH"
fi
}
trap cleanup_partial_dump EXIT
$DOCKER_COMPOSE_CMD -f "$COMPOSE_FILE" exec -T postgres bash -c 'set -euo pipefail; export PGPASSWORD="${POSTGRES_PASSWORD}"; pg_dump --format=custom --no-owner --no-acl -U "${POSTGRES_USER}" "${POSTGRES_DB:-${POSTGRES_USER}}"' >"$LOCAL_BACKUP_PATH"
trap - EXIT
require_command aws
require_env R2_BUCKET
require_env R2_ACCOUNT_ID
require_env R2_ACCESS_KEY_ID
require_env R2_SECRET_ACCESS_KEY
R2_ENDPOINT="${R2_ENDPOINT:-https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com}"
REMOTE_KEY="$BACKUP_FILENAME"
if [ -n "$R2_PREFIX" ]; then
R2_PREFIX="${R2_PREFIX%/}"
REMOTE_KEY="${R2_PREFIX}/${REMOTE_KEY}"
fi
REMOTE_URI="s3://${R2_BUCKET%/}/${REMOTE_KEY}"
log "Uploading backup to ${REMOTE_URI} via R2"
AWS_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID" \
AWS_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY" \
AWS_DEFAULT_REGION="$R2_REGION" \
aws s3 cp "$LOCAL_BACKUP_PATH" "$REMOTE_URI" --endpoint-url "$R2_ENDPOINT"
if [ "$KEEP_LOCAL_COPY" != "true" ]; then
log "Removing local backup ${LOCAL_BACKUP_PATH}"
rm -f "$LOCAL_BACKUP_PATH"
else
log "Keeping local backup ${LOCAL_BACKUP_PATH}"
fi
log "Backup completed successfully"