#!/bin/sh set -eu # 通过 Docker Engine unix socket 触发的通用数据库备份。 # 支持 PostgreSQL, MySQL/MariaDB 和 Kingbase 容器。 log() { printf '%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" } print_usage() { cat <&2 用法: $0 [--socket=路径] [--api-version=版本] --container=名称 \\ [--type=postgres|mysql|kingbase] --backup-prefix=前缀 [--backup-dir=目录] \\ [--mysql-user=用户] [--mysql-password=密码] [--exec-user=名称] 选项: --socket=PATH Docker Engine unix socket 路径 (默认: /var/run/docker.sock) --api-version=VERSION Docker API 版本 (默认: v1.51) --container=NAME 执行备份的容器名称 --type=TYPE 数据库类型: postgres, mysql, 或 kingbase (如果省略,则从容器名称自动检测) --backup-dir=DIR 存储备份的目录 (默认: /backups) --backup-prefix=PREFIX 备份文件名前缀 --mysql-user=USER MySQL 用户 (默认: root) --mysql-password=PASSWORD MySQL 密码 (可选) --exec-user=NAME 在容器内运行命令时强制使用特定用户 --help 显示此帮助信息 EOF_USAGE } DOCKER_SOCKET="/var/run/docker.sock" DOCKER_API_VERSION="v1.51" CONTAINER_NAME="" DB_TYPE="" BACKUP_DIR="/backups" BACKUP_PREFIX="" MYSQL_USER="root" MYSQL_PASSWORD="" MYSQL_PASSWORD_SET=0 EXEC_USER_OVERRIDE="" EXEC_SHELL="sh" detect_db_type_from_name() { name_lower=$(printf '%s' "$1" | tr 'A-Z' 'a-z') case $name_lower in *postgres*|*pgsql*|*pg-*) printf 'postgres' return 0 ;; *pg*) printf 'postgres' return 0 ;; *mysql*|*mariadb*|*maria*) printf 'mysql' return 0 ;; *kingbase*) printf 'kingbase' return 0 ;; esac return 1 } require_command() { if ! command -v "$1" >/dev/null 2>&1; then printf '%s 需要 PATH 中包含 %s\n' "$0" "$1" >&2 exit 1 fi } missing_value() { printf '缺少 %s 的值\n' "$1" >&2 print_usage exit 1 } escape_for_double_quotes() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/`/\\`/g; s/\$/\\$/g' } docker_exec() { exec_container_name=$1 exec_cmd=$2 exec_user=${3:-} set -- \ --socket="$DOCKER_SOCKET" \ --api-version="$DOCKER_API_VERSION" \ --container="$exec_container_name" \ --cmd="$exec_cmd" if [ -n "$exec_user" ]; then set -- "$@" --user="$exec_user" fi if [ -n "${EXEC_SHELL:-}" ]; then set -- "$@" --shell="$EXEC_SHELL" fi curl -fsSL "https://Git.1-H.CC/Scripts/Linux/raw/branch/main/docker-exec-via-sock.sh" | sh -s -- "$@" } while [ "$#" -gt 0 ]; do case $1 in --socket=*) DOCKER_SOCKET="${1#*=}" ;; --socket) if [ "$#" -lt 2 ]; then missing_value '--socket'; fi shift DOCKER_SOCKET="$1" ;; --api-version=*) DOCKER_API_VERSION="${1#*=}" ;; --api-version) if [ "$#" -lt 2 ]; then missing_value '--api-version'; fi shift DOCKER_API_VERSION="$1" ;; --container=*) CONTAINER_NAME="${1#*=}" ;; --container) if [ "$#" -lt 2 ]; then missing_value '--container'; fi shift CONTAINER_NAME="$1" ;; --type=*) DB_TYPE="${1#*=}" ;; --type) if [ "$#" -lt 2 ]; then missing_value '--type'; fi shift DB_TYPE="$1" ;; --backup-dir=*) BACKUP_DIR="${1#*=}" ;; --backup-dir) if [ "$#" -lt 2 ]; then missing_value '--backup-dir'; fi shift BACKUP_DIR="$1" ;; --backup-prefix=*) BACKUP_PREFIX="${1#*=}" ;; --backup-prefix) if [ "$#" -lt 2 ]; then missing_value '--backup-prefix'; fi shift BACKUP_PREFIX="$1" ;; --mysql-user=*) MYSQL_USER="${1#*=}" ;; --mysql-user) if [ "$#" -lt 2 ]; then missing_value '--mysql-user'; fi shift MYSQL_USER="$1" ;; --mysql-password=*) MYSQL_PASSWORD="${1#*=}" MYSQL_PASSWORD_SET=1 ;; --mysql-password) if [ "$#" -lt 2 ]; then missing_value '--mysql-password'; fi shift MYSQL_PASSWORD="$1" MYSQL_PASSWORD_SET=1 ;; --exec-user=*) EXEC_USER_OVERRIDE="${1#*=}" ;; --exec-user) if [ "$#" -lt 2 ]; then missing_value '--exec-user'; fi shift EXEC_USER_OVERRIDE="$1" ;; --help) print_usage exit 0 ;; --) shift break ;; *) printf '未知选项: %s\n' "$1" >&2 print_usage exit 1 ;; esac shift done if [ "$#" -gt 0 ]; then printf '意外的位置参数: %s\n' "$*" >&2 print_usage exit 1 fi require_command curl require_command jq require_command sed require_command tr if [ -z "$DOCKER_SOCKET" ] || [ -z "$DOCKER_API_VERSION" ] || [ -z "$CONTAINER_NAME" ] || [ -z "$BACKUP_DIR" ] || [ -z "$BACKUP_PREFIX" ]; then print_usage exit 1 fi if [ ! -S "$DOCKER_SOCKET" ]; then log "未找到 docker socket $DOCKER_SOCKET" exit 1 fi if [ -z "$DB_TYPE" ]; then if detected_type=$(detect_db_type_from_name "$CONTAINER_NAME"); then DB_TYPE=$detected_type log "从容器名称 '$CONTAINER_NAME' 自动检测到数据库类型 '$DB_TYPE'" else printf '无法从容器名称检测数据库类型: %s\n' "$CONTAINER_NAME" >&2 printf '请指定 --type=postgres, --type=mysql 或 --type=kingbase\n' >&2 exit 1 fi fi EXEC_USER_DEFAULT="" case $DB_TYPE in postgres) BACKUP_EXTENSION=".sql.zst" DUMP_CMD="pg_dumpall --username=\"\${POSTGRES_USER:-postgres}\" --clean" COMPRESS_CMD="zstd" EXEC_SHELL="bash" ;; mysql) BACKUP_EXTENSION=".sql.zst" mysql_user_escaped=$(escape_for_double_quotes "$MYSQL_USER") DUMP_CMD="mysqldump --all-databases --user=\"${mysql_user_escaped}\"" if [ "$MYSQL_PASSWORD_SET" -eq 1 ]; then mysql_password_escaped=$(escape_for_double_quotes "$MYSQL_PASSWORD") DUMP_CMD="MYSQL_PWD=\"${mysql_password_escaped}\" $DUMP_CMD" fi COMPRESS_CMD="zstd" EXEC_SHELL="sh" ;; kingbase) BACKUP_EXTENSION=".sql.zst" DUMP_CMD="sys_dumpall --clean --username=\"\${DB_USER:-kingbase}\"" COMPRESS_CMD="zstd" EXEC_USER_DEFAULT="root" EXEC_SHELL="sh" ;; *) printf '不支持的数据库类型: %s\n' "$DB_TYPE" >&2 exit 1 ;; esac if [ -n "$EXEC_USER_OVERRIDE" ]; then EXEC_USER="$EXEC_USER_OVERRIDE" else EXEC_USER="$EXEC_USER_DEFAULT" fi timestamp=$(date +%Y-%m-%d_%H-%M-%S) backup_path="$BACKUP_DIR/${BACKUP_PREFIX}${timestamp}${BACKUP_EXTENSION}" log "正在通过 docker unix socket 准备 $DB_TYPE 备份到 $backup_path" cmd="mkdir -p \"${BACKUP_DIR}\" && set -o pipefail && ${DUMP_CMD} | ${COMPRESS_CMD} > \"${backup_path}\"" log_border="----------------------------------------------------------------------" printf '%s\n' "$log_border" >&2 if ! docker_exec "$CONTAINER_NAME" "$cmd" "${EXEC_USER:-}"; then printf '%s\n' "$log_border" >&2 log "备份命令失败" exit 1 fi printf '%s\n' "$log_border" >&2 printf '\n' >&2 log "备份命令完成,正在验证容器内的文件大小" verify_cmd="du -h \"${backup_path}\" 2>/dev/null | awk 'NR==1{print \$1}'" printf '%s\n' "$log_border" >&2 size=$(docker_exec "$CONTAINER_NAME" "$verify_cmd" "${EXEC_USER:-}" | tr -d '\r') printf '%s\n' "$log_border" >&2 printf '\n' >&2 if [ -n "$size" ]; then log "容器内备份完成: $backup_path ($size)" else log "备份完成,但无法确定容器内的文件大小。" fi printf '\n' >&2 log "数据库备份完成" log "正在调用远程清理脚本,前缀为 '$BACKUP_PREFIX',目录为 $BACKUP_DIR" if ! curl -fsSL "https://Git.1-H.CC/Scripts/Linux/raw/branch/main/database-dump-cleanup.sh" | \ sh -s -- --prefix="$BACKUP_PREFIX" --dir="$BACKUP_DIR"; then log "清理脚本失败" fi