#!/usr/bin/env bash # 在线备份脚本共用函数。仅供 scripts/backup-online.sh、archive-binlog.sh 和 # restore-online.sh source,不要单独执行。 BACKUP_ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BACKUP_COMPOSE_FILE="${BACKUP_ROOT_DIR}/deploy/docker-compose.prod.yml" BACKUP_ENV_FILE="${BACKUP_ROOT_DIR}/backend/.env" BACKUP_XTRABACKUP_IMAGE="${XTRABACKUP_IMAGE:-percona/percona-xtrabackup:8.4}" backup_log() { printf '[backup] %s\n' "$*" >&2 } backup_error() { printf '[backup] ERROR: %s\n' "$*" >&2 } backup_require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then backup_error "缺少命令:$1" exit 1 fi } backup_env_value() { local key="$1" awk -F= -v key="${key}" '$1 == key { sub(/^[^=]*=/, ""); print; exit }' "${BACKUP_ENV_FILE}" } backup_require_env() { local key="$1" local value value="$(backup_env_value "${key}")" if [[ -z "${value}" ]]; then backup_error "${BACKUP_ENV_FILE} 缺少或未填写 ${key}" exit 1 fi printf '%s' "${value}" } backup_compose() { docker compose --env-file "${BACKUP_ENV_FILE}" -f "${BACKUP_COMPOSE_FILE}" "$@" } backup_mysql_container_id() { local cid cid="$(backup_compose ps -q mysql 2>/dev/null || true)" if [[ -z "${cid}" ]]; then backup_error "MySQL 容器未运行" exit 1 fi printf '%s' "${cid}" } backup_mysql_network() { local network network="$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{end}}' "$1")" if [[ -z "${network}" ]]; then backup_error "无法读取 MySQL Docker 网络" exit 1 fi printf '%s' "${network}" } backup_mysql_root_query() { backup_compose exec -T mysql sh -c \ 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=socket -N -B "$MYSQL_DATABASE" -e "$1"' \ sh "$1" } backup_validate_dir() { local dir="$1" if [[ "${dir}" != /data/* || "${dir}" == "/data" ]]; then backup_error "备份目录必须是 /data 之下的具体绝对路径,当前值:${dir}" exit 1 fi } backup_lock_or_exit() { local path="$1" local wait_seconds="${2:-${BACKUP_LOCK_WAIT_SECONDS:-0}}" backup_require_cmd flock mkdir -p "$(dirname "${path}")" exec 9>"${path}" if [[ ! "${wait_seconds}" =~ ^[0-9]+$ ]]; then backup_error "锁等待时间必须是非负整数:${wait_seconds}" exit 1 fi if ! flock -w "${wait_seconds}" 9; then backup_error "已有同类备份任务在执行,拒绝并发运行" exit 1 fi } backup_server_uuid() { local value value="$(backup_mysql_root_query 'SELECT @@server_uuid;' 2>/dev/null || true)" if [[ ! "${value}" =~ ^[0-9A-Fa-f-]{36}$ ]]; then backup_error "无法读取 MySQL server_uuid" exit 1 fi printf '%s' "${value,,}" } backup_binlog_index() { local filename="$1" if [[ "${filename}" =~ ^mysql-bin\.([0-9]{6,})$ ]]; then printf '%d' "$((10#${BASH_REMATCH[1]}))" return fi backup_error "不支持的 binlog 文件名:${filename}" exit 1 } backup_sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' return fi shasum -a 256 "$1" | awk '{print $1}' } backup_file_size() { if [[ "$(uname)" == 'Darwin' ]]; then stat -f%z "$1" else stat -c%s "$1" fi } backup_encrypt_file() { local source="$1" local destination="$2" local passphrase passphrase="$(backup_require_env BACKUP_PASSPHRASE)" BACKUP_PASSPHRASE="${passphrase}" openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -salt \ -pass env:BACKUP_PASSPHRASE \ -in "${source}" \ -out "${destination}" unset passphrase } backup_decrypt_file() { local source="$1" local destination="$2" local passphrase passphrase="$(backup_require_env BACKUP_PASSPHRASE)" BACKUP_PASSPHRASE="${passphrase}" openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 \ -pass env:BACKUP_PASSPHRASE \ -in "${source}" \ -out "${destination}" unset passphrase } backup_oss_upload() { local source="$1" local destination="$2" local kms_key_id kms_key_id="$(backup_env_value BACKUP_KMS_KEY_ID)" if [[ -n "${kms_key_id}" ]]; then ossutil cp "${source}" "${destination}" \ --encryption KMS --kms-master-key-id "${kms_key_id}" return fi # 未配置 KMS 时依赖 Bucket 已启用的 OSS 托管服务器端加密;备份载荷仍会先经过客户端加密。 ossutil cp "${source}" "${destination}" } backup_validate_oss_uri() { local uri="$1" if [[ ! "${uri}" =~ ^oss://[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then backup_error "BACKUP_OSS_URI 必须是 oss://bucket 格式,当前值:${uri}" exit 1 fi } backup_oss_download() { local source="$1" local destination="$2" ossutil cp "${source}" "${destination}" } backup_write_complete_marker() { local path="$1" local payload_name="$2" local payload_sha256="$3" local payload_size="$4" local server_uuid="$5" { printf 'format_version=1\n' printf 'created_at=%s\n' "$(date -Iseconds)" printf 'server_uuid=%s\n' "${server_uuid}" printf 'payload=%s\n' "${payload_name}" printf 'payload_sha256=%s\n' "${payload_sha256}" printf 'payload_size_bytes=%s\n' "${payload_size}" } > "${path}" }