From e9ff1874e2a9a4b8de1bc494061020a51fcaf6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E6=B5=A9?= Date: Tue, 23 Sep 2025 12:31:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(database):=20=E6=B7=BB=E5=8A=A0=E9=80=9A?= =?UTF-8?q?=E8=BF=87=20Docker=20=E5=A5=97=E6=8E=A5=E5=AD=97=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E6=95=B0=E6=8D=AE=E5=BA=93=E5=A4=87=E4=BB=BD=E7=9A=84?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增一个通用的数据库备份脚本,支持通过 Docker Engine 的 Unix 套接字对 PostgreSQL 和 MySQL/MariaDB 容器进行备份操作。该脚本可自动检测数据库类型,并支持压缩备份文件。 --- database-dump-via-docker-sock.sh | 262 +++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100755 database-dump-via-docker-sock.sh diff --git a/database-dump-via-docker-sock.sh b/database-dump-via-docker-sock.sh new file mode 100755 index 0000000..7614a26 --- /dev/null +++ b/database-dump-via-docker-sock.sh @@ -0,0 +1,262 @@ +#!/bin/sh +set -eu + +# Generic database backup triggered through the Docker Engine unix socket. +# Supports PostgreSQL and MySQL/MariaDB containers. + +log() { + printf '%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" +} + +print_usage() { + cat <&2 +Usage: $0 [--socket=PATH] [--api-version=VERSION] --container=NAME \\ + [--type=postgres|mysql] --backup-prefix=PREFIX [--backup-dir=DIR] \\ + [--mysql-user=USER] [--mysql-password=PASSWORD] + +Options: + --socket=PATH Docker Engine unix socket path (default: /var/run/docker.sock) + --api-version=VERSION Docker API version (default: v1.51) + --container=NAME Container name to execute the backup in + --type=TYPE Database type: postgres or mysql (auto-detected from container name if omitted) + --backup-dir=DIR Directory to store backups (default: /backups) + --backup-prefix=PREFIX Backup filename prefix + --mysql-user=USER MySQL user (default: root) + --mysql-password=PASSWORD MySQL password (optional) + --help Show this help message +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 + +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 + ;; + esac + + return 1 +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + printf '%s requires %s in PATH\n' "$0" "$1" >&2 + exit 1 + fi +} + +missing_value() { + printf 'Missing value for %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 + + curl -fsSL "https://Git.1-H.CC/Scripts/Linux/raw/branch/main/docker-exec-via-sock.sh" | sh -s -- \ + --socket="$DOCKER_SOCKET" \ + --api-version="$DOCKER_API_VERSION" \ + --container="$exec_container_name" \ + --cmd="$exec_cmd" +} + +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 + ;; + --help) + print_usage + exit 0 + ;; + --) + shift + break + ;; + *) + printf 'Unknown option: %s\n' "$1" >&2 + print_usage + exit 1 + ;; + esac + shift +done + +if [ "$#" -gt 0 ]; then + printf 'Unexpected positional arguments: %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 not found" + exit 1 +fi + +if [ -z "$DB_TYPE" ]; then + if detected_type=$(detect_db_type_from_name "$CONTAINER_NAME"); then + DB_TYPE=$detected_type + log "auto-detected database type '$DB_TYPE' from container name '$CONTAINER_NAME'" + else + printf 'Unable to detect database type from container name: %s\n' "$CONTAINER_NAME" >&2 + printf 'Please specify --type=postgres or --type=mysql\n' >&2 + exit 1 + fi +fi + +case $DB_TYPE in + postgres) + BACKUP_EXTENSION=".sql.zst" + DUMP_CMD="pg_dumpall --username=\"\${POSTGRES_USER:-postgres}\" --clean" + COMPRESS_CMD="zstd" + ;; + 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" + ;; + *) + printf 'Unsupported database type: %s\n' "$DB_TYPE" >&2 + exit 1 + ;; +esac + +timestamp=$(date +%Y-%m-%d_%H-%M-%S) +backup_path="$BACKUP_DIR/${BACKUP_PREFIX}${timestamp}${BACKUP_EXTENSION}" + +log "preparing $DB_TYPE backup at $backup_path via docker unix socket" + +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"; then + printf '%s\n' "$log_border" >&2 + log "backup command failed" + exit 1 +fi +printf '%s\n' "$log_border" >&2 + +printf '\n' >&2 +log "backup command finished, verifying file size inside container" + +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" | tr -d '\r') +printf '%s\n' "$log_border" >&2 + +printf '\n' >&2 +if [ -n "$size" ]; then + log "backup completed inside container: $backup_path ($size)" +else + log "backup completed, but could not determine file size inside container." +fi + +printf '\n' >&2 +log "database backup finished"