1222 lines
53 KiB
Bash
Executable File
1222 lines
53 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
umask 077
|
||
|
||
APP_ROOT=${KAIDI_APP_ROOT:-/opt/kaidi}
|
||
STATE_ROOT=${KAIDI_STATE_ROOT:-/var/lib/kaidi}
|
||
UPDATE_STATE_ROOT=${KAIDI_UPDATE_STATE_ROOT:-/var/lib/kaidi-update}
|
||
CONFIG_ROOT=${KAIDI_CONFIG_ROOT:-/etc/kaidi}
|
||
RELEASE_BASE_URL=${KAIDI_RELEASE_BASE_URL:-}
|
||
RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest}
|
||
RELEASE_TOKEN=${KAIDI_RELEASE_TOKEN:-}
|
||
RELEASE_TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-}
|
||
HEALTH_URL=${KAIDI_HEALTH_URL:-}
|
||
APP_INDEX_URL=${KAIDI_APP_INDEX_URL:-}
|
||
APP_PORT=${KAIDI_APP_PORT:-}
|
||
SERVER_ADDRESS=${KAIDI_SERVER_ADDRESS:-127.0.0.1}
|
||
PROXY_TARGET=
|
||
TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}
|
||
REINSTALL=${KAIDI_REINSTALL:-false}
|
||
if [ "${KAIDI_SETUP_WIZARD+x}" = x ]; then
|
||
SETUP_WIZARD=$KAIDI_SETUP_WIZARD
|
||
elif [ "$REINSTALL" = true ]; then
|
||
SETUP_WIZARD=false
|
||
else
|
||
SETUP_WIZARD=true
|
||
fi
|
||
WORK_DIR=
|
||
RELEASE_AUTH_HEADER_FILE=
|
||
INSTALL_TRANSACTION_ARMED=false
|
||
INSTALL_COMMITTED=false
|
||
RELEASE_DIR=
|
||
BACKUP_DIR=
|
||
JAVA_BIN=
|
||
JAVA_STAGED_DIR=
|
||
JAVA_PREVIOUS_DIR=
|
||
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=()
|
||
|
||
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}'
|
||
}
|
||
|
||
normalized_host_arch() {
|
||
local machine host_bits
|
||
machine=$(uname -m)
|
||
host_bits=$(getconf LONG_BIT 2>/dev/null || true)
|
||
case "$machine" in
|
||
x86_64|amd64)
|
||
[ "$host_bits" = 32 ] && printf x86 || printf x86_64
|
||
;;
|
||
i386|i486|i586|i686) printf x86 ;;
|
||
aarch64|arm64)
|
||
[ "$host_bits" = 32 ] && printf arm || printf aarch64
|
||
;;
|
||
armv7l|armv6l) printf arm ;;
|
||
*) die "Unsupported CPU architecture: $machine" ;;
|
||
esac
|
||
}
|
||
|
||
preflight_host() {
|
||
local host_bits expected_bits
|
||
[ "$(uname -s)" = Linux ] || die "The installer only supports Linux"
|
||
[ "$APP_ROOT" = /opt/kaidi ] && [ "$STATE_ROOT" = /var/lib/kaidi ] \
|
||
&& [ "$UPDATE_STATE_ROOT" = /var/lib/kaidi-update ] && [ "$CONFIG_ROOT" = /etc/kaidi ] \
|
||
|| die "Custom installation roots are not supported by the packaged systemd configuration"
|
||
command -v systemctl >/dev/null 2>&1 || die "systemd is required"
|
||
command -v systemd-analyze >/dev/null 2>&1 || die "systemd-analyze is required"
|
||
command -v getconf >/dev/null 2>&1 || die "getconf is required"
|
||
[ -d /run/systemd/system ] || die "systemd is not running as PID 1"
|
||
[ -d /etc/systemd/system ] || die "/etc/systemd/system is missing"
|
||
HOST_ARCH=$(normalized_host_arch)
|
||
host_bits=$(getconf LONG_BIT 2>/dev/null || true)
|
||
case "$HOST_ARCH" in
|
||
x86_64|aarch64) expected_bits=64 ;;
|
||
x86|arm) expected_bits=32 ;;
|
||
esac
|
||
[ "$host_bits" = "$expected_bits" ] \
|
||
|| die "CPU architecture and userspace word size do not match: $HOST_ARCH/$host_bits-bit"
|
||
if [ "$HOST_ARCH" = x86 ]; then
|
||
getconf GNU_LIBC_VERSION >/dev/null 2>&1 \
|
||
|| die "32-bit Linux deployment requires glibc; the downloaded i686 Java runtime is not compatible with musl"
|
||
if ! for loader in /lib/ld-linux.so.2 /lib32/ld-linux.so.2 /lib/i386-linux-gnu/ld-linux.so.2; do
|
||
[ -x "$loader" ] && break
|
||
done; then
|
||
die "32-bit Linux deployment requires the glibc loader /lib/ld-linux.so.2"
|
||
fi
|
||
fi
|
||
[[ "$TRUSTED_PUBLIC_KEY_SHA256" =~ ^[0-9A-Fa-f]{64}$ ]] \
|
||
|| die "Set KAIDI_RELEASE_PUBLIC_KEY_SHA256 to the trusted release public-key SHA-256"
|
||
case "$REINSTALL" in
|
||
true|false) ;;
|
||
*) die "KAIDI_REINSTALL must be true or false" ;;
|
||
esac
|
||
case "$SETUP_WIZARD" in
|
||
true|false) ;;
|
||
*) die "KAIDI_SETUP_WIZARD must be true or false" ;;
|
||
esac
|
||
if [ "$SETUP_WIZARD" = true ] && [ "$REINSTALL" = true ]; then
|
||
[ -s "$CONFIG_ROOT/kaidi.env" ] \
|
||
|| die "A setup-wizard repair needs the existing $CONFIG_ROOT/kaidi.env"
|
||
[ "$(read_existing_env FINANCE_SETUP_ENABLED)" = true ] \
|
||
|| die "The existing installation is already in production mode; use a normal repair reinstall"
|
||
[ ! -e "$STATE_ROOT/setup/locked" ] \
|
||
|| die "The setup wizard is already locked; use a normal repair reinstall"
|
||
fi
|
||
log "Environment verified: $(uname -sr), architecture=$HOST_ARCH, userspace=${host_bits}-bit, systemd"
|
||
}
|
||
|
||
validate_inputs() {
|
||
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
|
||
[ -z "$RELEASE_TOKEN" ] || [ -z "$RELEASE_TOKEN_FILE" ] \
|
||
|| die "Set only one of KAIDI_RELEASE_TOKEN or KAIDI_RELEASE_TOKEN_FILE"
|
||
if [ -n "$RELEASE_TOKEN_FILE" ]; then
|
||
[ -f "$RELEASE_TOKEN_FILE" ] && [ ! -L "$RELEASE_TOKEN_FILE" ] \
|
||
|| die "KAIDI_RELEASE_TOKEN_FILE must be a regular file"
|
||
[ "$(wc -c < "$RELEASE_TOKEN_FILE" | tr -d '[:space:]')" -le 512 ] \
|
||
|| die "KAIDI_RELEASE_TOKEN_FILE is too large"
|
||
RELEASE_TOKEN=$(cat "$RELEASE_TOKEN_FILE")
|
||
fi
|
||
[ "$REINSTALL" = "true" ] || [ ! -e "$APP_ROOT/current" ] \
|
||
|| die "Kaidi Finance is already installed; use the system update page"
|
||
if [ -z "$RELEASE_API_URL" ]; then
|
||
case "$RELEASE_BASE_URL" in
|
||
*OWNER/REPO*) die "Set KAIDI_RELEASE_BASE_URL or KAIDI_RELEASE_API_URL to the Git release source" ;;
|
||
esac
|
||
fi
|
||
[ -z "$RELEASE_TOKEN" ] || { [ "${#RELEASE_TOKEN}" -le 512 ] \
|
||
&& ! printf '%s' "$RELEASE_TOKEN" | grep -q '[[:cntrl:]]'; } \
|
||
|| die "KAIDI_RELEASE_TOKEN is invalid"
|
||
}
|
||
|
||
download() {
|
||
local url=$1 output=$2
|
||
case "$url" in
|
||
https://*)
|
||
if ! curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
|
||
--tlsv1.2 --connect-timeout 15 --max-time 600 "$url" -o "$output"; then
|
||
die "Download failed: $url"
|
||
fi
|
||
;;
|
||
*)
|
||
[ "${KAIDI_ALLOW_INSECURE_UPDATE:-false}" = "true" ] || die "Release URLs must use HTTPS"
|
||
if ! curl --fail --silent --show-error --location --proto '=http,https' \
|
||
--proto-redir '=http,https' --connect-timeout 15 --max-time 600 "$url" -o "$output"; then
|
||
die "Download failed: $url"
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
download_release_url() {
|
||
local url=$1 output=$2
|
||
case "$url" in
|
||
https://*)
|
||
if [ -n "$RELEASE_TOKEN" ]; then
|
||
[ -s "$RELEASE_AUTH_HEADER_FILE" ] || die "Release authentication header is unavailable"
|
||
curl --fail --silent --show-error --proto '=https' --tlsv1.2 \
|
||
--header "@$RELEASE_AUTH_HEADER_FILE" "$url" -o "$output"
|
||
else
|
||
download "$url" "$output"
|
||
fi
|
||
;;
|
||
*)
|
||
[ "${KAIDI_ALLOW_INSECURE_UPDATE:-false}" = "true" ] || die "Release URLs must use HTTPS"
|
||
if [ -n "$RELEASE_TOKEN" ]; then
|
||
[ -s "$RELEASE_AUTH_HEADER_FILE" ] || die "Release authentication header is unavailable"
|
||
curl --fail --silent --show-error --proto '=http,https' \
|
||
--header "@$RELEASE_AUTH_HEADER_FILE" "$url" -o "$output"
|
||
else
|
||
download "$url" "$output"
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
release_asset_url() {
|
||
local asset_name=$1 api_origin repo_path release_tag encoded_tag encoded_asset
|
||
if [ -n "$RELEASE_API_URL" ]; then
|
||
jq -e --arg name "$asset_name" 'any(.assets[]?; .name == $name)' \
|
||
"$WORK_DIR/release-api.json" >/dev/null || die "Release asset $asset_name is missing"
|
||
release_tag=$(jq -er '.tag_name | strings
|
||
| select(test("^v[0-9A-Za-z][0-9A-Za-z._+-]{0,127}$"))' \
|
||
"$WORK_DIR/release-api.json") || die "Gitea Release tag is invalid"
|
||
api_origin=$(printf '%s' "$RELEASE_API_URL" | sed -E 's#^(https?://[^/]+).*$#\1#')
|
||
repo_path=$(printf '%s' "$RELEASE_API_URL" | sed -nE \
|
||
's#^https?://[^/]+/api/v1/repos/([A-Za-z0-9._-]+/[A-Za-z0-9._-]+)/releases/(latest|tags/[^/?#]+)$#\1#p')
|
||
[ -n "$repo_path" ] || die "KAIDI_RELEASE_API_URL must be a Gitea repository Release API endpoint"
|
||
encoded_tag=$(jq -rn --arg value "$release_tag" '$value | @uri')
|
||
encoded_asset=$(jq -rn --arg value "$asset_name" '$value | @uri')
|
||
printf '%s/%s/releases/download/%s/%s\n' "$api_origin" "$repo_path" "$encoded_tag" "$encoded_asset"
|
||
else
|
||
printf '%s/%s\n' "${RELEASE_BASE_URL%/}" "$asset_name"
|
||
fi
|
||
}
|
||
|
||
download_release_asset() {
|
||
local asset_name=$1 output=$2 url
|
||
url=$(release_asset_url "$asset_name")
|
||
download_release_url "$url" "$output"
|
||
}
|
||
|
||
install_packages() {
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
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 \
|
||
>/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 \
|
||
>/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() {
|
||
local command_name
|
||
for command_name in \
|
||
awk bash chmod chown cp curl cut date find getconf getent grep gzip head id install jq journalctl \
|
||
ln mktemp mv nologin openssl readlink rm runuser sed sha256sum sleep sort systemctl \
|
||
systemd-analyze tar tr useradd usermod groupadd wc flock setsid; do
|
||
command -v "$command_name" >/dev/null 2>&1 \
|
||
|| die "Required command is missing after dependency preparation: $command_name"
|
||
done
|
||
}
|
||
|
||
azul_arch() {
|
||
case "${HOST_ARCH:-$(normalized_host_arch)}" in
|
||
x86_64) printf x86 ;;
|
||
x86) printf i686 ;;
|
||
aarch64) printf arm_64 ;;
|
||
arm) printf arm ;;
|
||
esac
|
||
}
|
||
|
||
java_arch_matches_host() {
|
||
local java_bin=$1 java_arch host_arch
|
||
host_arch=${HOST_ARCH:-$(normalized_host_arch)}
|
||
java_arch=$(
|
||
"$java_bin" -XshowSettings:properties -version 2>&1 \
|
||
| sed -n 's/^[[:space:]]*os\.arch = [[:space:]]*//p' \
|
||
| head -n 1
|
||
)
|
||
case "$host_arch:$java_arch" in
|
||
x86_64:amd64|x86_64:x86_64|x86:x86|x86:i386|x86:i486|x86:i586|x86:i686|\
|
||
aarch64:aarch64|aarch64:arm64|arm:arm) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
java_home_from_bin() {
|
||
local java_bin=$1 resolved_java java_major home
|
||
[ -x "$java_bin" ] || return 1
|
||
java_major=$(
|
||
"$java_bin" -version 2>&1 \
|
||
| sed -n 's/.*version "\([0-9][0-9]*\).*/\1/p' \
|
||
| head -n 1
|
||
)
|
||
[[ "$java_major" =~ ^[0-9]+$ ]] && [ "$java_major" -ge 17 ] || return 1
|
||
java_arch_matches_host "$java_bin" || return 1
|
||
resolved_java=$(readlink -f "$java_bin" 2>/dev/null || printf '%s' "$java_bin")
|
||
case "$resolved_java" in
|
||
*/bin/java) home=${resolved_java%/bin/java} ;;
|
||
*) return 1 ;;
|
||
esac
|
||
[ -x "$home/bin/java" ] || return 1
|
||
printf '%s\n' "$home"
|
||
}
|
||
|
||
system_java_home() {
|
||
local candidate home
|
||
if [ -n "${KAIDI_JAVA_HOME:-}" ]; then
|
||
candidate="${KAIDI_JAVA_HOME%/}/bin/java"
|
||
java_home_from_bin "$candidate" && return 0
|
||
return 1
|
||
fi
|
||
candidate=$(command -v java 2>/dev/null || true)
|
||
if [ -n "$candidate" ] && home=$(java_home_from_bin "$candidate"); then
|
||
printf '%s\n' "$home"
|
||
return 0
|
||
fi
|
||
# Baota commonly keeps JDKs outside PATH. Prefer an existing Java 17 there
|
||
# before consulting the remote Azul metadata endpoint.
|
||
for candidate in \
|
||
/www/server/java/*/bin/java \
|
||
/www/server/java/*/jre/bin/java \
|
||
/usr/lib/jvm/*/bin/java \
|
||
/usr/java/*/bin/java; do
|
||
[ -x "$candidate" ] || continue
|
||
if home=$(java_home_from_bin "$candidate"); then
|
||
printf '%s\n' "$home"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
prepare_java() {
|
||
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"
|
||
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"
|
||
api="https://api.azul.com/metadata/v1/zulu/packages/?java_version=17&os=linux&arch=$(azul_arch)&archive_type=tar.gz&java_package_type=jre&release_status=ga&availability_types=CA&latest=true"
|
||
java_metadata="$WORK_DIR/zulu-metadata.json"
|
||
download "$api" "$java_metadata"
|
||
package_uuid=$(jq -er '[.[] | select(.name | contains("-fx-") | not)][0].package_uuid' \
|
||
"$java_metadata") \
|
||
|| die "No Java 17 runtime is published for this Linux architecture"
|
||
package_metadata="$WORK_DIR/zulu-package.json"
|
||
download "https://api.azul.com/metadata/v1/zulu/packages/$package_uuid" "$package_metadata"
|
||
java_url=$(jq -er '.download_url | strings | select(startswith("https://cdn.azul.com/"))' \
|
||
"$package_metadata") || die "Java 17 runtime download URL is invalid"
|
||
java_sha256=$(jq -er '.sha256_hash | strings | ascii_downcase | select(test("^[0-9a-f]{64}$"))' \
|
||
"$package_metadata") || die "Java 17 runtime SHA-256 is unavailable"
|
||
JAVA_STAGED_DIR="$WORK_DIR/java.next"
|
||
download "$java_url" "$WORK_DIR/java.tar.gz"
|
||
actual_sha256=$(sha256_file "$WORK_DIR/java.tar.gz")
|
||
[ "$actual_sha256" = "$java_sha256" ] || die "Java 17 runtime SHA-256 verification failed"
|
||
mkdir -p "$JAVA_STAGED_DIR"
|
||
tar -xzf "$WORK_DIR/java.tar.gz" --strip-components=1 -C "$JAVA_STAGED_DIR"
|
||
java_arch_matches_host "$JAVA_STAGED_DIR/bin/java" \
|
||
|| die "Downloaded Java runtime architecture does not match $HOST_ARCH"
|
||
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"
|
||
java_version=$(printf '%s\n' "$java_line" | sed -n 's/.*version "\([0-9][0-9.]*\).*/\1/p')
|
||
log "下载的 Java 运行时校验通过(版本 ${java_version:-未知})"
|
||
}
|
||
|
||
activate_java() {
|
||
[ -n "$JAVA_STAGED_DIR" ] || return 0
|
||
mkdir -p "$APP_ROOT/runtime"
|
||
JAVA_PREVIOUS_DIR="$WORK_DIR/java.previous"
|
||
if [ -e "$APP_ROOT/runtime/java" ] || [ -L "$APP_ROOT/runtime/java" ]; then
|
||
mv "$APP_ROOT/runtime/java" "$JAVA_PREVIOUS_DIR"
|
||
fi
|
||
mv "$JAVA_STAGED_DIR" "$APP_ROOT/runtime/java"
|
||
chown -R root:"$SERVICE_GROUP" "$APP_ROOT/runtime/java"
|
||
chmod -R u=rwX,g=rX,o= "$APP_ROOT/runtime/java"
|
||
JAVA_ACTIVATED=true
|
||
}
|
||
|
||
ensure_service_identity() {
|
||
local existing_home nologin_path
|
||
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}')
|
||
[ "$existing_home" = "$STATE_ROOT" ] \
|
||
|| die "Existing user $SERVICE_USER has unexpected home directory $existing_home"
|
||
if ! id -nG "$SERVICE_USER" | tr ' ' '\n' | grep -Fxq "$SERVICE_GROUP"; then
|
||
usermod --append --groups "$SERVICE_GROUP" "$SERVICE_USER"
|
||
fi
|
||
return 0
|
||
fi
|
||
nologin_path=$(command -v nologin 2>/dev/null || true)
|
||
[ -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 \
|
||
"$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" /var/log/kaidi
|
||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0700 "$STATE_ROOT/setup"
|
||
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT"
|
||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT/inbox"
|
||
install -d -o root -g root -m 0700 "$CONFIG_ROOT"
|
||
}
|
||
|
||
secure_release_tree() {
|
||
local release_dir=$1
|
||
chown -R root:"$SERVICE_GROUP" "$release_dir"
|
||
find "$release_dir" -type d -exec chmod 0750 {} +
|
||
find "$release_dir" -type f -exec chmod 0640 {} +
|
||
chmod 0750 "$release_dir/ops/update.sh" "$release_dir/ops/baota-start.sh"
|
||
}
|
||
|
||
restore_security_contexts() {
|
||
command -v restorecon >/dev/null 2>&1 || return 0
|
||
restorecon -RF "$APP_ROOT" "$STATE_ROOT" "$UPDATE_STATE_ROOT" /var/log/kaidi \
|
||
/etc/systemd/system/kaidi-finance.service \
|
||
/etc/systemd/system/kaidi-update.service \
|
||
/etc/systemd/system/kaidi-update.path >/dev/null 2>&1 \
|
||
|| log "SELinux context restoration reported a warning; service access checks will decide whether installation can continue"
|
||
}
|
||
|
||
verify_service_access() {
|
||
# shellcheck disable=SC2016 # Positional parameters are expanded by the child shell.
|
||
runuser -u "$SERVICE_USER" -- sh -c \
|
||
'cd "$1" && test -r app.jar && test -r public/index.html' sh "$APP_ROOT/current" \
|
||
|| die "$SERVICE_USER cannot traverse or read the active release"
|
||
[ -n "$JAVA_BIN" ] && [ -x "$JAVA_BIN" ] \
|
||
|| die "The selected Java runtime is unavailable at $JAVA_BIN"
|
||
runuser -u "$SERVICE_USER" -- test -x "$JAVA_BIN" \
|
||
|| die "$SERVICE_USER cannot execute the selected Java runtime"
|
||
runuser -u "$SERVICE_USER" -- "$JAVA_BIN" -version >/dev/null 2>&1 \
|
||
|| die "The selected Java runtime cannot execute as $SERVICE_USER"
|
||
runuser -u "$SERVICE_USER" -- test -w "$STATE_ROOT/files" \
|
||
|| die "$SERVICE_USER cannot write the file-storage directory"
|
||
runuser -u "$SERVICE_USER" -- test -w "$UPDATE_STATE_ROOT/inbox" \
|
||
|| die "$SERVICE_USER cannot write the update inbox"
|
||
log "Service-user filesystem and Java access checks passed"
|
||
}
|
||
|
||
random_secret() { openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-36; }
|
||
|
||
decode_env_value() {
|
||
local raw=$1 result='' char next index=0 length
|
||
if [[ "$raw" == \"*\" && ${#raw} -ge 2 ]]; then
|
||
raw=${raw:1:${#raw}-2}
|
||
length=${#raw}
|
||
while [ "$index" -lt "$length" ]; do
|
||
char=${raw:index:1}
|
||
if [ "$char" = "\\" ] && [ $((index + 1)) -lt "$length" ]; then
|
||
next=${raw:index+1:1}
|
||
if [ "$next" = "\\" ] || [ "$next" = '"' ]; then
|
||
result+="$next"
|
||
index=$((index + 2))
|
||
continue
|
||
fi
|
||
fi
|
||
result+="$char"
|
||
index=$((index + 1))
|
||
done
|
||
printf '%s' "$result"
|
||
elif [[ "$raw" == \'*\' && ${#raw} -ge 2 ]]; then
|
||
printf '%s' "${raw:1:${#raw}-2}"
|
||
else
|
||
printf '%s' "$raw"
|
||
fi
|
||
}
|
||
|
||
read_existing_env() {
|
||
read_env_file "$CONFIG_ROOT/kaidi.env" "$1"
|
||
}
|
||
|
||
read_env_file() {
|
||
local file=$1 name=$2 line value='' found=false
|
||
[ -s "$file" ] || return 0
|
||
while IFS= read -r line || [ -n "$line" ]; do
|
||
case "$line" in
|
||
"$name="*)
|
||
value=${line#*=}
|
||
found=true
|
||
;;
|
||
esac
|
||
done < "$file"
|
||
if [ "$found" = true ]; then
|
||
decode_env_value "$value"
|
||
fi
|
||
}
|
||
|
||
read_setup_env() {
|
||
read_env_file "$STATE_ROOT/setup/application.env" "$1"
|
||
}
|
||
|
||
read_reinstall_env() {
|
||
local value
|
||
value=$(read_setup_env "$1")
|
||
if [ -n "$value" ]; then
|
||
printf '%s' "$value"
|
||
else
|
||
read_existing_env "$1"
|
||
fi
|
||
}
|
||
|
||
port_is_listening() {
|
||
local port_hex files=(/proc/net/tcp)
|
||
port_hex=$(printf '%04X' "$APP_PORT")
|
||
[ -r /proc/net/tcp ] || return 1
|
||
[ ! -r /proc/net/tcp6 ] || files+=(/proc/net/tcp6)
|
||
awk -v port="$port_hex" '
|
||
toupper($2) ~ (":" port "$") && $4 == "0A" { found=1 }
|
||
END { exit(found ? 0 : 1) }
|
||
' "${files[@]}" 2>/dev/null
|
||
}
|
||
|
||
valid_app_port() {
|
||
[[ "$1" =~ ^[0-9]{1,5}$ ]] && [ "$1" -ge 1024 ] && [ "$1" -le 65535 ]
|
||
}
|
||
|
||
configure_app_port() {
|
||
local default_port=18080 existing_port entered probe_host probe_authority
|
||
if [ -z "$APP_PORT" ]; then
|
||
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] 应用端口 [%s]:' "$default_port" > /dev/tty
|
||
if IFS= read -r entered < /dev/tty; then
|
||
APP_PORT=${entered:-$default_port}
|
||
else
|
||
APP_PORT=$default_port
|
||
fi
|
||
else
|
||
APP_PORT=$default_port
|
||
log "No interactive terminal detected; using application port $APP_PORT"
|
||
fi
|
||
fi
|
||
valid_app_port "$APP_PORT" \
|
||
|| die "KAIDI_APP_PORT must be an integer between 1024 and 65535"
|
||
case "$SERVER_ADDRESS" in
|
||
''|*[!A-Za-z0-9_.:-]*) die "KAIDI_SERVER_ADDRESS contains unsupported characters" ;;
|
||
esac
|
||
if port_is_listening; then
|
||
existing_port=$(read_existing_env SERVER_PORT || true)
|
||
if [ "$REINSTALL" = true ] && [ "$existing_port" = "$APP_PORT" ] \
|
||
&& systemctl is-active --quiet kaidi-finance.service; then
|
||
log "Application port $APP_PORT is already held by the existing Kaidi service"
|
||
else
|
||
die "Application port $APP_PORT is already in use; choose another port with KAIDI_APP_PORT"
|
||
fi
|
||
fi
|
||
probe_host=${KAIDI_HEALTH_HOST:-$SERVER_ADDRESS}
|
||
case "$probe_host" in
|
||
0.0.0.0) probe_host=127.0.0.1 ;;
|
||
::) probe_host=::1 ;;
|
||
esac
|
||
case "$probe_host" in
|
||
*:*) probe_authority="[$probe_host]" ;;
|
||
*) probe_authority="$probe_host" ;;
|
||
esac
|
||
if [ -z "$HEALTH_URL" ]; then
|
||
HEALTH_URL="http://${probe_authority}:${APP_PORT}/actuator/health"
|
||
fi
|
||
if [ -z "$APP_INDEX_URL" ]; then
|
||
APP_INDEX_URL="http://${probe_authority}:${APP_PORT}/"
|
||
fi
|
||
PROXY_TARGET="http://${probe_authority}:${APP_PORT}"
|
||
log "Application will bind ${SERVER_ADDRESS}:${APP_PORT}"
|
||
log "Reverse proxy target: $PROXY_TARGET"
|
||
}
|
||
|
||
is_semver() {
|
||
[ "${#1}" -le 128 ] \
|
||
&& LC_ALL=C grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' \
|
||
<<EOF
|
||
$1
|
||
EOF
|
||
}
|
||
|
||
configure_database() {
|
||
if [ "$SETUP_WIZARD" = true ]; then
|
||
[ -z "${KAIDI_DB_URL:-}${KAIDI_DB_USERNAME:-}${KAIDI_DB_PASSWORD:-}" ] \
|
||
|| die "Do not pass database credentials when KAIDI_SETUP_WIZARD=true; enter them in the browser wizard"
|
||
# First-run mode does not create a datasource. Keep database fields empty
|
||
# so hosting panels cannot mistake a placeholder for a local MySQL service.
|
||
DB_URL=
|
||
DB_USERNAME=
|
||
DB_PASSWORD=
|
||
return 0
|
||
fi
|
||
if [ "$REINSTALL" = true ]; then
|
||
[ -s "$CONFIG_ROOT/kaidi.env" ] || die "KAIDI_REINSTALL needs the existing $CONFIG_ROOT/kaidi.env"
|
||
DB_USERNAME=${KAIDI_DB_USERNAME:-$(read_reinstall_env DB_USERNAME)}
|
||
DB_PASSWORD=${KAIDI_DB_PASSWORD:-$(read_reinstall_env DB_PASSWORD)}
|
||
DB_URL=${KAIDI_DB_URL:-$(read_reinstall_env DB_URL)}
|
||
else
|
||
DB_USERNAME=${KAIDI_DB_USERNAME:-}
|
||
DB_PASSWORD=${KAIDI_DB_PASSWORD:-}
|
||
DB_URL=${KAIDI_DB_URL:-}
|
||
fi
|
||
if [ -n "$DB_URL" ]; then
|
||
[ -n "$DB_USERNAME" ] && [ -n "$DB_PASSWORD" ] \
|
||
|| die "KAIDI_DB_USERNAME and KAIDI_DB_PASSWORD are required with KAIDI_DB_URL"
|
||
return 0
|
||
fi
|
||
[ "$REINSTALL" != true ] \
|
||
|| die "KAIDI_REINSTALL needs database credentials in the existing installation or command environment"
|
||
die "An external MySQL 8.4 database is required; set KAIDI_DB_URL, KAIDI_DB_USERNAME, and KAIDI_DB_PASSWORD"
|
||
}
|
||
|
||
database_host() {
|
||
local endpoint host_port
|
||
[ -n "$DB_URL" ] || { printf ''; return; }
|
||
endpoint=${DB_URL#jdbc:mysql://}
|
||
host_port=${endpoint%%/*}
|
||
printf '%s' "${KAIDI_DB_HOST:-${host_port%%:*}}"
|
||
}
|
||
|
||
database_port() {
|
||
local endpoint host_port candidate
|
||
[ -n "$DB_URL" ] || { printf ''; return; }
|
||
endpoint=${DB_URL#jdbc:mysql://}
|
||
host_port=${endpoint%%/*}
|
||
candidate=${host_port##*:}
|
||
[ "$candidate" != "$host_port" ] || candidate=3306
|
||
printf '%s' "${KAIDI_DB_PORT:-$candidate}"
|
||
}
|
||
|
||
database_name() {
|
||
local endpoint name
|
||
[ -n "$DB_URL" ] || { printf ''; return; }
|
||
endpoint=${DB_URL#jdbc:mysql://}
|
||
name=${endpoint#*/}
|
||
name=${name%%\?*}
|
||
printf '%s' "${KAIDI_DB_NAME:-$name}"
|
||
}
|
||
|
||
validate_database_configuration() {
|
||
[ "$SETUP_WIZARD" != true ] || return 0
|
||
case "$DB_URL" in
|
||
jdbc:mysql://*) ;;
|
||
*) die "KAIDI_DB_URL must start with jdbc:mysql://" ;;
|
||
esac
|
||
[ -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"
|
||
[[ "$(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"
|
||
}
|
||
|
||
# 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
|
||
}
|
||
|
||
write_env_file() {
|
||
local output=$1 temporary name value escaped
|
||
shift
|
||
temporary="${output}.next.$$"
|
||
: > "$temporary"
|
||
while [ "$#" -gt 0 ]; do
|
||
name=$1
|
||
value=$2
|
||
shift 2
|
||
case "$value" in
|
||
*$'\n'*|*$'\r'*) die "$name contains a line break" ;;
|
||
esac
|
||
escaped=${value//\\/\\\\}
|
||
escaped=${escaped//\"/\\\"}
|
||
printf '%s="%s"\n' "$name" "$escaped" >> "$temporary"
|
||
done
|
||
mv -f "$temporary" "$output"
|
||
}
|
||
|
||
write_env_file_preserving_unknown() {
|
||
local output=$1 existing='' temporary name value escaped line key managed_keys='|'
|
||
shift
|
||
if [ "$REINSTALL" = true ] && [ -f "$output" ]; then
|
||
existing="$WORK_DIR/existing-kaidi.env"
|
||
cp -p "$output" "$existing"
|
||
fi
|
||
local arguments=("$@")
|
||
while [ "$#" -gt 0 ]; do
|
||
managed_keys+="$1|"
|
||
shift 2
|
||
done
|
||
temporary="${output}.next.$$"
|
||
: > "$temporary"
|
||
if [ -n "$existing" ]; then
|
||
while IFS= read -r line || [ -n "$line" ]; do
|
||
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]]; then
|
||
key=${BASH_REMATCH[1]}
|
||
case "$managed_keys" in
|
||
*"|$key|"*) continue ;;
|
||
esac
|
||
fi
|
||
printf '%s\n' "$line" >> "$temporary"
|
||
done < "$existing"
|
||
fi
|
||
set -- "${arguments[@]}"
|
||
while [ "$#" -gt 0 ]; do
|
||
name=$1
|
||
value=$2
|
||
shift 2
|
||
case "$value" in
|
||
*$'\n'*|*$'\r'*) die "$name contains a line break" ;;
|
||
esac
|
||
escaped=${value//\\/\\\\}
|
||
escaped=${escaped//\"/\\\"}
|
||
printf '%s="%s"\n' "$name" "$escaped" >> "$temporary"
|
||
done
|
||
mv -f "$temporary" "$output"
|
||
}
|
||
|
||
wait_for_health() {
|
||
local attempts=0 active_state restart_count
|
||
log "Waiting for application startup at $HEALTH_URL"
|
||
while [ "$attempts" -lt 60 ]; do
|
||
if curl --fail --silent --connect-timeout 1 --max-time 2 "$HEALTH_URL" 2>/dev/null \
|
||
| jq -e '.status == "UP"' >/dev/null 2>&1; then
|
||
log "Application health check is UP"
|
||
return 0
|
||
fi
|
||
active_state=$(systemctl show kaidi-finance.service --property=ActiveState --value 2>/dev/null || printf unknown)
|
||
restart_count=$(systemctl show kaidi-finance.service --property=NRestarts --value 2>/dev/null || printf 0)
|
||
[[ "$restart_count" =~ ^[0-9]+$ ]] || restart_count=0
|
||
if [ "$active_state" = failed ] || [ "$restart_count" -ge 3 ]; then
|
||
journalctl -u kaidi-finance.service -n 80 --no-pager >&2 || true
|
||
die "Application service failed during startup (state=$active_state, restarts=$restart_count)"
|
||
fi
|
||
attempts=$((attempts + 1))
|
||
if [ $((attempts % 5)) -eq 0 ]; then
|
||
log "Application is still starting (state=$active_state, elapsed=$((attempts * 2))s)"
|
||
fi
|
||
sleep 2
|
||
done
|
||
journalctl -u kaidi-finance.service -n 80 --no-pager >&2 || true
|
||
die "Application health check did not become UP"
|
||
}
|
||
|
||
backup_managed_state() {
|
||
local index=0 path
|
||
MANAGED_PATHS=(
|
||
"$APP_ROOT/current"
|
||
"$APP_ROOT/bin/update.sh"
|
||
"$CONFIG_ROOT/kaidi.env"
|
||
"$CONFIG_ROOT/update.env"
|
||
"$CONFIG_ROOT/release-public.pem"
|
||
"$UPDATE_STATE_ROOT/status.json"
|
||
/root/kaidi-first-login.txt
|
||
/etc/systemd/system/kaidi-finance.service
|
||
/etc/systemd/system/kaidi-update.service
|
||
/etc/systemd/system/kaidi-update.path
|
||
)
|
||
BACKUP_DIR="$WORK_DIR/rollback"
|
||
mkdir -p "$BACKUP_DIR/files"
|
||
for path in "${MANAGED_PATHS[@]}"; do
|
||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||
cp -a -- "$path" "$BACKUP_DIR/files/$index"
|
||
: > "$BACKUP_DIR/files/$index.exists"
|
||
fi
|
||
index=$((index + 1))
|
||
done
|
||
systemctl is-active --quiet kaidi-finance.service >/dev/null 2>&1 \
|
||
&& PREVIOUS_APP_ACTIVE=true || PREVIOUS_APP_ACTIVE=false
|
||
systemctl is-enabled --quiet kaidi-finance.service >/dev/null 2>&1 \
|
||
&& PREVIOUS_APP_ENABLED=true || PREVIOUS_APP_ENABLED=false
|
||
systemctl is-active --quiet kaidi-update.path >/dev/null 2>&1 \
|
||
&& PREVIOUS_UPDATE_ACTIVE=true || PREVIOUS_UPDATE_ACTIVE=false
|
||
systemctl is-enabled --quiet kaidi-update.path >/dev/null 2>&1 \
|
||
&& PREVIOUS_UPDATE_ENABLED=true || PREVIOUS_UPDATE_ENABLED=false
|
||
INSTALL_TRANSACTION_ARMED=true
|
||
}
|
||
|
||
restore_unit_state() {
|
||
local unit=$1 was_enabled=$2 was_active=$3
|
||
if ! systemctl cat "$unit" >/dev/null 2>&1; then
|
||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||
[ "$was_enabled" = false ] && [ "$was_active" = false ]
|
||
return
|
||
fi
|
||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||
if [ "$was_enabled" = true ]; then
|
||
systemctl enable "$unit" >/dev/null 2>&1
|
||
else
|
||
systemctl disable "$unit" >/dev/null 2>&1
|
||
fi
|
||
if [ "$was_active" = true ]; then
|
||
systemctl start "$unit" >/dev/null 2>&1
|
||
else
|
||
systemctl stop "$unit" >/dev/null 2>&1
|
||
fi
|
||
}
|
||
|
||
restore_managed_path() {
|
||
local path=$1 index=$2 temporary
|
||
if [ -e "$BACKUP_DIR/files/$index.exists" ]; then
|
||
mkdir -p "$(dirname "$path")"
|
||
temporary="${path}.rollback.$$"
|
||
rm -rf -- "$temporary"
|
||
cp -a -- "$BACKUP_DIR/files/$index" "$temporary" || return 1
|
||
mv -Tf -- "$temporary" "$path" || {
|
||
rm -rf -- "$temporary"
|
||
return 1
|
||
}
|
||
else
|
||
rm -rf -- "$path"
|
||
fi
|
||
}
|
||
|
||
rollback_install() {
|
||
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
|
||
for path in "${MANAGED_PATHS[@]}"; do
|
||
restore_managed_path "$path" "$index" || rollback_failed=true
|
||
index=$((index + 1))
|
||
done
|
||
if [ -n "$RELEASE_DIR" ]; then
|
||
rm -rf -- "$RELEASE_DIR" || rollback_failed=true
|
||
fi
|
||
if [ -d "$JAVA_PREVIOUS_DIR" ] || [ -L "$JAVA_PREVIOUS_DIR" ]; then
|
||
rm -rf -- "$APP_ROOT/runtime/java" || rollback_failed=true
|
||
mkdir -p "$APP_ROOT/runtime" || rollback_failed=true
|
||
mv "$JAVA_PREVIOUS_DIR" "$APP_ROOT/runtime/java" || rollback_failed=true
|
||
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
|
||
restore_unit_state kaidi-update.path "$PREVIOUS_UPDATE_ENABLED" "$PREVIOUS_UPDATE_ACTIVE" \
|
||
|| rollback_failed=true
|
||
set -e
|
||
[ "$rollback_failed" = false ]
|
||
}
|
||
|
||
cleanup() {
|
||
local result=$?
|
||
trap - EXIT
|
||
if [ "$result" -ne 0 ] && [ "$INSTALL_TRANSACTION_ARMED" = true ] \
|
||
&& [ "$INSTALL_COMMITTED" != true ]; then
|
||
rollback_install || log "ERROR: rollback was incomplete; inspect the managed systemd state"
|
||
fi
|
||
[ -z "$WORK_DIR" ] || rm -rf "$WORK_DIR"
|
||
exit "$result"
|
||
}
|
||
|
||
main() {
|
||
trap cleanup EXIT
|
||
|
||
validate_inputs
|
||
validate_database_backup_mode
|
||
configure_app_port
|
||
preflight_host
|
||
install_packages
|
||
preflight_runtime_commands
|
||
|
||
BASE=${RELEASE_BASE_URL%/}
|
||
WORK_DIR=$(mktemp -d)
|
||
if [ -n "$RELEASE_TOKEN" ]; then
|
||
RELEASE_AUTH_HEADER_FILE="$WORK_DIR/release-auth-header"
|
||
printf 'Authorization: token %s\n' "$RELEASE_TOKEN" > "$RELEASE_AUTH_HEADER_FILE"
|
||
chmod 0600 "$RELEASE_AUTH_HEADER_FILE"
|
||
fi
|
||
if [ -n "$RELEASE_API_URL" ]; then
|
||
download_release_url "$RELEASE_API_URL" "$WORK_DIR/release-api.json"
|
||
fi
|
||
download_release_asset release-public.pem "$WORK_DIR/release-public.pem"
|
||
ACTUAL_PUBLIC_KEY_SHA256=$(sha256_file "$WORK_DIR/release-public.pem" | tr '[:upper:]' '[:lower:]')
|
||
EXPECTED_PUBLIC_KEY_SHA256=$(printf '%s' "$TRUSTED_PUBLIC_KEY_SHA256" | tr '[:upper:]' '[:lower:]')
|
||
[ "$ACTUAL_PUBLIC_KEY_SHA256" = "$EXPECTED_PUBLIC_KEY_SHA256" ] \
|
||
|| die "Release public-key SHA-256 does not match the trusted fingerprint"
|
||
download_release_asset release-manifest.json "$WORK_DIR/release-manifest.json"
|
||
download_release_asset release-manifest.sig "$WORK_DIR/release-manifest.sig"
|
||
openssl dgst -sha256 -verify "$WORK_DIR/release-public.pem" -signature "$WORK_DIR/release-manifest.sig" \
|
||
"$WORK_DIR/release-manifest.json" >/dev/null 2>&1 || die "Release manifest signature verification failed"
|
||
|
||
VERSION=$(jq -er '.version | strings | select(length > 0 and length <= 128)' \
|
||
"$WORK_DIR/release-manifest.json") || die "Release version is invalid"
|
||
is_semver "$VERSION" || die "Release version is invalid"
|
||
ARTIFACT=$(jq -er '.artifact | strings | select(test("^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}\\.tar\\.gz$"))' \
|
||
"$WORK_DIR/release-manifest.json") || die "Release artifact name is invalid"
|
||
EXPECTED_SHA=$(jq -er '.sha256 | strings | ascii_downcase | select(test("^[0-9a-f]{64}$"))' \
|
||
"$WORK_DIR/release-manifest.json") || die "Release SHA-256 is invalid"
|
||
download_release_asset "$ARTIFACT" "$WORK_DIR/release.tar.gz"
|
||
ACTUAL_SHA=$(sha256sum "$WORK_DIR/release.tar.gz" | awk '{print $1}')
|
||
[ "$ACTUAL_SHA" = "$EXPECTED_SHA" ] || die "Release artifact SHA-256 verification failed"
|
||
tar -tzf "$WORK_DIR/release.tar.gz" > "$WORK_DIR/archive.list"
|
||
grep -Eq '(^/|(^|/)\.\.(/|$))' "$WORK_DIR/archive.list" \
|
||
&& die "Release archive contains an unsafe path"
|
||
mkdir "$WORK_DIR/extracted"
|
||
tar -xzf "$WORK_DIR/release.tar.gz" -C "$WORK_DIR/extracted"
|
||
find "$WORK_DIR/extracted" -type l -print -quit | grep -q . \
|
||
&& die "Release archive must not contain symbolic links"
|
||
[ -s "$WORK_DIR/extracted/app.jar" ] || die "Release app.jar is missing"
|
||
[ -s "$WORK_DIR/extracted/public/index.html" ] || die "Release frontend is missing"
|
||
[ "$(cat "$WORK_DIR/extracted/VERSION")" = "$VERSION" ] || die "Release version mismatch"
|
||
[ -x "$WORK_DIR/extracted/ops/update.sh" ] || die "Release updater is missing"
|
||
[ -x "$WORK_DIR/extracted/ops/baota-start.sh" ] || die "Baota Spring Boot launcher is missing"
|
||
bash -n "$WORK_DIR/extracted/ops/baota-start.sh" || die "Baota Spring Boot launcher is invalid"
|
||
|
||
configure_database
|
||
preflight_database
|
||
ensure_service_identity
|
||
prepare_managed_layout
|
||
prepare_java
|
||
|
||
RELEASE_DIR="$APP_ROOT/releases/$VERSION"
|
||
[ ! -e "$RELEASE_DIR" ] || RELEASE_DIR="$APP_ROOT/releases/${VERSION}-reinstall-$(date -u +%Y%m%dT%H%M%SZ)"
|
||
backup_managed_state
|
||
activate_java
|
||
mv "$WORK_DIR/extracted" "$RELEASE_DIR"
|
||
secure_release_tree "$RELEASE_DIR"
|
||
ln -sfn "$RELEASE_DIR" "$APP_ROOT/current.next"
|
||
mv -Tf "$APP_ROOT/current.next" "$APP_ROOT/current"
|
||
install -m 0755 "$RELEASE_DIR/ops/update.sh" "$APP_ROOT/bin/update.sh"
|
||
install -m 0644 "$WORK_DIR/release-public.pem" "$CONFIG_ROOT/release-public.pem"
|
||
|
||
if [ "$REINSTALL" = true ]; then
|
||
FIELD_KEY=$(read_existing_env FIELD_ENCRYPTION_KEY)
|
||
[ -n "$FIELD_KEY" ] || die "The existing FIELD_ENCRYPTION_KEY is missing"
|
||
ADMIN_PASSWORD=
|
||
SESSION_COOKIE_SECURE=${KAIDI_SESSION_COOKIE_SECURE:-$(read_existing_env SESSION_COOKIE_SECURE)}
|
||
FILE_STORAGE_ROOT_VALUE=$(read_existing_env FILE_STORAGE_ROOT)
|
||
FILE_STORAGE_TEMP_VALUE=$(read_existing_env FILE_STORAGE_TEMP)
|
||
FILE_SCANNER_ENABLED_VALUE=$(read_existing_env FILE_SCANNER_ENABLED)
|
||
[ -n "$SESSION_COOKIE_SECURE" ] || SESSION_COOKIE_SECURE=false
|
||
[ -n "$FILE_STORAGE_ROOT_VALUE" ] || FILE_STORAGE_ROOT_VALUE="$STATE_ROOT/files"
|
||
[ -n "$FILE_STORAGE_TEMP_VALUE" ] || FILE_STORAGE_TEMP_VALUE="$STATE_ROOT/tmp"
|
||
[ -n "$FILE_SCANNER_ENABLED_VALUE" ] || FILE_SCANNER_ENABLED_VALUE=false
|
||
else
|
||
FIELD_KEY=$(openssl rand -base64 32 | tr -d '\n')
|
||
if [ "$SETUP_WIZARD" = true ]; then
|
||
ADMIN_PASSWORD=
|
||
else
|
||
ADMIN_PASSWORD="Kaidi!$(random_secret)Aa1"
|
||
fi
|
||
SESSION_COOKIE_SECURE=${KAIDI_SESSION_COOKIE_SECURE:-false}
|
||
FILE_STORAGE_ROOT_VALUE="$STATE_ROOT/files"
|
||
FILE_STORAGE_TEMP_VALUE="$STATE_ROOT/tmp"
|
||
FILE_SCANNER_ENABLED_VALUE=${KAIDI_FILE_SCANNER_ENABLED:-false}
|
||
fi
|
||
BOOTSTRAP_ENABLED=true
|
||
SETUP_CODE=
|
||
SETUP_TOKEN_SHA256=
|
||
if [ "$REINSTALL" = true ] || [ "$SETUP_WIZARD" = true ]; then
|
||
BOOTSTRAP_ENABLED=false
|
||
fi
|
||
if [ "$SETUP_WIZARD" = true ]; then
|
||
SETUP_CODE="KD-$(random_secret)"
|
||
SETUP_TOKEN_SHA256=$(printf '%s' "$SETUP_CODE" | sha256sum | awk '{print $1}')
|
||
fi
|
||
write_env_file_preserving_unknown "$CONFIG_ROOT/kaidi.env" \
|
||
SPRING_PROFILES_ACTIVE production \
|
||
KAIDI_JAVA_BIN "$JAVA_BIN" \
|
||
SERVER_PORT "$APP_PORT" \
|
||
SERVER_ADDRESS "$SERVER_ADDRESS" \
|
||
SESSION_COOKIE_SECURE "$SESSION_COOKIE_SECURE" \
|
||
DB_URL "$DB_URL" \
|
||
DB_USERNAME "$DB_USERNAME" \
|
||
DB_PASSWORD "$DB_PASSWORD" \
|
||
FIELD_ENCRYPTION_KEY "$FIELD_KEY" \
|
||
FILE_STORAGE_ROOT "$FILE_STORAGE_ROOT_VALUE" \
|
||
FILE_STORAGE_TEMP "$FILE_STORAGE_TEMP_VALUE" \
|
||
FILE_SCANNER_ENABLED "$FILE_SCANNER_ENABLED_VALUE" \
|
||
FINANCE_BOOTSTRAP_ENABLED "$BOOTSTRAP_ENABLED" \
|
||
FINANCE_BOOTSTRAP_PASSWORD "$ADMIN_PASSWORD" \
|
||
FINANCE_SETUP_ENABLED "$SETUP_WIZARD" \
|
||
FINANCE_SETUP_TOKEN_SHA256 "$SETUP_TOKEN_SHA256" \
|
||
FINANCE_SETUP_ENV_FILE "$STATE_ROOT/setup/application.env" \
|
||
FINANCE_SETUP_MARKER_FILE "$STATE_ROOT/setup/locked" \
|
||
FINANCE_SETUP_RESTART_AFTER_COMPLETE true \
|
||
APP_VERSION "$VERSION" \
|
||
UPDATE_CURRENT_VERSION_FILE "$APP_ROOT/current/VERSION" \
|
||
FINANCE_UPDATE_ENABLED true \
|
||
UPDATE_RELEASE_BASE_URL "$([ -n "$RELEASE_API_URL" ] && printf '' || printf '%s/' "$BASE")" \
|
||
UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \
|
||
UPDATE_RELEASE_TOKEN "$RELEASE_TOKEN" \
|
||
UPDATE_REQUEST_FILE "$UPDATE_STATE_ROOT/inbox/request.json" \
|
||
UPDATE_STATUS_FILE "$UPDATE_STATE_ROOT/status.json"
|
||
chmod 0600 "$CONFIG_ROOT/kaidi.env"
|
||
|
||
write_env_file "$CONFIG_ROOT/update.env" \
|
||
UPDATE_RELEASE_BASE_URL "$([ -n "$RELEASE_API_URL" ] && printf '' || printf '%s/' "$BASE")" \
|
||
UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \
|
||
UPDATE_RELEASE_TOKEN "$RELEASE_TOKEN" \
|
||
UPDATE_REQUEST_FILE "$UPDATE_STATE_ROOT/inbox/request.json" \
|
||
UPDATE_STATUS_FILE "$UPDATE_STATE_ROOT/status.json" \
|
||
UPDATE_PUBLIC_KEY "$CONFIG_ROOT/release-public.pem" \
|
||
KAIDI_APP_ROOT "$APP_ROOT" \
|
||
KAIDI_UPDATE_STATE_ROOT "$UPDATE_STATE_ROOT" \
|
||
KAIDI_SERVICE_NAME kaidi-finance.service \
|
||
KAIDI_JAVA_BIN "$JAVA_BIN" \
|
||
KAIDI_HEALTH_URL "$HEALTH_URL" \
|
||
KAIDI_APP_INDEX_URL "$APP_INDEX_URL" \
|
||
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_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
|
||
install -m 0644 "$RELEASE_DIR/ops/kaidi-update.service" /etc/systemd/system/kaidi-update.service
|
||
install -m 0644 "$RELEASE_DIR/ops/kaidi-update.path" /etc/systemd/system/kaidi-update.path
|
||
restore_security_contexts
|
||
verify_service_access
|
||
systemd-analyze verify \
|
||
/etc/systemd/system/kaidi-finance.service \
|
||
/etc/systemd/system/kaidi-update.service \
|
||
/etc/systemd/system/kaidi-update.path >/dev/null \
|
||
|| die "Installed systemd units failed validation"
|
||
systemctl daemon-reload
|
||
systemctl reset-failed kaidi-finance.service >/dev/null 2>&1 || true
|
||
systemctl enable kaidi-finance.service
|
||
systemctl restart kaidi-finance.service
|
||
wait_for_health
|
||
curl -fsS "$APP_INDEX_URL" | grep -Eiq '<!doctype|<html' \
|
||
|| die "Application frontend entry point is unavailable"
|
||
|
||
if [ "$SETUP_WIZARD" != true ]; then
|
||
sed -i 's/^FINANCE_BOOTSTRAP_ENABLED="true"$/FINANCE_BOOTSTRAP_ENABLED="false"/' "$CONFIG_ROOT/kaidi.env"
|
||
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:"初始版本正在运行",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
|
||
反向代理访问地址: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
|
||
反向代理访问地址:http://SERVER_IP/
|
||
反向代理目标:$PROXY_TARGET
|
||
管理员账号:admin
|
||
临时密码:$ADMIN_PASSWORD
|
||
版本:$VERSION
|
||
EOF
|
||
chmod 0600 /root/kaidi-first-login.txt
|
||
fi
|
||
INSTALL_COMMITTED=true
|
||
INSTALL_TRANSACTION_ARMED=false
|
||
|
||
log "Kaidi Finance $VERSION is installed"
|
||
if [ "$SETUP_WIZARD" = true ]; then
|
||
log "Configure your reverse proxy to $PROXY_TARGET"
|
||
log "Open /setup through your reverse-proxy domain and complete the first-run wizard"
|
||
log "Setup code: /root/kaidi-first-login.txt"
|
||
else
|
||
log "Configure your reverse proxy to $PROXY_TARGET"
|
||
log "Open the reverse-proxy domain and sign in as admin"
|
||
fi
|
||
if [ "$REINSTALL" != true ] && [ "$SETUP_WIZARD" != true ]; then
|
||
log "Temporary credentials: /root/kaidi-first-login.txt"
|
||
log "Change the temporary password immediately after first sign-in"
|
||
fi
|
||
}
|
||
|
||
main "$@"
|