110 lines
4.5 KiB
Bash
Executable File
110 lines
4.5 KiB
Bash
Executable File
#!/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。"
|