312 lines
7.7 KiB
Bash
Executable File
312 lines
7.7 KiB
Bash
Executable File
#!/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 <<EOF_USAGE >&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
|