#!/usr/bin/env bash set -Eeuo pipefail umask 077 SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)" STORAGE_MOUNT="${MYSQL_STORAGE_MOUNT:-}" DATA_DIR="${MYSQL_DATA_DIR:-}" BACKUP_ROOT="${DATA_BACKUP_ROOT:-}" STORAGE_ID="${MYSQL_STORAGE_ID:-}" MYSQL_UID="${MYSQL_CONTAINER_UID:-999}" MYSQL_GID="${MYSQL_CONTAINER_GID:-999}" ADOPT_EXISTING="${ADOPT_EXISTING_MYSQL_DATA:-false}" fail() { echo "错误:$*" >&2 exit 2 } if [[ "${EUID}" -ne 0 ]]; then fail "请使用 sudo -E 执行,以便创建并设置持久化目录权限" fi for command_name in find findmnt install mountpoint realpath; do command -v "${command_name}" >/dev/null 2>&1 || fail "缺少命令:${command_name}" done [[ "${STORAGE_MOUNT}" == /* ]] || fail "MYSQL_STORAGE_MOUNT 必须是独立数据盘的绝对挂载点" [[ "${DATA_DIR}" == /* ]] || fail "MYSQL_DATA_DIR 必须是项目外绝对路径" [[ "${BACKUP_ROOT}" == /* ]] || fail "DATA_BACKUP_ROOT 必须是项目外绝对路径" [[ "${STORAGE_ID}" =~ ^[A-Za-z0-9._-]{16,128}$ ]] || fail "MYSQL_STORAGE_ID 必须为16–128位安全标识" [[ "${MYSQL_UID}" =~ ^[0-9]+$ && "${MYSQL_GID}" =~ ^[0-9]+$ ]] || fail "MySQL容器UID/GID必须是数字" STORAGE_MOUNT="$(realpath -e -- "${STORAGE_MOUNT}")" mountpoint -q -- "${STORAGE_MOUNT}" || fail "${STORAGE_MOUNT} 当前不是独立挂载点,拒绝写入系统盘" filesystem_type="$(findmnt -n -o FSTYPE --target "${STORAGE_MOUNT}")" mount_options="$(findmnt -n -o OPTIONS --target "${STORAGE_MOUNT}")" case "${filesystem_type}" in overlay|tmpfs|ramfs|nfs|nfs4|cifs|fuse.*) fail "MySQL运行数据不能使用 ${filesystem_type} 文件系统;请使用本地持久化块存储" ;; esac case ",${mount_options}," in *,rw,*) ;; *) fail "数据盘 ${STORAGE_MOUNT} 不是可写挂载" ;; esac DATA_DIR="$(realpath -m -- "${DATA_DIR}")" BACKUP_ROOT="$(realpath -m -- "${BACKUP_ROOT}")" case "${DATA_DIR}/" in "${STORAGE_MOUNT}/"*) ;; *) fail "MYSQL_DATA_DIR 必须位于已确认的数据盘 ${STORAGE_MOUNT} 下" ;; esac [[ "${DATA_DIR}" != "${STORAGE_MOUNT}" ]] || fail "MySQL数据目录不能直接使用磁盘挂载根目录" case "${DATA_DIR}/" in "${PROJECT_DIR}/"*) fail "MySQL数据目录不能位于代码仓库内" ;; esac case "${BACKUP_ROOT}/" in "${PROJECT_DIR}/"*) fail "备份目录不能位于代码仓库内" ;; esac if [[ "${DATA_DIR}" == "${BACKUP_ROOT}" || "${BACKUP_ROOT}/" == "${DATA_DIR}/"* || "${DATA_DIR}/" == "${BACKUP_ROOT}/"* ]]; then fail "MySQL运行数据目录和备份目录不能相互包含" fi install -d -m 0750 -o "${MYSQL_UID}" -g "${MYSQL_GID}" -- "${DATA_DIR}" backup_uid="${SUDO_UID:-0}" backup_gid="${SUDO_GID:-0}" install -d -m 0700 -o "${backup_uid}" -g "${backup_gid}" -- "${BACKUP_ROOT}" marker="${STORAGE_MOUNT}/.nianxx-mysql-storage-id" [[ ! -L "${marker}" ]] || fail "存储标识文件不能是符号链接" if [[ -f "${marker}" ]]; then actual_id="$(<"${marker}")" [[ "${actual_id}" == "${STORAGE_ID}" ]] || fail "数据盘标识不一致,拒绝覆盖或切换数据库" else existing_entry="$(find "${DATA_DIR}" -mindepth 1 -maxdepth 1 -print -quit)" if [[ -n "${existing_entry}" ]]; then [[ "${ADOPT_EXISTING}" == "true" ]] || fail "目标目录已有数据但缺少标识;确认备份后设置 ADOPT_EXISTING_MYSQL_DATA=true 再执行" [[ -f "${DATA_DIR}/auto.cnf" && -d "${DATA_DIR}/mysql" ]] || fail "现有目录不是完整的MySQL数据目录" fi temporary_marker="$(mktemp "${STORAGE_MOUNT}/.storage-id.XXXXXX")" printf '%s\n' "${STORAGE_ID}" > "${temporary_marker}" chown root:root "${temporary_marker}" chmod 0444 "${temporary_marker}" mv -n -- "${temporary_marker}" "${marker}" fi actual_id="$(<"${marker}")" [[ "${actual_id}" == "${STORAGE_ID}" ]] || fail "数据盘标识写入后校验失败" existing_entry="$(find "${DATA_DIR}" -mindepth 1 -maxdepth 1 -print -quit)" if [[ -n "${existing_entry}" && ( ! -f "${DATA_DIR}/auto.cnf" || ! -d "${DATA_DIR}/mysql" ) ]]; then fail "MySQL数据目录非空但结构不完整,拒绝启动" fi chown "${MYSQL_UID}:${MYSQL_GID}" "${DATA_DIR}" chmod 0750 "${DATA_DIR}" echo "MySQL生产存储已准备完成:" echo " 数据盘挂载点:${STORAGE_MOUNT}" echo " 文件系统:${filesystem_type} (${mount_options})" echo " MySQL数据目录:${DATA_DIR}" echo " 加密备份目录:${BACKUP_ROOT}" echo " 存储标识:${STORAGE_ID}" echo "请将以上三个路径/标识写入服务器 .env,然后再启动生产 Compose。"