fix: stabilize external-db deployment and updates
Release / release (push) Canceled after 0s

This commit is contained in:
Qiufeng
2026-08-18 22:51:35 +08:00
parent 227c4b0129
commit 8c6c10eb85
22 changed files with 767 additions and 279 deletions
+68 -13
View File
@@ -15,12 +15,47 @@ INIT_ARMED=false
INIT_COMMITTED=false
INIT_RELEASE_ROOT=
INIT_APP_ROOT=
LOG_LOCALE=${KAIDI_LOG_LOCALE:-zh-CN}
SERVICE_USER_CREATED=false
SERVICE_GROUP_CREATED=false
CREATED_PATHS=()
log() { printf '[kaidi-baota-init] %s\n' "$*"; }
die() { printf '[kaidi-baota-init] ERROR: %s\n' "$*" >&2; exit 1; }
localize_message() {
local message=$1
[ "$LOG_LOCALE" = en ] && { printf '%s' "$message"; return; }
case "$message" in
"Run with sudo or as root") printf '请使用 sudo 或 root 运行' ;;
"Baota initialization only supports Linux") printf '宝塔初始化仅支持 Linux' ;;
"Extract the release"*) printf '请先将发布包解压到 APP_ROOT/releases/VERSION' ;;
"The supported Baota project root"*) printf '宝塔项目根目录必须是 /www/wwwroot/kaidi' ;;
"The extracted release is incomplete") printf '解压后的发布包不完整' ;;
"The extracted operations scripts are incomplete") printf '发布包中的运维脚本不完整' ;;
"The release public key is missing") printf '缺少发布公钥' ;;
"The release public-key fingerprint is invalid") printf '发布公钥指纹不匹配' ;;
"Existing user "*" has unexpected home directory "*) printf '现有用户目录不符合 Kaidi 约定:%s' "${message#Existing user }" ;;
"Existing user "*" is not a member"*) printf '现有 Kaidi 用户不属于服务用户组' ;;
"A nologin shell is required") printf '服务用户必须使用 nologin shell' ;;
*" contains a line break") printf '配置项包含换行符,已拒绝:%s' "${message%% contains a line break*}" ;;
"find is required"|"openssl is required"|"sha256sum is required") printf '缺少初始化依赖命令:%s' "${message%% is required}" ;;
"Kaidi is already initialized"*) printf 'Kaidi 已初始化,请先执行卸载流程再重新安装' ;;
"The old kaidi-finance.service"*) printf '检测到旧 kaidi-finance.service,请先执行卸载流程' ;;
"Old Kaidi update units"*) printf '检测到旧 Kaidi 更新单元,请先执行卸载流程' ;;
"The Baota current release link"*) printf '宝塔 current 发布链接已存在,请先执行卸载流程' ;;
"Port must be an integer"*) printf '端口必须是 1024 到 65535 的整数' ;;
"Initialization failed"*) printf '初始化失败,本次创建的文件已回滚,可以修正原因后重试' ;;
"Manual deployment is initialized"*) printf '宝塔手动部署初始化完成:%s' "${message#Manual deployment is initialized for Kaidi Finance }" ;;
"Create the Baota Spring Boot project"*) printf '请在宝塔创建 Spring Boot 项目,项目路径:%s' "${message#Create the Baota Spring Boot project with }" ;;
"Baota environment variables: leave empty") printf '宝塔环境变量请全部留空' ;;
"Setup code: "*) printf '安装码位置:%s' "${message#Setup code: }" ;;
*) printf '%s' "$message" ;;
esac
}
log() { printf '[kaidi-baota-init] %s\n' "$(localize_message "$*")"; }
die() { printf '[kaidi-baota-init] 错误:%s\n' "$(localize_message "$*")" >&2; exit 1; }
rollback_initialization() {
local rc=$?
local rc=$? index path service_uid
trap - EXIT HUP INT TERM
if [ "$rc" -ne 0 ] && [ "$INIT_ARMED" = true ] && [ "$INIT_COMMITTED" != true ]; then
systemctl disable --now kaidi-update.path >/dev/null 2>&1 || true
@@ -35,6 +70,18 @@ rollback_initialization() {
&& [ "$(readlink "$INIT_APP_ROOT/current" 2>/dev/null || true)" = "$INIT_RELEASE_ROOT" ]; then
rm -f "$INIT_APP_ROOT/current"
fi
rm -f "$CONFIG_ROOT"/*.next.* "$UPDATE_STATE_ROOT"/*.next.* "$STATE_ROOT/setup"/*.next.*
if [ "$SERVICE_USER_CREATED" = true ]; then
service_uid=$(id -u "$SERVICE_USER" 2>/dev/null || true)
[ -z "$service_uid" ] || userdel --force "$SERVICE_USER" >/dev/null 2>&1 || true
fi
if [ "$SERVICE_GROUP_CREATED" = true ]; then
groupdel "$SERVICE_GROUP" >/dev/null 2>&1 || true
fi
for ((index=${#CREATED_PATHS[@]} - 1; index >= 0; index--)); do
path=${CREATED_PATHS[$index]}
rmdir -- "$path" >/dev/null 2>&1 || true
done
log "Initialization failed; files created by this attempt were rolled back and the command can be retried"
fi
exit "$rc"
@@ -48,6 +95,10 @@ sha256_file() {
sha256sum "$1" | awk '{print $1}'
}
record_path() {
[ -e "$1" ] || [ -L "$1" ] || CREATED_PATHS+=("$1")
}
random_secret() {
openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-36
}
@@ -73,6 +124,7 @@ ensure_service_identity() {
local existing_home nologin_path
if ! getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
groupadd --system "$SERVICE_GROUP"
SERVICE_GROUP_CREATED=true
fi
if id "$SERVICE_USER" >/dev/null 2>&1; then
existing_home=$(getent passwd "$SERVICE_USER" | awk -F: '{print $6}')
@@ -86,14 +138,13 @@ ensure_service_identity() {
[ -n "$nologin_path" ] || nologin_path=/usr/sbin/nologin
[ -x "$nologin_path" ] || die "A nologin shell is required"
useradd --system --gid "$SERVICE_GROUP" --home-dir "$STATE_ROOT" --shell "$nologin_path" "$SERVICE_USER"
SERVICE_USER_CREATED=true
}
main() {
local script_dir release_root releases_root app_root version app_port field_key setup_code setup_hash
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
[ "$(uname -s)" = Linux ] || die "Baota initialization only supports Linux"
command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ] \
|| die "systemd is required for the privileged background updater"
for command in find openssl sha256sum; do
command -v "$command" >/dev/null 2>&1 || die "$command is required"
done
@@ -122,7 +173,7 @@ main() {
[ ! -e "$CONFIG_ROOT/kaidi.env" ] && [ ! -e "$CONFIG_ROOT/update.env" ] \
&& [ ! -e "$STATE_ROOT/setup/locked" ] \
|| die "Kaidi is already initialized; run the published purge workflow before a fresh installation"
! systemctl cat kaidi-finance.service >/dev/null 2>&1 \
[ ! -e /etc/systemd/system/kaidi-finance.service ] \
|| die "The old kaidi-finance.service still exists; run the published purge workflow first"
[ ! -e /etc/systemd/system/kaidi-update.service ] \
&& [ ! -e /etc/systemd/system/kaidi-update.path ] \
@@ -136,6 +187,10 @@ main() {
INIT_ARMED=true
ensure_service_identity
for path in "$app_root" "$releases_root" "$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" \
"$LOG_ROOT" "$STATE_ROOT/setup" "$UPDATE_STATE_ROOT" "$UPDATE_STATE_ROOT/inbox" "$CONFIG_ROOT"; do
record_path "$path"
done
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$app_root" "$releases_root"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \
"$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" "$LOG_ROOT"
@@ -193,16 +248,16 @@ main() {
# panel lifecycle can participate in the transaction state machine.
install -m 0644 "$release_root/ops/release-public.pem" "$CONFIG_ROOT/release-public.pem"
printf '{"state":"CURRENT","message":"Baota release is prepared","targetVersion":"%s","updatedAt":"%s"}\n' \
printf '{"state":"CURRENT","message":"宝塔发布已准备","targetVersion":"%s","updatedAt":"%s"}\n' \
"$version" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$UPDATE_STATE_ROOT/status.json"
chmod 0644 "$UPDATE_STATE_ROOT/status.json"
cat > /root/kaidi-first-login.txt <<EOF
Project path: $app_root/current
Start command: $app_root/current/ops/baota-start.sh
Reverse proxy target: http://127.0.0.1:$app_port
Setup URL: /setup
Setup code: $setup_code
Version: $version
项目路径:$app_root/current
启动命令:$app_root/current/ops/baota-start.sh
反向代理目标:http://127.0.0.1:$app_port
安装向导地址:/setup
安装码:$setup_code
版本:$version
EOF
chmod 0600 /root/kaidi-first-login.txt
+29 -1
View File
@@ -2,9 +2,37 @@
set -Eeuo pipefail
umask 027
LOG_LOCALE=${KAIDI_LOG_LOCALE:-zh-CN}
localize_message() {
local message=$1
[ "$LOG_LOCALE" = en ] && { printf '%s' "$message"; return; }
case "$message" in
"Configuration file is not a regular file: "*) printf '配置文件不是普通文件:%s' "${message#Configuration file is not a regular file: }" ;;
"Startup user cannot read configuration file: "*) printf '启动用户无法读取配置文件:%s' "${message#Startup user cannot read configuration file: }" ;;
"Configuration file is too large: "*) printf '配置文件过大:%s' "${message#Configuration file is too large: }" ;;
"Configuration file contains an invalid line: "*) printf '配置文件包含无效行:%s' "${message#Configuration file contains an invalid line: }" ;;
"app.jar is missing from "*) printf '缺少 app.jar:%s' "${message#app.jar is missing from }" ;;
"public/index.html is missing from "*) printf '缺少前端入口 public/index.html:%s' "${message#public/index.html is missing from }" ;;
"VERSION is missing from "*) printf '缺少 VERSION:%s' "${message#VERSION is missing from }" ;;
"Java 17 or newer was not found") printf '未找到 Java 17 或更高版本' ;;
"Java executable is not available at "*) printf 'Java 可执行文件不可用:%s' "${message#Java executable is not available at }" ;;
"Java executable "*" is not available") printf 'Java 可执行文件不可用' ;;
"Java 17 or newer is required") printf '需要 Java 17 或更高版本' ;;
*" is missing from the protected Kaidi configuration") printf '受保护的 Kaidi 配置缺少:%s' "${message% is missing from the protected Kaidi configuration}" ;;
"Storage directory "*" does not exist") printf '存储目录不存在:%s' "${message#Storage directory }" ;;
"Startup user cannot write storage directory "*) printf '启动用户无法写入存储目录:%s' "${message#Startup user cannot write storage directory }" ;;
"PID directory does not exist") printf 'PID 目录不存在' ;;
"Startup user cannot write the PID directory") printf '启动用户无法写入 PID 目录' ;;
"KAIDI_PID_FILE must be an absolute path") printf 'KAIDI_PID_FILE 必须是绝对路径' ;;
"PID file must not be a symbolic link") printf 'PID 文件不能是符号链接' ;;
"Process start time could not be read"*) printf '无法读取进程启动时间' ;;
*) printf '%s' "$message" ;;
esac
}
die() {
printf '[kaidi-baota] ERROR: %s\n' "$1" >&2
printf '[kaidi-baota] 错误:%s\n' "$(localize_message "$1")" >&2
exit 1
}
+3
View File
@@ -1,5 +1,8 @@
services:
mysql:
# Local-development fixture only. Production installers never invoke
# Compose and never create this service.
profiles: [local-db]
image: mysql:8.4
container_name: kaidi-finance-mysql
restart: unless-stopped
+1 -1
View File
@@ -15,7 +15,7 @@ FILE_SCANNER_ENABLED=true
FINANCE_BOOTSTRAP_ENABLED=false
FINANCE_BOOTSTRAP_PASSWORD=
APP_VERSION=1.0.0-preview.41
APP_VERSION=1.0.0-preview.42
UPDATE_CURRENT_VERSION_FILE=/opt/kaidi/current/VERSION
FINANCE_UPDATE_ENABLED=true
# Use either a stable direct asset base URL or the public Gitea latest-release API.
+24 -6
View File
@@ -5,13 +5,31 @@ umask 077
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
INSTALLER="$ROOT/deploy/install.sh"
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-fcc4c132812eaa304e2459f3b0cc176d75552afb6bd6a3be7c347fbf8f01e51b}
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-5aadfab9bdeef9279aeab6eee9baaae7182b2f3338fa61a558b3c073b69ad396}
RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest}
PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}
TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-}
log() { printf '[kaidi-git-install] %s\n' "$*"; }
die() { printf '[kaidi-git-install] ERROR: %s\n' "$*" >&2; exit 1; }
localize_message() {
local message=$1
[ "${KAIDI_LOG_LOCALE:-zh-CN}" = en ] && { printf '%s' "$message"; return; }
case "$message" in
"deploy/install.sh is missing"*) printf '缺少 deploy/install.sh,请在 Kaidi Git 工作区运行此命令' ;;
"sha256sum is required") printf '需要 sha256sum' ;;
"The installer SHA-256 is invalid") printf '安装器 SHA-256 无效' ;;
"The release public-key SHA-256 is invalid") printf '发布公钥 SHA-256 无效' ;;
"deploy/install.sh does not match"*) printf 'deploy/install.sh 与该 Git 标签的受信摘要不一致' ;;
"sudo is required when not running as root") printf '非 root 用户运行时需要 sudo' ;;
"The Gitea token file is not a regular file") printf 'Gitea Token 文件必须是普通文件' ;;
"The Gitea token file must contain 1 to 512 bytes") printf 'Gitea Token 文件必须包含 1 到 512 字节' ;;
"The Gitea token file contains invalid control characters") printf 'Gitea Token 文件包含无效控制字符' ;;
*) printf '%s' "$message" ;;
esac
}
die() { printf '[kaidi-git-install] 错误:%s\n' "$(localize_message "$*")" >&2; exit 1; }
[ -f "$INSTALLER" ] || die "deploy/install.sh is missing; run this command from the Kaidi Git checkout"
command -v sha256sum >/dev/null 2>&1 || die "sha256sum is required"
@@ -52,19 +70,19 @@ KAIDI_DB_PASSWORD=${KAIDI_DB_PASSWORD:-}
KAIDI_DB_HOST=${KAIDI_DB_HOST:-}
KAIDI_DB_PORT=${KAIDI_DB_PORT:-}
KAIDI_DB_NAME=${KAIDI_DB_NAME:-}
KAIDI_DB_CONTAINER=${KAIDI_DB_CONTAINER:-}
KAIDI_SESSION_COOKIE_SECURE=${KAIDI_SESSION_COOKIE_SECURE:-}
KAIDI_FILE_SCANNER_ENABLED=${KAIDI_FILE_SCANNER_ENABLED:-}
KAIDI_DB_BACKUP_MODE=${KAIDI_DB_BACKUP_MODE:-}
for name in KAIDI_REINSTALL KAIDI_SETUP_WIZARD KAIDI_APP_PORT KAIDI_SERVER_ADDRESS \
KAIDI_DB_URL KAIDI_DB_USERNAME KAIDI_DB_PASSWORD \
KAIDI_DB_HOST KAIDI_DB_PORT KAIDI_DB_NAME KAIDI_DB_CONTAINER \
KAIDI_SESSION_COOKIE_SECURE KAIDI_FILE_SCANNER_ENABLED; do
KAIDI_DB_HOST KAIDI_DB_PORT KAIDI_DB_NAME \
KAIDI_SESSION_COOKIE_SECURE KAIDI_FILE_SCANNER_ENABLED KAIDI_DB_BACKUP_MODE; do
value=${!name}
if [ -n "$value" ]; then
install_env+=("$name=$value")
fi
done
log "Installing the latest signed release from $RELEASE_API_URL"
log "正在从 Gitea Release API 安装最新签名版本:$RELEASE_API_URL"
"${root_command[@]}" "${install_env[@]}" bash "$INSTALLER"
+203 -83
View File
@@ -38,9 +38,125 @@ JAVA_ACTIVATED=false
SERVICE_USER=kaidi
SERVICE_GROUP=kaidi
HOST_ARCH=
LOG_LOCALE=${KAIDI_LOG_LOCALE:-zh-CN}
SERVICE_USER_CREATED=false
SERVICE_GROUP_CREATED=false
CREATED_LAYOUT_PATHS=()
log() { printf '[kaidi-install] %s\n' "$*"; }
die() { printf '[kaidi-install] ERROR: %s\n' "$*" >&2; exit 1; }
localize_message() {
local message=$1
[ "$LOG_LOCALE" = en ] && { printf '%s' "$message"; return; }
case "$message" in
"Unsupported CPU architecture: "*) printf '不支持的 CPU 架构:%s' "${message#Unsupported CPU architecture: }" ;;
"The installer only supports Linux") printf '安装程序仅支持 Linux' ;;
"Custom installation roots"*) printf '打包版 systemd 不支持自定义安装目录' ;;
"systemd is required") printf '需要 systemd' ;;
"systemd-analyze is required") printf '需要 systemd-analyze' ;;
"getconf is required") printf '需要 getconf' ;;
"systemd is not running as PID 1") printf 'systemd 当前不是 PID 1,无法托管服务' ;;
"/etc/systemd/system is missing") printf '缺少 /etc/systemd/system' ;;
"CPU architecture and userspace word size"*) printf 'CPU 架构与用户空间位数不匹配:%s' "${message#*: }" ;;
"32-bit Linux deployment requires glibc"*) printf '32 位 Linux 需要 glibc;当前系统不兼容 i686 Java' ;;
"32-bit Linux deployment requires the glibc loader"*) printf '32 位 Linux 缺少 glibc 加载器 /lib/ld-linux.so.2' ;;
"Set KAIDI_RELEASE_PUBLIC_KEY_SHA256"*) printf '请设置受信任的发布公钥 SHA-256:KAIDI_RELEASE_PUBLIC_KEY_SHA256' ;;
"Run with sudo or as root") printf '请使用 sudo 或 root 运行' ;;
"Set only one of KAIDI_RELEASE_TOKEN"*) printf 'KAIDI_RELEASE_TOKEN 与 KAIDI_RELEASE_TOKEN_FILE 只能设置一个' ;;
"KAIDI_RELEASE_TOKEN_FILE must be a regular file") printf 'KAIDI_RELEASE_TOKEN_FILE 必须是普通文件' ;;
"KAIDI_RELEASE_TOKEN_FILE is too large") printf 'KAIDI_RELEASE_TOKEN_FILE 过大' ;;
"KAIDI_REINSTALL must be true or false") printf 'KAIDI_REINSTALL 只能是 true 或 false' ;;
"KAIDI_SETUP_WIZARD must be true or false") printf 'KAIDI_SETUP_WIZARD 只能是 true 或 false' ;;
"A setup-wizard repair needs"*) printf '安装向导修复需要已有配置文件:%s' "${message#A setup-wizard repair needs the existing }" ;;
"The existing installation is already in production mode"*) printf '现有安装已是正式模式,请执行普通修复重装' ;;
"The setup wizard is already locked"*) printf '安装向导已锁定,请执行普通修复重装' ;;
"Kaidi Finance is already installed"*) printf 'Kaidi 财务系统已安装,请从系统更新页面执行更新或先卸载' ;;
"Set KAIDI_RELEASE_BASE_URL"*) printf '请设置 Gitea Release 源:KAIDI_RELEASE_BASE_URL 或 KAIDI_RELEASE_API_URL' ;;
"KAIDI_RELEASE_TOKEN is invalid") printf 'KAIDI_RELEASE_TOKEN 无效' ;;
"Download failed: "*) printf '下载失败:%s' "${message#Download failed: }" ;;
"Release URLs must use HTTPS") printf '发布地址必须使用 HTTPS' ;;
"Release authentication header is unavailable") printf '发布认证请求头不可用' ;;
"Release asset "*" is missing") printf '缺少发布资产:%s' "${message#Release asset }" ;;
"Gitea Release tag is invalid") printf 'Gitea Release 标签无效' ;;
"KAIDI_RELEASE_API_URL must be a Gitea"*) printf 'KAIDI_RELEASE_API_URL 必须是 Gitea Release API 地址' ;;
"Required command is missing"*) printf '依赖命令缺失:%s' "${message#*: }" ;;
"Supported package managers"*) printf '仅支持 apt、dnf 或 yum' ;;
"Using existing Java"*) printf '使用现有 Java:%s' "${message#Using existing Java 17+ runtime at }" ;;
"Local Java verified: "*) printf '本机 Java 校验通过:%s' "${message#Local Java verified: }" ;;
"No local Java"*) printf '未找到可用 Java 17,正在下载 Azul Java 17 运行时' ;;
"Java 17 runtime download URL"*) printf 'Java 17 运行时下载地址无效' ;;
"Downloaded Java verified: "*) printf '下载的 Java 校验通过:%s' "${message#Downloaded Java verified: }" ;;
"The existing Java runtime cannot start"*) printf '现有 Java 运行时无法启动,请检查架构和权限' ;;
"No Java 17 runtime is published"*) printf '当前 Linux 架构没有可用的 Java 17 运行时' ;;
"Java 17 runtime SHA-256 verification failed") printf 'Java 17 运行时 SHA-256 校验失败' ;;
"Downloaded Java runtime architecture"*) printf '下载的 Java 运行时架构与主机不匹配' ;;
"The downloaded Java runtime cannot start"*) printf '下载的 Java 运行时无法启动,请检查 glibc 和主机架构' ;;
"Java 17 runtime SHA-256 is unavailable") printf 'Java 17 运行时缺少 SHA-256 摘要' ;;
"Existing user "*" has unexpected home directory "*) printf '现有用户目录不符合 Kaidi 约定:%s' "${message#Existing user }" ;;
"Service-user filesystem and Java access checks passed") printf '服务用户文件系统与 Java 访问检查通过' ;;
*" cannot traverse or read the active release") printf '服务用户无法进入或读取当前发布目录' ;;
"The selected Java runtime is unavailable"*) printf '选定的 Java 运行时不可用:%s' "${message#The selected Java runtime is unavailable at }" ;;
"The selected Java runtime cannot execute"*) printf '服务用户无法执行选定的 Java 运行时' ;;
*" cannot execute the selected Java runtime") printf '服务用户无法执行选定的 Java 运行时' ;;
*" cannot execute as "*) printf '服务用户无法启动 Java 运行时' ;;
*" cannot write the file-storage directory") printf '服务用户无法写入文件存储目录' ;;
*" cannot write the update inbox") printf '服务用户无法写入更新请求目录' ;;
"SELinux context restoration reported"*) printf 'SELinux 上下文恢复有提示,将继续进行服务访问检查' ;;
"No interactive terminal detected"*) printf '未检测到交互终端,使用应用端口 %s' "${message##*port }" ;;
"Application port "*" is already held"*) printf '应用端口已由现有 Kaidi 服务占用:%s' "${message#Application port }" ;;
"Application port "*" is already in use"*) printf '应用端口已被占用,请使用 KAIDI_APP_PORT 更换端口' ;;
"Application will bind "*) printf '应用监听地址:%s' "${message#Application will bind }" ;;
"Reverse proxy target: "*) printf '反向代理目标:%s' "${message#Reverse proxy target: }" ;;
"KAIDI_SERVER_ADDRESS contains"*) printf 'KAIDI_SERVER_ADDRESS 含有不支持的字符' ;;
"KAIDI_APP_PORT must be an integer"*) printf 'KAIDI_APP_PORT 必须是 1024 到 65535 的整数' ;;
"Do not pass database credentials"*) printf '向导模式不要在命令行传数据库账号密码,请在浏览器向导中填写' ;;
"KAIDI_DB_USERNAME and KAIDI_DB_PASSWORD"*) printf '设置 KAIDI_DB_URL 时必须同时提供数据库用户名和密码' ;;
"An external MySQL 8.4 database is required"*) printf '需要连接用户预先准备的 MySQL 8.4 数据库:请设置 KAIDI_DB_URL、KAIDI_DB_USERNAME、KAIDI_DB_PASSWORD' ;;
"KAIDI_REINSTALL needs"*) printf 'KAIDI_REINSTALL 需要读取已有安装配置或提供数据库配置' ;;
"The database host is empty") printf '数据库主机不能为空' ;;
"The database port is invalid") printf '数据库端口无效' ;;
"The database name is invalid") printf '数据库名无效,只能包含字母、数字、下划线和美元符号' ;;
"KAIDI_DB_URL must start with jdbc:mysql://") printf 'KAIDI_DB_URL 必须以 jdbc:mysql:// 开头' ;;
"KAIDI_DB_USERNAME is empty") printf 'KAIDI_DB_USERNAME 不能为空' ;;
"KAIDI_DB_PASSWORD is empty") printf 'KAIDI_DB_PASSWORD 不能为空' ;;
"KAIDI_DB_BACKUP_MODE must be skip or mysqldump") printf 'KAIDI_DB_BACKUP_MODE 只能是 skip 或 mysqldump' ;;
"Waiting for application startup"*) printf '正在等待应用启动:%s' "${message#Waiting for application startup at }" ;;
"Application health check is UP") printf '应用健康检查通过(UP)' ;;
"Application is still starting"*) printf '应用仍在启动:%s' "${message#Application is still starting }" ;;
"Application service failed"*) printf '应用服务启动失败:%s' "${message#Application service failed during startup }" ;;
"Application health check did not become UP") printf '应用健康检查未变为 UP,请查看服务日志' ;;
"Application frontend entry point is unavailable") printf '应用前端入口不可访问,请检查服务和端口' ;;
"Installation failed; restoring"*) printf '安装失败,正在恢复原有运行状态' ;;
"ERROR: rollback was incomplete"*) printf '错误:回滚未完成,请检查 systemd 状态' ;;
*" contains a line break") printf '配置项包含换行符,已拒绝:%s' "${message%% contains a line break*}" ;;
"Kaidi Finance "*" is installed") printf 'Kaidi 财务系统 %s 已安装' "${message#Kaidi Finance }" ;;
"Configure your reverse proxy"*) printf '请将反向代理指向:%s' "${message#Configure your reverse proxy to }" ;;
"Open /setup"*) printf '请通过反向代理域名打开 /setup,完成首次安装向导' ;;
"Setup code: "*) printf '安装码位置:%s' "${message#Setup code: }" ;;
"Open the reverse-proxy domain"*) printf '请打开反向代理域名并使用管理员账号登录' ;;
"Temporary credentials: "*) printf '临时凭据位置:%s' "${message#Temporary credentials: }" ;;
"Change the temporary password"*) printf '首次登录后请立即修改临时密码' ;;
"Release public-key SHA-256 does not match"*) printf '发布公钥 SHA-256 与受信指纹不一致' ;;
"Release manifest signature verification failed") printf '发布清单签名校验失败' ;;
"Release version is invalid") printf '发布版本无效' ;;
"Release artifact name is invalid") printf '发布包文件名无效' ;;
"Release SHA-256 is invalid") printf '发布包 SHA-256 无效' ;;
"Release artifact SHA-256 verification failed") printf '发布包 SHA-256 校验失败' ;;
"Release archive contains an unsafe path") printf '发布归档包含不安全路径,已拒绝' ;;
"Release archive must not contain symbolic links") printf '发布归档不能包含符号链接,已拒绝' ;;
"Release app.jar is missing") printf '发布包缺少 app.jar' ;;
"Release frontend is missing") printf '发布包缺少前端文件' ;;
"Release version mismatch") printf '发布包版本与清单不一致' ;;
"Release updater is missing") printf '发布包缺少更新脚本' ;;
"Baota Spring Boot launcher is missing") printf '发布包缺少宝塔启动脚本' ;;
"Baota Spring Boot launcher is invalid") printf '宝塔启动脚本语法无效' ;;
"The existing FIELD_ENCRYPTION_KEY is missing") printf '已有安装缺少 FIELD_ENCRYPTION_KEY' ;;
"Installed systemd units failed validation") printf '已安装的 systemd 单元校验失败' ;;
"Environment verified: "*) printf '环境检查通过:%s' "${message#Environment verified: }" ;;
*) printf '%s' "$message" ;;
esac
}
log() { printf '[kaidi-install] %s\n' "$(localize_message "$*")"; }
die() { printf '[kaidi-install] 错误:%s\n' "$(localize_message "$*")" >&2; exit 1; }
sha256_file() {
sha256sum "$1" | awk '{print $1}'
@@ -208,15 +324,23 @@ download_release_asset() {
install_packages() {
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq ca-certificates coreutils curl findutils gzip jq openssl tar util-linux
apt-get update -qq >/dev/null 2>&1 \
|| die "系统软件源更新失败,请检查网络和 apt 配置"
apt-get install -y -qq ca-certificates coreutils curl findutils gzip jq openssl tar util-linux \
>/dev/null 2>&1 \
|| die "系统依赖安装失败;安装器不会安装 MySQL 或 Docker"
elif command -v dnf >/dev/null 2>&1; then
dnf install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux
dnf install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux \
>/dev/null 2>&1 \
|| die "系统依赖安装失败;安装器不会安装 MySQL 或 Docker"
elif command -v yum >/dev/null 2>&1; then
yum install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux
yum install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux \
>/dev/null 2>&1 \
|| die "系统依赖安装失败;安装器不会安装 MySQL 或 Docker"
else
die "Supported package managers are apt, dnf, and yum"
fi
log "系统依赖检查完成(未安装 MySQL、MariaDB、Docker)"
}
preflight_runtime_commands() {
@@ -230,31 +354,6 @@ preflight_runtime_commands() {
done
}
mysql_client_bin() {
local candidate
if [ -n "${KAIDI_MYSQL_CLIENT:-}" ]; then
case "$KAIDI_MYSQL_CLIENT" in
*/*) [ -x "$KAIDI_MYSQL_CLIENT" ] && printf '%s\n' "$KAIDI_MYSQL_CLIENT" && return 0 ;;
*) candidate=$(command -v "$KAIDI_MYSQL_CLIENT" 2>/dev/null || true); [ -n "$candidate" ] && printf '%s\n' "$candidate" && return 0 ;;
esac
return 1
fi
candidate=$(command -v mysql 2>/dev/null || command -v mariadb 2>/dev/null || true)
if [ -n "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
for candidate in \
/www/server/mysql/bin/mysql /www/server/mysql/bin/mariadb \
/usr/bin/mysql /usr/bin/mariadb /usr/local/mysql/bin/mysql /opt/mysql/bin/mysql; do
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
azul_arch() {
case "${HOST_ARCH:-$(normalized_host_arch)}" in
x86_64) printf x86 ;;
@@ -327,15 +426,15 @@ system_java_home() {
}
prepare_java() {
local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256 system_home java_line
local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256 system_home java_line java_version
JAVA_BIN=
JAVA_STAGED_DIR=
if system_home=$(system_java_home); then
JAVA_BIN="$system_home/bin/java"
java_line=$("$JAVA_BIN" -version 2>&1 | head -n 1) \
|| die "The existing Java runtime cannot start on this host"
log "Using existing Java 17+ runtime at $system_home"
log "Local Java verified: $java_line"
java_version=$(printf '%s\n' "$java_line" | sed -n 's/.*version "\([0-9][0-9.]*\).*/\1/p')
log "使用现有 Java 运行时:$system_home(版本 ${java_version:-未知})"
return 0
fi
log "No local Java 17 runtime found; downloading the official Azul Java 17 runtime"
@@ -362,7 +461,8 @@ prepare_java() {
java_line=$("$JAVA_STAGED_DIR/bin/java" -version 2>&1 | head -n 1) \
|| die "The downloaded Java runtime cannot start; verify glibc and the host architecture"
JAVA_BIN="$APP_ROOT/runtime/java/bin/java"
log "Downloaded Java verified: $java_line"
java_version=$(printf '%s\n' "$java_line" | sed -n 's/.*version "\([0-9][0-9.]*\).*/\1/p')
log "下载的 Java 运行时校验通过(版本 ${java_version:-未知})"
}
activate_java() {
@@ -383,6 +483,7 @@ ensure_service_identity() {
command -v getent >/dev/null 2>&1 || die "getent is required"
if ! getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
groupadd --system "$SERVICE_GROUP"
SERVICE_GROUP_CREATED=true
fi
if id "$SERVICE_USER" >/dev/null 2>&1; then
existing_home=$(getent passwd "$SERVICE_USER" | awk -F: '{print $6}')
@@ -397,10 +498,27 @@ ensure_service_identity() {
[ -n "$nologin_path" ] || nologin_path=/usr/sbin/nologin
[ -x "$nologin_path" ] || die "A nologin shell is required"
useradd --system --gid "$SERVICE_GROUP" --home-dir "$STATE_ROOT" --shell "$nologin_path" "$SERVICE_USER"
SERVICE_USER_CREATED=true
}
record_layout_path() {
[ -e "$1" ] || [ -L "$1" ] || CREATED_LAYOUT_PATHS+=("$1")
}
prepare_managed_layout() {
command -v runuser >/dev/null 2>&1 || die "runuser is required"
record_layout_path "$APP_ROOT"
record_layout_path "$APP_ROOT/releases"
record_layout_path "$APP_ROOT/runtime"
record_layout_path "$APP_ROOT/bin"
record_layout_path "$STATE_ROOT"
record_layout_path "$STATE_ROOT/files"
record_layout_path "$STATE_ROOT/tmp"
record_layout_path /var/log/kaidi
record_layout_path "$STATE_ROOT/setup"
record_layout_path "$UPDATE_STATE_ROOT"
record_layout_path "$UPDATE_STATE_ROOT/inbox"
record_layout_path "$CONFIG_ROOT"
install -d -o root -g "$SERVICE_GROUP" -m 0750 \
"$APP_ROOT" "$APP_ROOT/releases" "$APP_ROOT/runtime" "$APP_ROOT/bin"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \
@@ -529,7 +647,7 @@ configure_app_port() {
default_port=$(read_reinstall_env SERVER_PORT || true)
[[ "$default_port" =~ ^[0-9]{1,5}$ ]] || default_port=18080
if [ -t 1 ] && [ -r /dev/tty ]; then
printf '[kaidi-install] Application port [%s]: ' "$default_port" > /dev/tty
printf '[kaidi-install] 应用端口 [%s]:' "$default_port" > /dev/tty
if IFS= read -r entered < /dev/tty; then
APP_PORT=${entered:-$default_port}
else
@@ -640,48 +758,36 @@ database_name() {
printf '%s' "${KAIDI_DB_NAME:-$name}"
}
preflight_database() {
local client host port name version table
validate_database_configuration() {
[ "$SETUP_WIZARD" != true ] || return 0
case "$DB_URL" in
jdbc:mysql://*) ;;
*) die "KAIDI_DB_URL must start with jdbc:mysql://" ;;
esac
client=$(mysql_client_bin || true)
[ -n "$client" ] || die "A MySQL command-line client is required; set KAIDI_MYSQL_CLIENT or add mysql/mariadb to PATH"
host=$(database_host)
port=$(database_port)
name=$(database_name)
[ -n "$host" ] || die "The database host is empty"
[[ "$port" =~ ^[0-9]{1,5}$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ] \
[ -n "$DB_USERNAME" ] || die "KAIDI_DB_USERNAME is empty"
[ -n "$DB_PASSWORD" ] || die "KAIDI_DB_PASSWORD is empty"
[ -n "$(database_host)" ] || die "The database host is empty"
[[ "$(database_port)" =~ ^[0-9]{1,5}$ ]] \
&& [ "$(database_port)" -ge 1 ] && [ "$(database_port)" -le 65535 ] \
|| die "The database port is invalid"
[[ "$name" =~ ^[A-Za-z0-9_$]+$ ]] || die "The database name is invalid"
[[ "$(database_name)" =~ ^[A-Za-z0-9_$]+$ ]] || die "The database name is invalid"
# The installer deliberately does not invoke mysql/mariadb and does not
# execute DDL/DML. The application performs Flyway migrations only after
# it has connected to the operator-provided schema.
log "已记录外部 MySQL 连接配置;安装器不安装 MySQL、不连接数据库、不执行 SQL"
}
version=$(MYSQL_PWD="$DB_PASSWORD" "$client" --protocol=TCP --connect-timeout=10 \
-h "$host" -P "$port" -u "$DB_USERNAME" "$name" --batch --skip-column-names \
-e 'SELECT VERSION()' 2>/dev/null) || die "Cannot connect to the configured MySQL database"
case "$version" in
8.4.*) ;;
*) die "MySQL 8.4.x is required; server reported $version" ;;
# Kept as a compatibility name for older local fixtures and wrappers. It is
# now a pure configuration check and has no database side effects.
preflight_database() {
validate_database_configuration
}
validate_database_backup_mode() {
case "${KAIDI_DB_BACKUP_MODE:-skip}" in
skip|mysqldump) ;;
*) die "KAIDI_DB_BACKUP_MODE must be skip or mysqldump" ;;
esac
table="kaidi_install_preflight_$$"
if ! MYSQL_PWD="$DB_PASSWORD" "$client" --protocol=TCP --connect-timeout=10 \
-h "$host" -P "$port" -u "$DB_USERNAME" "$name" >/dev/null <<SQL
DROP TABLE IF EXISTS $table;
CREATE TABLE $table (id INT NOT NULL PRIMARY KEY);
INSERT INTO $table (id) VALUES (1);
UPDATE $table SET id = 2 WHERE id = 1;
DELETE FROM $table WHERE id = 2;
DROP TABLE $table;
SQL
then
MYSQL_PWD="$DB_PASSWORD" "$client" --protocol=TCP --connect-timeout=10 \
-h "$host" -P "$port" -u "$DB_USERNAME" "$name" \
-e "DROP TABLE IF EXISTS $table" >/dev/null 2>&1 || true
die "The database account needs DDL and DML permissions on $name"
fi
log "MySQL $version connectivity and DDL/DML permissions verified"
}
write_env_file() {
@@ -840,7 +946,7 @@ restore_managed_path() {
}
rollback_install() {
local index=0 path rollback_failed=false
local index=0 path rollback_failed=false layout_path service_uid
set +e
log "Installation failed; restoring the previous managed state"
systemctl stop kaidi-update.path kaidi-finance.service >/dev/null 2>&1 || true
@@ -858,6 +964,19 @@ rollback_install() {
elif [ "$JAVA_ACTIVATED" = true ]; then
rm -rf -- "$APP_ROOT/runtime/java" || rollback_failed=true
fi
if [ "$SERVICE_USER_CREATED" = true ]; then
service_uid=$(id -u "$SERVICE_USER" 2>/dev/null || true)
if [ -n "$service_uid" ]; then
userdel --force "$SERVICE_USER" >/dev/null 2>&1 || rollback_failed=true
fi
fi
if [ "$SERVICE_GROUP_CREATED" = true ] && getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
groupdel "$SERVICE_GROUP" >/dev/null 2>&1 || rollback_failed=true
fi
for ((index=${#CREATED_LAYOUT_PATHS[@]} - 1; index >= 0; index--)); do
layout_path=${CREATED_LAYOUT_PATHS[$index]}
rmdir -- "$layout_path" >/dev/null 2>&1 || true
done
systemctl daemon-reload >/dev/null 2>&1 || rollback_failed=true
restore_unit_state kaidi-finance.service "$PREVIOUS_APP_ENABLED" "$PREVIOUS_APP_ACTIVE" \
|| rollback_failed=true
@@ -882,6 +1001,7 @@ main() {
trap cleanup EXIT
validate_inputs
validate_database_backup_mode
configure_app_port
preflight_host
install_packages
@@ -1025,13 +1145,13 @@ write_env_file "$CONFIG_ROOT/update.env" \
KAIDI_JAVA_BIN "$JAVA_BIN" \
KAIDI_HEALTH_URL "$HEALTH_URL" \
KAIDI_APP_INDEX_URL "$APP_INDEX_URL" \
KAIDI_DB_CONTAINER "${KAIDI_DB_CONTAINER:-}" \
KAIDI_DB_HOST "$(database_host)" \
KAIDI_DB_PORT "$(database_port)" \
KAIDI_DB_NAME "$(database_name)" \
KAIDI_DB_USERNAME "$DB_USERNAME" \
KAIDI_DB_PASSWORD "$DB_PASSWORD" \
KAIDI_MYSQLDUMP_BIN "${KAIDI_MYSQLDUMP_BIN:-}"
KAIDI_MYSQLDUMP_BIN "${KAIDI_MYSQLDUMP_BIN:-}" \
KAIDI_DB_BACKUP_MODE "${KAIDI_DB_BACKUP_MODE:-skip}"
chmod 0600 "$CONFIG_ROOT/update.env"
install -m 0644 "$RELEASE_DIR/ops/kaidi-finance.service" /etc/systemd/system/kaidi-finance.service
@@ -1057,26 +1177,26 @@ if [ "$SETUP_WIZARD" != true ]; then
sed -i 's/^FINANCE_BOOTSTRAP_PASSWORD=.*$/FINANCE_BOOTSTRAP_PASSWORD=""/' "$CONFIG_ROOT/kaidi.env"
fi
jq -n --arg version "$VERSION" --arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{state:"CURRENT",message:"Initial release is running",targetVersion:$version,updatedAt:$updatedAt}' \
'{state:"CURRENT",message:"初始版本正在运行",targetVersion:$version,updatedAt:$updatedAt}' \
> "$UPDATE_STATE_ROOT/status.json"
chmod 0644 "$UPDATE_STATE_ROOT/status.json"
systemctl enable --now kaidi-update.path
if [ "$SETUP_WIZARD" = true ]; then
cat > /root/kaidi-first-login.txt <<EOF
URL after reverse proxy: http://SERVER_IP/setup
Reverse proxy target: $PROXY_TARGET
Setup code: $SETUP_CODE
Version: $VERSION
反向代理访问地址:http://SERVER_IP/setup
反向代理目标:$PROXY_TARGET
安装码:$SETUP_CODE
版本:$VERSION
EOF
chmod 0600 /root/kaidi-first-login.txt
elif [ "$REINSTALL" != true ]; then
cat > /root/kaidi-first-login.txt <<EOF
URL after reverse proxy: http://SERVER_IP/
Reverse proxy target: $PROXY_TARGET
Username: admin
Temporary password: $ADMIN_PASSWORD
Version: $VERSION
反向代理访问地址:http://SERVER_IP/
反向代理目标:$PROXY_TARGET
管理员账号:admin
临时密码:$ADMIN_PASSWORD
版本:$VERSION
EOF
chmod 0600 /root/kaidi-first-login.txt
fi
+57 -3
View File
@@ -16,9 +16,40 @@ REQUIRED_CONFIRMATION=DELETE_LOCAL_KAIDI_INSTALLATION
SERVICE_USER=kaidi
SERVICE_GROUP=kaidi
BACKUP_ARCHIVE=
LOG_LOCALE=${KAIDI_LOG_LOCALE:-zh-CN}
DELETE_RECOVERY_BACKUP=${KAIDI_PURGE_DELETE_BACKUP:-false}
log() { printf '[kaidi-purge] %s\n' "$*"; }
die() { printf '[kaidi-purge] ERROR: %s\n' "$*" >&2; exit 1; }
localize_message() {
local message=$1
[ "$LOG_LOCALE" = en ] && { printf '%s' "$message"; return; }
case "$message" in
"Run with sudo or as root") printf '请使用 sudo 或 root 运行' ;;
"The purge script only supports Linux") printf '卸载程序仅支持 Linux' ;;
"Set KAIDI_PURGE_CONFIRM="*) printf '请设置确认变量:KAIDI_PURGE_CONFIRM=%s' "${message#Set KAIDI_PURGE_CONFIRM=}" ;;
"KAIDI_PURGE_BACKUP_ROOT must be an absolute path") printf 'KAIDI_PURGE_BACKUP_ROOT 必须是绝对路径' ;;
"KAIDI_PURGE_DELETE_BACKUP must be true or false") printf 'KAIDI_PURGE_DELETE_BACKUP 只能是 true 或 false' ;;
"The recovery backup must be outside"*) printf '恢复备份目录必须位于 Kaidi 管理目录之外' ;;
"The recovery backup root must not be a symbolic link") printf '恢复备份目录不能是符号链接' ;;
"Failed to stop "*) printf '停止服务失败:%s' "${message#Failed to stop }" ;;
"Stopping processes owned by"*) printf '正在停止专用服务账号进程:%s' "${message##*: }" ;;
"Stopping remaining Kaidi processes: "*) printf '正在停止剩余 Kaidi 进程:%s' "${message#Stopping remaining Kaidi processes: }" ;;
"A process manager restarted"*) printf '检测到宝塔等进程管理器正在重新拉起 Kaidi;请先停止并删除宝塔中的 Kaidi 项目,再重试卸载' ;;
"No local configuration or data"*) printf '没有需要备份的本地配置或数据' ;;
"Creating a root-only recovery backup at "*) printf '正在创建仅 root 可读的恢复备份:%s' "${message#Creating a root-only recovery backup at }" ;;
"Failed to remove the dedicated"*) printf '删除 Kaidi 专用服务账号失败' ;;
"Processes owned by the removed"*) printf '删除服务账号后仍有进程运行' ;;
"Keeping pre-existing user"*) printf '保留预先存在的用户:%s' "${message#Keeping pre-existing user }" ;;
"Keeping group "*) printf '保留仍被其他账号使用的用户组:%s' "${message#Keeping group }" ;;
"External MySQL data and reverse-proxy configuration will not be modified") printf '不会修改外部 MySQL 数据和反向代理配置' ;;
"Local Kaidi installation state has been removed") printf '本地 Kaidi 安装文件、服务和运行状态已删除' ;;
"Recovery backup: "*) printf '恢复备份:%s' "${message#Recovery backup: }" ;;
"Use a new empty MySQL database for the next installation") printf '重新安装时请使用新的空 MySQL 数据库' ;;
*) printf '%s' "$message" ;;
esac
}
log() { printf '[kaidi-purge] %s\n' "$(localize_message "$*")"; }
die() { printf '[kaidi-purge] 错误:%s\n' "$(localize_message "$*")" >&2; exit 1; }
systemd_available() {
command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]
@@ -40,6 +71,10 @@ validate_inputs() {
;;
esac
[ ! -L "$BACKUP_ROOT" ] || die "The recovery backup root must not be a symbolic link"
case "$DELETE_RECOVERY_BACKUP" in
true|false) ;;
*) die "KAIDI_PURGE_DELETE_BACKUP must be true or false" ;;
esac
}
stop_systemd_units() {
@@ -206,7 +241,20 @@ remove_managed_paths() {
}
remove_download_archives() {
rm -f -- /tmp/kaidi-finance-*.tar.gz
rm -f -- \
/tmp/kaidi-finance-*.tar.gz \
/tmp/kaidi-purge.sh \
/tmp/kaidi-SHA256SUMS \
/tmp/kaidi-install.sh
}
verify_managed_paths_removed() {
local path
for path in "$APP_ROOT" "$BAOTA_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" \
"$UPDATE_STATE_ROOT" "$LOG_ROOT" "$FIRST_LOGIN_FILE"; do
[ ! -e "$path" ] && [ ! -L "$path" ] \
|| die "卸载后仍发现受管路径:$path"
done
}
remove_service_identity() {
@@ -258,9 +306,15 @@ main() {
remove_download_archives
remove_service_identity
stop_managed_processes false
verify_managed_paths_removed
log "Local Kaidi installation state has been removed"
if [ -n "$BACKUP_ARCHIVE" ]; then
log "Recovery backup: $BACKUP_ARCHIVE"
if [ "$DELETE_RECOVERY_BACKUP" = true ]; then
rm -f -- "$BACKUP_ARCHIVE"
rmdir -- "$BACKUP_ROOT" >/dev/null 2>&1 || true
log "已按 KAIDI_PURGE_DELETE_BACKUP=true 删除恢复备份"
fi
fi
log "Use a new empty MySQL database for the next installation"
}
+181 -40
View File
@@ -30,6 +30,7 @@ HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health}
APP_INDEX_URL=${KAIDI_APP_INDEX_URL:-http://127.0.0.1:18080/}
LOCK_FILE=$STATE_ROOT/update.lock
BACKUP_ROOT=${KAIDI_BACKUP_ROOT:-$STATE_ROOT/backups}
DB_BACKUP_MODE=${KAIDI_DB_BACKUP_MODE:-skip}
UPDATER_PATH=${KAIDI_UPDATER_PATH:-$APP_ROOT/bin/update.sh}
SYSTEMD_ROOT=${KAIDI_SYSTEMD_ROOT:-/etc/systemd/system}
LOG_ROOT=${KAIDI_LOG_ROOT:-/var/log/kaidi}
@@ -45,33 +46,167 @@ DOWNLOAD_PID=
DOWNLOAD_PGID=
LAST_DOWNLOAD_SPEED=0
SURFACE_FAILURE=
LOG_LOCALE=${KAIDI_LOG_LOCALE:-zh-CN}
localize_message() {
message=$1
[ "$LOG_LOCALE" = en ] && { printf '%s' "$message"; return; }
# A rollback failure must keep the manual-recovery signal visible even when
# the original reason also contains a more specific error prefix.
case "$message" in
*"rollback is incomplete and manual recovery is required"*)
printf '新版本应用验证失败,回滚未完成,需要人工恢复'
return
;;
esac
case "$message" in
"Update health-check settings must be non-negative integers") printf '更新健康检查参数必须是非负整数' ;;
"Update health-check attempts must be at least 1") printf '更新健康检查次数至少为 1' ;;
"KAIDI_PROCESS_MANAGER must be systemd or baota") printf 'KAIDI_PROCESS_MANAGER 只能是 systemd 或 baota' ;;
"KAIDI_UPDATE_SHUTDOWN_ATTEMPTS must be a positive integer") printf 'KAIDI_UPDATE_SHUTDOWN_ATTEMPTS 必须是正整数' ;;
"KAIDI_UPDATE_SHUTDOWN_ATTEMPTS must be at least 1") printf 'KAIDI_UPDATE_SHUTDOWN_ATTEMPTS 至少为 1' ;;
"KAIDI_PID_FILE must be an absolute path") printf 'KAIDI_PID_FILE 必须是绝对路径' ;;
"KAIDI_RUNTIME_ENV_FILE must be an absolute path") printf 'KAIDI_RUNTIME_ENV_FILE 必须是绝对路径' ;;
"KAIDI_DB_BACKUP_MODE must be skip or mysqldump") printf 'KAIDI_DB_BACKUP_MODE 只能是 skip 或 mysqldump' ;;
"Setup runtime environment must be a regular file") printf '安装向导运行时配置必须是普通文件' ;;
"Setup runtime environment is not readable") printf '安装向导运行时配置不可读' ;;
"Setup runtime environment is too large") printf '安装向导运行时配置过大' ;;
"Setup runtime environment has an unexpected owner") printf '安装向导运行时配置属主不正确' ;;
"Setup runtime environment must not be writable"*) printf '安装向导运行时配置不能被组或其他用户写入' ;;
"Setup runtime environment contains an invalid line") printf '安装向导运行时配置包含无效行' ;;
"Setup runtime environment "*) printf '安装向导运行时配置错误,请检查 application.env' ;;
"Setup runtime database port is invalid") printf '安装向导数据库端口无效' ;;
"Database configuration is invalid: DB_URL is missing"*) printf '数据库配置无效:缺少 DB_URL,未重启应用' ;;
"Database configuration is invalid: database host and name are missing"*) printf '数据库配置无效:缺少数据库主机或库名,未重启应用' ;;
"Database configuration is invalid: DB_URL does not match"*) printf '数据库配置无效:DB_URL 与主机、端口、库名不一致,未重启应用' ;;
"UPDATE_RELEASE_TOKEN is invalid") printf 'UPDATE_RELEASE_TOKEN 无效' ;;
"UPDATE_RELEASE_BASE_URL or UPDATE_RELEASE_API_URL is not configured") printf '未配置更新源地址' ;;
"Service user "*" is missing") printf '缺少服务用户:%s' "$(printf '%s' "${message#Service user }" | sed 's/ is missing$//')" ;;
"Service user "*" is not a member"*) printf '服务用户不属于指定服务组' ;;
"runuser is required") printf '需要 runuser' ;;
"setsid is required") printf '需要 setsid' ;;
"Managed application or update directories could not be prepared") printf '应用或更新目录准备失败' ;;
"Writable update directories could not be prepared") printf '可写更新目录准备失败' ;;
"Private update directories could not be prepared") printf '私有更新目录准备失败' ;;
"Private update queue directories could not be prepared") printf '私有更新队列目录准备失败' ;;
"Downloading signed release "*) printf '正在下载已签名版本:%s' "${message#Downloading signed release }" ;;
"Release "*" download completed") printf '版本下载完成:%s' "${message#Release }" ;;
"Validating signed release "*) printf '正在校验签名版本:%s' "${message#Validating signed release }" ;;
"Revalidating cached release "*) printf '正在重新校验已缓存版本:%s' "${message#Revalidating cached release }" ;;
"Release "*" is downloaded and verified; confirm installation") printf '版本已下载并校验,请确认安装:%s' "${message#Release }" ;;
"Release "*" is running") printf '版本运行中:%s' "${message#Release }" ;;
*"automatic update watcher could not be started"*) printf '自动更新监听器未能启动' ;;
"Version "*" is already installed") printf '版本已安装:%s' "${message#Version }" ;;
"Starting and verifying release "*) printf '正在启动并验证版本:%s' "${message#Starting and verifying release }" ;;
"Validating current release before switching to "*) printf '切换前正在验证当前版本(目标:%s)' "${message#Validating current release before switching to }" ;;
"Backing up database before installing "*) printf '安装前正在备份数据库(目标:%s)' "${message#Backing up database before installing }" ;;
"Database backup skipped; updater does not access MySQL") printf '已跳过数据库备份;更新器不会访问 MySQL' ;;
"Installing release "*) printf '正在安装版本:%s' "${message#Installing release }" ;;
"Update process was interrupted"*) printf '更新进程被中断,系统将执行自动恢复' ;;
"Update transaction evidence could not be quarantined"*) printf '更新事务证据无法隔离,已停止自动更新' ;;
"Update request could not be archived"*) printf '更新请求无法归档,已停止自动更新' ;;
"Update service is locked for manual recovery"*) printf '更新服务已锁定,需要人工恢复;新请求已拒绝' ;;
"Processing update request must be a regular file") printf '处理中更新请求必须是普通文件' ;;
"Update request must be a regular file") printf '更新请求必须是普通文件' ;;
"Claimed update request must be a regular file") printf '已领取的更新请求必须是普通文件' ;;
"Verified release cache is missing") printf '缺少已校验的发布缓存' ;;
"Cached release manifest is missing") printf '缺少缓存的发布清单' ;;
"Cached release signature is missing") printf '缺少缓存的发布签名' ;;
"Cached release artifact is missing") printf '缺少缓存的发布包' ;;
"Update request version is invalid") printf '更新请求版本无效' ;;
"Update request action is invalid") printf '更新请求动作无效' ;;
"Release manifest "*" is missing") printf '缺少发布清单资产' ;;
"Release manifest download failed") printf '发布清单下载失败' ;;
"Release manifest signature download failed") printf '发布清单签名下载失败' ;;
"Gitea latest Release lookup failed") printf 'Gitea 最新 Release 查询失败' ;;
"Release manifest signature verification failed") printf '发布清单签名校验失败' ;;
"Requested version is no longer the latest signed release") printf '请求版本已不是最新签名版本,请重新获取版本' ;;
"Release artifact download failed") printf '发布包下载失败' ;;
"Release artifact asset is missing") printf '缺少发布包资产' ;;
"Release artifact SHA-256 verification failed") printf '发布包 SHA-256 校验失败' ;;
"Release artifact size verification failed") printf '发布包大小校验失败' ;;
"Release archive contains an unsafe path") printf '发布归档包含不安全路径,已拒绝' ;;
"Release archive must not contain symbolic links") printf '发布归档不能包含符号链接,已拒绝' ;;
"Release application restart failed") printf '新版本应用重启失败' ;;
"Release application verification failed"*) printf '新版本应用验证失败:%s' "${message#Release application verification failed }" ;;
"Current release preflight failed"*) printf '当前版本预检查失败,未重启应用:%s' "${message#Current release preflight failed }" ;;
"Current release path is invalid"*) printf '当前版本路径无效,未重启应用' ;;
"Service user cannot access the release or selected Java runtime") printf '服务用户无法访问发布目录或 Java 运行时' ;;
"Release operations files could not be backed up") printf '旧版本运维文件备份失败' ;;
"Existing operations files could not be backed up") printf '已有运维文件备份失败' ;;
"Release directory is already active") printf '目标版本目录已处于 active 状态' ;;
"Failed release staging directory could not be cleaned") printf '失败版本暂存目录清理失败' ;;
"Release directory could not be activated") printf '目标版本目录无法启用' ;;
"Release ownership or permissions could not be secured") printf '目标版本目录权限保护失败' ;;
"Release operations validation failed") printf '发布运维文件校验失败' ;;
"Release public key is missing") printf '发布公钥缺失' ;;
"Release verification public key is missing") printf '缺少发布校验公钥' ;;
"Release manifest version is invalid") printf '发布清单版本无效' ;;
"Release artifact name is invalid") printf '发布包文件名无效' ;;
"Release SHA-256 is invalid") printf '发布包 SHA-256 无效' ;;
"Release artifact size is invalid") printf '发布包大小无效' ;;
"Release archive could not be listed") printf '发布归档无法读取' ;;
"Release archive could not be extracted") printf '发布归档无法解压' ;;
"Release app.jar is missing") printf '发布包缺少 app.jar' ;;
"Release frontend is missing") printf '发布包缺少前端入口' ;;
"Release version file mismatch") printf '发布包 VERSION 与目标版本不一致' ;;
"Release updater is missing") printf '发布包缺少更新脚本' ;;
"Release Baota launcher is missing") printf '发布包缺少宝塔启动脚本' ;;
"Release Baota initializer is missing") printf '发布包缺少宝塔初始化脚本' ;;
"Release application unit is missing") printf '发布包缺少应用 systemd 单元' ;;
"Release updater unit is missing") printf '发布包缺少更新 systemd 单元' ;;
"Release updater path unit is missing") printf '发布包缺少更新监听单元' ;;
"Verified release cache path is unsafe") printf '已校验发布缓存路径不安全' ;;
"Verified release cache could not be activated") printf '已校验发布缓存无法启用' ;;
"Database backup failed") printf '数据库备份失败' ;;
"Database backup is empty") printf '数据库备份为空' ;;
"Database backup compression failed") printf '数据库备份压缩失败' ;;
"mysqldump is required"*) printf '更新前需要 mysqldump;请配置 KAIDI_MYSQLDUMP_BIN 或安装客户端' ;;
"Release operations files could not be activated") printf '发布运维文件无法切换' ;;
"Release application link could not be activated") printf '应用 current 链接无法切换' ;;
*"previous application release was restored and verified; database backup was retained"*) printf '已恢复并验证旧版本;数据库备份已保留' ;;
*"previous application release was restored and verified; no database backup was created"*) printf '已恢复并验证旧版本;本次未创建数据库备份' ;;
*"previous application release is running"*) printf '旧版本正在运行,但运维文件需要人工检查' ;;
*) printf '%s' "$message" ;;
esac
}
case "$HEALTH_ATTEMPTS:$HEALTH_INTERVAL_SECONDS" in
*[!0-9:]* | :* | *:) printf '%s\n' "Update health-check settings must be non-negative integers" >&2; exit 1 ;;
*[!0-9:]* | :* | *:) printf '%s\n' "$(localize_message 'Update health-check settings must be non-negative integers')" >&2; exit 1 ;;
esac
[ "$HEALTH_ATTEMPTS" -ge 1 ] || { printf '%s\n' "Update health-check attempts must be at least 1" >&2; exit 1; }
[ "$HEALTH_ATTEMPTS" -ge 1 ] || { printf '%s\n' "$(localize_message 'Update health-check attempts must be at least 1')" >&2; exit 1; }
case "$PROCESS_MANAGER" in
systemd|baota) ;;
*) printf '%s\n' "KAIDI_PROCESS_MANAGER must be systemd or baota" >&2; exit 1 ;;
*) printf '%s\n' "$(localize_message 'KAIDI_PROCESS_MANAGER must be systemd or baota')" >&2; exit 1 ;;
esac
case "$SHUTDOWN_ATTEMPTS" in
''|*[!0-9]*) printf '%s\n' "KAIDI_UPDATE_SHUTDOWN_ATTEMPTS must be a positive integer" >&2; exit 1 ;;
''|*[!0-9]*) printf '%s\n' "$(localize_message 'KAIDI_UPDATE_SHUTDOWN_ATTEMPTS must be a positive integer')" >&2; exit 1 ;;
esac
[ "$SHUTDOWN_ATTEMPTS" -ge 1 ] || { printf '%s\n' "KAIDI_UPDATE_SHUTDOWN_ATTEMPTS must be at least 1" >&2; exit 1; }
[ "$SHUTDOWN_ATTEMPTS" -ge 1 ] || { printf '%s\n' "$(localize_message 'KAIDI_UPDATE_SHUTDOWN_ATTEMPTS must be at least 1')" >&2; exit 1; }
case "$PID_FILE" in
/*) ;;
*) printf '%s\n' "KAIDI_PID_FILE must be an absolute path" >&2; exit 1 ;;
*) printf '%s\n' "$(localize_message 'KAIDI_PID_FILE must be an absolute path')" >&2; exit 1 ;;
esac
case "$RUNTIME_ENV_FILE" in
/*) ;;
*) printf '%s\n' "KAIDI_RUNTIME_ENV_FILE must be an absolute path" >&2; exit 1 ;;
*) printf '%s\n' "$(localize_message 'KAIDI_RUNTIME_ENV_FILE must be an absolute path')" >&2; exit 1 ;;
esac
case "${KAIDI_SKIP_DB_BACKUP:-}" in
true) DB_BACKUP_MODE=skip ;;
false)
[ -n "${KAIDI_DB_BACKUP_MODE:-}" ] || DB_BACKUP_MODE=mysqldump
;;
esac
case "$DB_BACKUP_MODE" in
skip|mysqldump) ;;
*) printf '%s\n' "$(localize_message 'KAIDI_DB_BACKUP_MODE must be skip or mysqldump')" >&2; exit 1 ;;
esac
bootstrap_die() {
if [ -f "$PROCESSING_FILE" ] && [ ! -L "$PROCESSING_FILE" ]; then
fail "$1"
fi
printf '%s\n' "$1" >&2
printf '%s\n' "$(localize_message "$1")" >&2
exit 1
}
@@ -149,7 +284,7 @@ validate_runtime_database_config() {
status() {
state=$1
message=$2
message=$(localize_message "$2")
version=${3:-}
downloaded_bytes=${4:-}
total_bytes=${5:-}
@@ -227,9 +362,10 @@ complete_request() {
fail() {
TERMINAL_STATUS_WRITTEN=true
status FAILED "$1" "${TARGET_VERSION:-}"
message=$(localize_message "$1")
status FAILED "$message" "${TARGET_VERSION:-}"
archive_processing_request || true
printf '%s\n' "$1" >&2
printf '%s\n' "$message" >&2
exit 1
}
@@ -601,7 +737,10 @@ mysqldump_bin() {
backup_database() {
DATABASE_BACKUP=
[ "${KAIDI_SKIP_DB_BACKUP:-false}" = "true" ] && return
if [ "$DB_BACKUP_MODE" = skip ]; then
event INFO BACKING_UP '数据库备份已跳过;更新器不会访问 MySQL'
return 0
fi
mkdir -p "$BACKUP_ROOT"
chmod 0700 "$BACKUP_ROOT"
dump_file=$(mktemp "$BACKUP_ROOT/.mysql-dump.XXXXXX.sql")
@@ -609,26 +748,16 @@ backup_database() {
backup_tmp="$backup.tmp.$$"
chmod 0600 "$dump_file"
if [ -n "${KAIDI_DB_CONTAINER:-}" ] && command -v docker >/dev/null 2>&1 \
&& docker inspect "$KAIDI_DB_CONTAINER" >/dev/null 2>&1; then
if ! docker exec -e MYSQL_PWD="${KAIDI_DB_PASSWORD:-}" "$KAIDI_DB_CONTAINER" \
mysqldump --single-transaction --routines --triggers \
-u "${KAIDI_DB_USERNAME:-kaidi}" "${KAIDI_DB_NAME:-kaidi_finance}" > "$dump_file"; then
rm -f "$dump_file" "$backup_tmp"
fail "Database backup failed"
fi
else
dump_bin=$(mysqldump_bin || true)
[ -n "$dump_bin" ] || {
rm -f "$dump_file" "$backup_tmp"
fail "mysqldump is required before installing an update; set KAIDI_MYSQLDUMP_BIN or install it in a standard MySQL bin directory"
}
if ! MYSQL_PWD=${KAIDI_DB_PASSWORD:-} "$dump_bin" --single-transaction --routines --triggers \
-h "${KAIDI_DB_HOST:-127.0.0.1}" -P "${KAIDI_DB_PORT:-3306}" \
-u "${KAIDI_DB_USERNAME:-kaidi}" "${KAIDI_DB_NAME:-kaidi_finance}" > "$dump_file"; then
rm -f "$dump_file" "$backup_tmp"
fail "Database backup failed"
fi
dump_bin=$(mysqldump_bin || true)
[ -n "$dump_bin" ] || {
rm -f "$dump_file" "$backup_tmp"
fail "mysqldump is required before installing an update; set KAIDI_MYSQLDUMP_BIN or install it in a standard MySQL bin directory"
}
if ! MYSQL_PWD=${KAIDI_DB_PASSWORD:-} "$dump_bin" --single-transaction --routines --triggers \
-h "${KAIDI_DB_HOST:-127.0.0.1}" -P "${KAIDI_DB_PORT:-3306}" \
-u "${KAIDI_DB_USERNAME:-kaidi}" "${KAIDI_DB_NAME:-kaidi_finance}" > "$dump_file"; then
rm -f "$dump_file" "$backup_tmp"
fail "Database backup failed"
fi
[ -s "$dump_file" ] || {
@@ -643,8 +772,12 @@ backup_database() {
mv -f "$backup_tmp" "$backup"
DATABASE_BACKUP=$backup
rm -f "$dump_file"
find "$BACKUP_ROOT" -type f -name 'mysql-*.sql.gz' -printf '%T@ %p\n' \
| sort -nr | awk 'NR > 5 {sub(/^[^ ]+ /, ""); print}' | xargs -r rm -f
# Keep the newest five backups without relying on GNU find's -printf; the
# updater is also exercised on BSD/macOS fixtures during release checks.
# shellcheck disable=SC2012 # Generated backup names contain no whitespace.
LC_ALL=C ls -1t "$BACKUP_ROOT"/mysql-*.sql.gz 2>/dev/null \
| awk 'NR > 5' \
| while IFS= read -r old_backup; do rm -f -- "$old_backup"; done
}
atomic_install() {
@@ -796,26 +929,30 @@ rollback_active_transaction() {
&& verify_app_surface "$previous_target"; then
remove_failed_release "$failed_release"
if [ "$operations_restored" = true ]; then
database_backup_record=$(transaction_value database-backup)
rm -rf "$ACTIVE_TRANSACTION"
fail "$reason; previous application release was restored and verified; database backup was retained"
if [ -n "$database_backup_record" ] || [ -n "${DATABASE_BACKUP:-}" ]; then
fail "$reason; previous application release was restored and verified; database backup was retained"
fi
fail "$reason; previous application release was restored and verified; no database backup was created"
fi
fail "$reason; previous application release is running, but operations restoration requires manual review; transaction evidence was retained"
fi
TERMINAL_STATUS_WRITTEN=true
recovery_guard_failed=false
if ! archive_processing_request; then
printf '%s\n' "Update request could not be archived after an incomplete rollback" >&2
printf '%s\n' "$(localize_message 'Update request could not be archived after an incomplete rollback')" >&2
recovery_guard_failed=true
fi
if ! quarantine_active_transaction; then
printf '%s\n' "Update transaction evidence could not be quarantined after an incomplete rollback" >&2
printf '%s\n' "$(localize_message 'Update transaction evidence could not be quarantined after an incomplete rollback')" >&2
recovery_guard_failed=true
fi
if [ "$recovery_guard_failed" = true ]; then
systemctl stop "$UPDATE_PATH_NAME" >/dev/null 2>&1 || true
fi
status RECOVERY_REQUIRED "$reason; rollback is incomplete and manual recovery is required" "${TARGET_VERSION:-}"
printf '%s\n' "$reason; rollback is incomplete and manual recovery is required" >&2
printf '%s\n' "$(localize_message "$reason; rollback is incomplete and manual recovery is required")" >&2
exit 1
}
@@ -885,7 +1022,7 @@ if [ -f "$STATUS_FILE" ] && [ ! -L "$STATUS_FILE" ] \
if ! archive_processing_request; then
systemctl stop "$UPDATE_PATH_NAME" >/dev/null 2>&1 || true
fi
printf '%s\n' "Update service is locked for manual recovery; new requests are rejected" >&2
printf '%s\n' "$(localize_message 'Update service is locked for manual recovery; new requests are rejected')" >&2
exit 1
fi
@@ -1053,7 +1190,11 @@ write_transaction_value previous-target "$PREVIOUS_TARGET"
write_transaction_value release-dir "$RELEASE_DIR"
write_transaction_value phase PREPARED
status BACKING_UP "Backing up database before installing $TARGET_VERSION" "$TARGET_VERSION"
if [ "$DB_BACKUP_MODE" = mysqldump ]; then
status BACKING_UP "Backing up database before installing $TARGET_VERSION" "$TARGET_VERSION"
else
status BACKING_UP "Database backup skipped; updater does not access MySQL" "$TARGET_VERSION"
fi
backup_database
[ -z "${DATABASE_BACKUP:-}" ] || write_transaction_value database-backup "$DATABASE_BACKUP"
if ! backup_operations; then