需求 1:模型上传支持压缩包(MMD 模型包) - common/zip-reader.ts:纯 TS 极简 ZIP 读取器(EOCD/中央目录、STORE+DEFLATE、 ZIP64 大小字段、Shift-JIS/GBK 文件名回退、拒绝 zip-slip 与加密条目) - models/model-convert.ts:解压 → 选主模型(指名字/最浅/最大)→ Assimp 导出 FBX → **assimp info 回读校验**(Assimp 对带蒙皮大骨架模型会写出损坏文件: 实测 714 骨骼 PMX 导出退出码 11、产物回读报 FBX-DOM no Objects dictionary found) → 失败自动回退 GLB;两种都失败则 422 - models/glb-textures.ts:把 Assimp 写成外部 URI 的贴图内嵌进 GLB 的 BIN chunk, 保证模型库里单文件即可加载(否则是白模) - 溯源:压缩包原件存为 model-archive 资产;models 新增 source_asset_id / conversion_note(迁移 1789450000000-AddModelConversionFields,已在真实库上 验证 up/down 可逆) - 默认模型:转换成功且 autoDefault 不为 false → is_primary=1 + published, 并把身份(名称/格式/产物 sha256)写进源码 src/config/default-model.ts; resolveDefaultModelId 增加 DEFAULT_MODEL_ID 与源码种子两步(按 sha256 匹配, 换库仍有效),实测库中有 2 个 VRM 时默认模型仍解析为该 GLB - scripts/import-default-model.mjs:运维 CLI(上传 zip 并写源码 / --write-source-only) - 修复 multipart 中文文件名乱码(upload.util.decodeUploadName) - DTO/契约新增 conversionNote、fileSha256;管理端 accept 增加 .zip 并展示转换来源 需求 2:首次启动 .env 引导 - deploy/init-env.sh:交互式逐项填写 8 个键(回车保留默认、口令不回显不加引号、 端口校验、mysql2 连通性自检且口令只经环境变量传递、只改这 8 个键并保留模板注释、 chmod 600、结尾打码摘要);--force/--check/-y/-i/--skip-db-check 等开关; 非交互(-y、OPENDANCE_NONINTERACTIVE、stdin 关闭)绝不提问、读取带超时 - install.sh 缺 .env 时改为调用引导,失败才 die 需求 3:默认监听 0.0.0.0 - 代码默认值、两份 .env 模板、install.sh、init-env.sh 五处一致,并新增测试守护 (未设置 HOST 时为 0.0.0.0;两份模板都写着 HOST=0.0.0.0) 契约:rest-api / data-models / model-formats → 0.3.0,database → 0.2.0(另两库已同步) 验证:125 项单测通过(7 文件);契约 6 项与 29 条路由一致;typecheck/build 通过; 真实包端到端 HTTP 201 / 0.89s / format=glb / isPrimary=true / published; GLB 1,152,464 字节、1 mesh / 240 nodes / 1 skin(238 joints) / 2 张内嵌贴图; 迁移 revert→run 实测可逆且数据无损恢复
648 lines
25 KiB
Bash
Executable File
648 lines
25 KiB
Bash
Executable File
#!/bin/bash
|
||
# ============================================================
|
||
# OpenDance Studio Web 服务端 —— deploy/.env 初始化引导
|
||
#
|
||
# 作用:把「cp deploy/.env.example deploy/.env && vi deploy/.env」这一步
|
||
# 变成可交互的问答,并在写盘前完成必填项 / 端口范围 / 数据库连通性自检。
|
||
#
|
||
# 用法:
|
||
# bash deploy/init-env.sh # 交互引导(终端里逐项询问)
|
||
# bash deploy/init-env.sh --force # 已有 .env 也重新逐项询问
|
||
# bash deploy/init-env.sh --check # 只校验,不改动、不提问
|
||
# bash deploy/init-env.sh --non-interactive # 非交互:只检查缺字段,绝不阻塞
|
||
#
|
||
# 选项:
|
||
# --check 只校验,不修改文件、不提问(缺失必填 / 端口非法时非 0 退出)
|
||
# --force 已有 .env 也重新询问一遍(默认:已有且完整则跳过询问)
|
||
# -y, --yes 非交互(等同 --non-interactive)
|
||
# --non-interactive CI / 自动化:跳过全部提问,仅做缺字段与端口校验
|
||
# -i, --interactive 即使 stdin 不是 TTY 也强制提问(读取超时仍然生效)
|
||
# --skip-db-check 跳过数据库连通性自检
|
||
# --dir DIR 以 DIR/.env 为目标、DIR/.env.example 为模板
|
||
# --env-file PATH 指定 .env 路径(默认 <脚本目录>/.env)
|
||
# --example PATH 指定模板路径(默认 .env 同目录的 .env.example)
|
||
# --timeout SECONDS 非 TTY 下每次读取的超时秒数(默认 10)
|
||
# -h, --help 显示本帮助
|
||
#
|
||
# 环境变量:
|
||
# OPENDANCE_NONINTERACTIVE=1 同 --non-interactive
|
||
# OPENDANCE_INTERACTIVE=1 同 --interactive
|
||
# OPENDANCE_SKIP_DB_CHECK=1 同 --skip-db-check
|
||
# OPENDANCE_ENV_FILE=PATH 同 --env-file
|
||
# OPENDANCE_ENV_EXAMPLE=PATH 同 --example
|
||
# OPENDANCE_PROMPT_TIMEOUT=SEC 同 --timeout
|
||
#
|
||
# 说明:
|
||
# * 数据库口令以 `DB_PASS=值` 原样写入(**不加引号**)。请勿使用会破坏
|
||
# shell 语法的字符(& $ ` \ 空格 ; | < >),否则 install.sh 的
|
||
# `set -a; . .env` 会解析失败。脚本检测到这类字符会给出警告。
|
||
# * 非交互判定:--yes / -y / --non-interactive / OPENDANCE_NONINTERACTIVE=1
|
||
# 一定跳过提问;否则 stdin 是 TTY、管道、socket 或普通文件时才提问
|
||
# (因此 `printf '...\n' | bash deploy/init-env.sh` 与自动化测试可用)。
|
||
# 每次 read 都带超时,EOF 立即报错退出——CI 与自动化下绝不挂住。
|
||
# * 写回时只替换/追加被填写的键,模板注释与其它字段原样保留。
|
||
# ============================================================
|
||
set -uo pipefail
|
||
|
||
SELF="${BASH_SOURCE[0]}"
|
||
SCRIPT_DIR="$(cd "$(dirname "$SELF")" && pwd)"
|
||
|
||
ENV_FILE="${OPENDANCE_ENV_FILE:-$SCRIPT_DIR/.env}"
|
||
EXAMPLE_FILE="${OPENDANCE_ENV_EXAMPLE:-}"
|
||
MODE_CHECK=0
|
||
FORCE=0
|
||
NONINTERACTIVE="${OPENDANCE_NONINTERACTIVE:-0}"
|
||
FORCE_INTERACTIVE="${OPENDANCE_INTERACTIVE:-0}"
|
||
SKIP_DB_CHECK="${OPENDANCE_SKIP_DB_CHECK:-0}"
|
||
PROMPT_TIMEOUT="${OPENDANCE_PROMPT_TIMEOUT:-10}"
|
||
|
||
# 颜色:仅在真正的终端上启用,CI / 测试里输出纯文本便于断言
|
||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||
C_RED=$'\033[1;31m'; C_YEL=$'\033[1;33m'; C_GRN=$'\033[1;32m'
|
||
C_BLU=$'\033[1;34m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'
|
||
else
|
||
C_RED=''; C_YEL=''; C_GRN=''; C_BLU=''; C_DIM=''; C_RST=''
|
||
fi
|
||
|
||
log() { printf '%s[init-env]%s %s\n' "$C_BLU" "$C_RST" "$*"; }
|
||
ok() { printf '%s[init-env]%s %s\n' "$C_GRN" "$C_RST" "$*"; }
|
||
warn() { printf '%s[init-env]%s %s\n' "$C_YEL" "$C_RST" "$*" >&2; }
|
||
err() { printf '%s[init-env]%s %s\n' "$C_RED" "$C_RST" "$*" >&2; }
|
||
die() { err "$*"; exit 1; }
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法:bash deploy/init-env.sh [选项]
|
||
|
||
--check 只校验,不修改文件、不提问
|
||
--force 已有 .env 也重新逐项询问
|
||
-y, --yes 非交互(等同 --non-interactive)
|
||
--non-interactive CI / 自动化:跳过全部提问,仅做缺字段校验
|
||
-i, --interactive 即使 stdin 不是 TTY 也强制提问(读取超时仍生效)
|
||
--skip-db-check 跳过数据库连通性自检
|
||
--dir DIR 等价于 --env-file DIR/.env --example DIR/.env.example
|
||
--env-file PATH 指定 .env 路径(默认 <脚本目录>/.env)
|
||
--example PATH 指定模板路径(默认 .env 同目录的 .env.example)
|
||
--timeout SECONDS 非 TTY 下每次读取的超时秒数(默认 10)
|
||
-h, --help 显示本帮助
|
||
|
||
环境变量:OPENDANCE_NONINTERACTIVE / OPENDANCE_INTERACTIVE /
|
||
OPENDANCE_SKIP_DB_CHECK / OPENDANCE_ENV_FILE /
|
||
OPENDANCE_ENV_EXAMPLE / OPENDANCE_PROMPT_TIMEOUT
|
||
EOF
|
||
}
|
||
|
||
# ---------- 参数 ----------
|
||
DIR_OPT=""
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--check) MODE_CHECK=1 ;;
|
||
--force|-f) FORCE=1 ;;
|
||
-y|--yes|--non-interactive) NONINTERACTIVE=1 ;;
|
||
-i|--interactive) FORCE_INTERACTIVE=1 ;;
|
||
--skip-db-check) SKIP_DB_CHECK=1 ;;
|
||
--dir) shift; [ $# -gt 0 ] || die "--dir 需要一个目录参数"; DIR_OPT="$1" ;;
|
||
--dir=*) DIR_OPT="${1#*=}" ;;
|
||
--env-file) shift; [ $# -gt 0 ] || die "--env-file 需要一个路径参数"; ENV_FILE="$1" ;;
|
||
--env-file=*) ENV_FILE="${1#*=}" ;;
|
||
--example) shift; [ $# -gt 0 ] || die "--example 需要一个路径参数"; EXAMPLE_FILE="$1" ;;
|
||
--example=*) EXAMPLE_FILE="${1#*=}" ;;
|
||
--timeout) shift; [ $# -gt 0 ] || die "--timeout 需要一个秒数"; PROMPT_TIMEOUT="$1" ;;
|
||
--timeout=*) PROMPT_TIMEOUT="${1#*=}" ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*) die "未知参数:$1(用 --help 查看用法)" ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
if [ -n "$DIR_OPT" ]; then
|
||
ENV_FILE="$DIR_OPT/.env"
|
||
EXAMPLE_FILE="$DIR_OPT/.env.example"
|
||
fi
|
||
[ -n "$ENV_FILE" ] || die "--env-file 路径不能为空"
|
||
|
||
case "$PROMPT_TIMEOUT" in
|
||
''|*[!0-9]*) die "--timeout 必须是整数秒(当前:$PROMPT_TIMEOUT)" ;;
|
||
esac
|
||
|
||
# 模板默认:优先 .env 同目录的 .env.example,其次脚本所在目录
|
||
if [ -z "$EXAMPLE_FILE" ]; then
|
||
if [ -f "$(dirname "$ENV_FILE")/.env.example" ]; then
|
||
EXAMPLE_FILE="$(dirname "$ENV_FILE")/.env.example"
|
||
else
|
||
EXAMPLE_FILE="$SCRIPT_DIR/.env.example"
|
||
fi
|
||
fi
|
||
|
||
# ---------- 小工具 ----------
|
||
# 1..65535 的整数端口
|
||
is_port() {
|
||
local v="${1:-}"
|
||
case "$v" in ''|*[!0-9]*) return 1 ;; esac
|
||
[ "${#v}" -le 5 ] || return 1
|
||
[ "$v" -ge 1 ] 2>/dev/null && [ "$v" -le 65535 ] 2>/dev/null
|
||
}
|
||
|
||
# 模板占位符(如 <existing-mysql-host> / <部署方提供>)视为“未填写”
|
||
is_placeholder() {
|
||
case "${1:-}" in '<'*'>') return 0 ;; *) return 1 ;; esac
|
||
}
|
||
|
||
# 读取 .env 中某个键的当前值(最后一次出现的、未注释的那行)
|
||
# 只做文本解析,不 source 文件,避免 .env 内容被执行
|
||
get_env_value() {
|
||
local key="$1" file="${2:-$ENV_FILE}" raw=""
|
||
[ -f "$file" ] || { printf ''; return 0; }
|
||
raw="$(awk -v k="$key" '
|
||
$0 ~ "^[[:space:]]*" k "[[:space:]]*=" {
|
||
line = $0
|
||
sub(/^[[:space:]]*/, "", line)
|
||
sub("^" k "[[:space:]]*=[[:space:]]*", "", line)
|
||
sub(/[[:space:]]+$/, "", line)
|
||
val = line
|
||
}
|
||
END { if (val != "") print val }
|
||
' "$file")"
|
||
case "$raw" in
|
||
\"*\") raw="${raw#\"}"; raw="${raw%\"}" ;;
|
||
\'*\') raw="${raw#\'}"; raw="${raw%\'}" ;;
|
||
esac
|
||
printf '%s' "$raw"
|
||
}
|
||
|
||
# 取默认值:先看 .env(已填写),再看模板
|
||
default_for() {
|
||
local key="$1" v=""
|
||
v="$(get_env_value "$key" "$ENV_FILE")"
|
||
if [ -z "$v" ] || is_placeholder "$v"; then
|
||
v="$(get_env_value "$key" "$EXAMPLE_FILE")"
|
||
fi
|
||
printf '%s' "$v"
|
||
}
|
||
|
||
# 替换或追加单个键,保留文件中的注释与其它行
|
||
# 值通过环境变量传入 awk(ENVIRON 不做转义处理),因此 $ * % 等原样落盘
|
||
set_env_value() {
|
||
local key="$1" value="$2" file="$3" tmp="" rc=0
|
||
local dir
|
||
dir="$(dirname "$file")"
|
||
[ -d "$dir" ] || mkdir -p "$dir" || return 1
|
||
tmp="$(mktemp "$dir/.env.tmp.XXXXXX")" || return 1
|
||
KEY_NAME="$key" KEY_VALUE="$value" awk '
|
||
BEGIN { k = ENVIRON["KEY_NAME"]; v = ENVIRON["KEY_VALUE"]; done = 0 }
|
||
$0 ~ "^[[:space:]]*" k "[[:space:]]*=" { print k "=" v; done = 1; next }
|
||
{ print }
|
||
END { if (!done) print k "=" v }
|
||
' "$file" > "$tmp" || rc=$?
|
||
if [ "$rc" -ne 0 ]; then rm -f "$tmp"; return 1; fi
|
||
chmod 600 "$tmp" 2>/dev/null || true
|
||
mv "$tmp" "$file" || { rm -f "$tmp"; return 1; }
|
||
return 0
|
||
}
|
||
|
||
# 单字段校验:通过则输出空串,否则输出错误信息
|
||
validate_field() {
|
||
local key="$1" val="${2:-}" required="$3"
|
||
if [ "$required" = 1 ] && { [ -z "$val" ] || is_placeholder "$val"; }; then
|
||
printf '%s 不能为空或仍是模板占位符(必填)' "$key"; return 0
|
||
fi
|
||
case "$key" in
|
||
DB_PORT|PORT|ADMIN_PORT)
|
||
if [ -n "$val" ] && ! is_port "$val"; then
|
||
printf '%s 必须是 1..65535 的整数(当前:%s)' "$key" "$val"; return 0
|
||
fi
|
||
;;
|
||
esac
|
||
return 0
|
||
}
|
||
|
||
# 口令里会破坏 `set -a; . .env` 解析的字符(脚本按要求不加引号写盘)
|
||
unsafe_for_unquoted() {
|
||
case "${1:-}" in
|
||
*'&'*|*'$'*|*'`'*|*'\'*|*' '*|*' '*|*';'*|*'|'*|*'<'*|*'>'*|*'('*|*')'*|*'!'*) return 0 ;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
# ---------- 交互判定 ----------
|
||
# 管道 / socket / 普通文件也算“有输入源”,这样 `printf ... | bash init-env.sh`
|
||
# 以及 Node spawnSync({input}) 都能交互;/dev/null、已关闭的 stdin 则判为非交互。
|
||
# 每次 read 都带超时,绝不无限阻塞。
|
||
detect_interactive() {
|
||
[ "$MODE_CHECK" = 1 ] && return 1
|
||
[ "$NONINTERACTIVE" = 1 ] && return 1
|
||
[ "$FORCE_INTERACTIVE" = 1 ] && return 0
|
||
[ -t 0 ] && return 0
|
||
[ -p /dev/stdin ] && return 0
|
||
[ -S /dev/stdin ] && return 0
|
||
[ -f /dev/stdin ] && return 0
|
||
return 1
|
||
}
|
||
|
||
INTERACTIVE=0
|
||
IS_TTY=0
|
||
[ -t 0 ] && IS_TTY=1
|
||
READ_OPTS=()
|
||
if [ "$IS_TTY" != 1 ]; then
|
||
READ_OPTS=(-t "$PROMPT_TIMEOUT")
|
||
fi
|
||
|
||
prompt_read_failed() {
|
||
local key="$1"
|
||
printf '\n'
|
||
err "读取 $key 失败:stdin 已结束或超过 ${PROMPT_TIMEOUT}s 无输入。"
|
||
err "非交互环境请改用:bash deploy/init-env.sh --non-interactive"
|
||
err "(或设置 OPENDANCE_NONINTERACTIVE=1,并提前把 $ENV_FILE 填好)"
|
||
exit 1
|
||
}
|
||
|
||
# 逐项询问,结果放入全局 ANS
|
||
ANS=""
|
||
prompt_field() {
|
||
local key="$1" usage_text="$2" dflt="$3" required="$4" silent="$5"
|
||
is_placeholder "$dflt" && dflt=""
|
||
local shown="$dflt" val="" rc=0 msg=""
|
||
if [ "$silent" = 1 ] && [ -n "$dflt" ]; then shown="***(已设置,直接回车保留)"; fi
|
||
while :; do
|
||
printf '\n%s──── %s ────%s\n' "$C_BLU" "$key" "$C_RST"
|
||
printf ' 用途:%s\n' "$usage_text"
|
||
if [ "$required" = 1 ]; then printf ' 必填:%s是%s\n' "$C_YEL" "$C_RST"
|
||
else printf ' 必填:否(直接回车用默认值)\n'; fi
|
||
if [ -n "$shown" ]; then printf ' 当前/默认:%s\n' "$shown"
|
||
else printf ' 当前/默认:%s(空)%s\n' "$C_YEL" "$C_RST"; fi
|
||
if [ "$silent" = 1 ]; then
|
||
printf ' 说明:输入不回显;口令不要加引号\n'
|
||
printf '请输入 %s:' "$key"
|
||
rc=0
|
||
IFS= read -r -s ${READ_OPTS[@]+"${READ_OPTS[@]}"} val || rc=$?
|
||
printf '\n'
|
||
else
|
||
printf '请输入 %s(回车保留默认):' "$key"
|
||
rc=0
|
||
IFS= read -r ${READ_OPTS[@]+"${READ_OPTS[@]}"} val || rc=$?
|
||
# 管道输入时回车不会被回显,补一个换行让输出可读
|
||
[ "$IS_TTY" = 1 ] || printf '\n'
|
||
fi
|
||
if [ "$rc" -ne 0 ]; then prompt_read_failed "$key"; fi
|
||
[ -z "$val" ] && val="$dflt"
|
||
msg="$(validate_field "$key" "$val" "$required")"
|
||
if [ -z "$msg" ]; then ANS="$val"; return 0; fi
|
||
printf '%s ✗ %s%s\n' "$C_RED" "$msg" "$C_RST"
|
||
printf ' 请重新输入 %s。\n' "$key"
|
||
done
|
||
}
|
||
|
||
# ---------- 数据库连通性自检 ----------
|
||
# 用项目自带的 mysql2(node -e 动态 require),凭据只经环境变量传给子进程,
|
||
# 不出现在命令行参数里;失败只警告,不影响写盘。
|
||
DB_CHECK_JS='
|
||
try {
|
||
var mysql = require(process.env.MYSQL2_PATH);
|
||
var conn = mysql.createConnection({
|
||
host: process.env.DB_HOST,
|
||
port: Number(process.env.DB_PORT || 3306),
|
||
user: process.env.DB_USER,
|
||
password: process.env.DB_PASS,
|
||
database: process.env.DB_NAME,
|
||
connectTimeout: 5000,
|
||
charset: "utf8mb4"
|
||
});
|
||
conn.query("SELECT 1", function (err) {
|
||
if (err) { console.error("db-check: " + err.message); process.exit(1); }
|
||
conn.end(function () { process.exit(0); });
|
||
});
|
||
} catch (e) {
|
||
console.error("db-check: " + (e && e.message ? e.message : String(e)));
|
||
process.exit(1);
|
||
}
|
||
'
|
||
|
||
find_mysql2() {
|
||
local c=""
|
||
local candidates=()
|
||
if [ -n "${OPENDANCE_MYSQL2_PATH:-}" ]; then candidates+=("$OPENDANCE_MYSQL2_PATH"); fi
|
||
candidates+=(
|
||
"$SCRIPT_DIR/../web-server/node_modules/mysql2"
|
||
"$PWD/web-server/node_modules/mysql2"
|
||
"$PWD/node_modules/mysql2"
|
||
"$SCRIPT_DIR/../node_modules/mysql2"
|
||
)
|
||
if [ -n "${APP_DIR:-}" ]; then candidates+=("$APP_DIR/web-server/node_modules/mysql2"); fi
|
||
for c in "${candidates[@]}"; do
|
||
if [ -d "$c" ]; then printf '%s' "$c"; return 0; fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
db_check() {
|
||
local host="$1" port="$2" user="$3" pass="$4" name="$5"
|
||
if [ "$SKIP_DB_CHECK" = 1 ]; then
|
||
log "已跳过数据库连通性检查(--skip-db-check / OPENDANCE_SKIP_DB_CHECK)"
|
||
return 2
|
||
fi
|
||
local node_bin=""
|
||
node_bin="$(command -v node 2>/dev/null || true)"
|
||
if [ -z "$node_bin" ]; then warn "未找到 node,跳过数据库连通性检查"; return 2; fi
|
||
local mysql2=""
|
||
mysql2="$(find_mysql2 || true)"
|
||
if [ -z "$mysql2" ]; then
|
||
warn "未找到 web-server/node_modules/mysql2,跳过数据库连通性检查"
|
||
return 2
|
||
fi
|
||
log "尝试连接数据库 ${host}:${port}/${name}(5 秒超时,口令不出现在命令行/日志)…"
|
||
local out="" rc=0
|
||
out="$(DB_HOST="$host" DB_PORT="$port" DB_USER="$user" DB_PASS="$pass" DB_NAME="$name" \
|
||
MYSQL2_PATH="$mysql2" "$node_bin" -e "$DB_CHECK_JS" 2>&1)" || rc=$?
|
||
# 兜底脱敏:万一驱动把口令回显进错误信息
|
||
if [ -n "$pass" ]; then out="${out//"$pass"/***}"; fi
|
||
if [ "$rc" -eq 0 ]; then
|
||
ok "数据库连接成功:${host}:${port}/${name}"
|
||
return 0
|
||
fi
|
||
warn "数据库连接失败(口令已隐藏):"
|
||
printf '%s\n' "$out" | tail -n 3 | sed 's/^/ /' >&2
|
||
warn "可先完成部署,稍后用 bash deploy/init-env.sh --check 复核。"
|
||
return 1
|
||
}
|
||
|
||
# ---------- 建文件 ----------
|
||
if [ ! -f "$ENV_FILE" ]; then
|
||
if [ "$MODE_CHECK" = 1 ]; then
|
||
err "缺少 $ENV_FILE(--check 只校验,不会创建文件)"
|
||
exit 1
|
||
fi
|
||
if [ ! -f "$EXAMPLE_FILE" ]; then
|
||
die "缺少 $ENV_FILE,且找不到模板 $EXAMPLE_FILE。
|
||
请从发布包中确认 deploy/.env.example 存在,或手动创建 $ENV_FILE。"
|
||
fi
|
||
mkdir -p "$(dirname "$ENV_FILE")" || die "无法创建目录:$(dirname "$ENV_FILE")"
|
||
cp "$EXAMPLE_FILE" "$ENV_FILE" || die "无法从模板复制:$EXAMPLE_FILE → $ENV_FILE"
|
||
chmod 600 "$ENV_FILE" 2>/dev/null || true
|
||
log "已从模板创建 $ENV_FILE(来源:$EXAMPLE_FILE)"
|
||
fi
|
||
|
||
# ---------- 读取当前值 ----------
|
||
CUR_PORT="$(get_env_value PORT "$ENV_FILE")"
|
||
[ -n "$CUR_PORT" ] || CUR_PORT="$(get_env_value API_PORT "$ENV_FILE")" # 兼容旧键名
|
||
CUR_ADMIN="$(get_env_value ADMIN_PORT "$ENV_FILE")"
|
||
CUR_DB_HOST="$(get_env_value DB_HOST "$ENV_FILE")"
|
||
CUR_DB_PORT="$(get_env_value DB_PORT "$ENV_FILE")"
|
||
CUR_DB_USER="$(get_env_value DB_USER "$ENV_FILE")"
|
||
CUR_DB_PASS="$(get_env_value DB_PASS "$ENV_FILE")"
|
||
CUR_DB_NAME="$(get_env_value DB_NAME "$ENV_FILE")"
|
||
CUR_HOST="$(get_env_value HOST "$ENV_FILE")"
|
||
|
||
# 必填是否已齐全(用于“已有 .env 且完整就不再询问”);端口已写但非法时
|
||
# 不算“完整”,这样交互模式会重新询问而不是直接报错退出。
|
||
current_complete() {
|
||
local k v p
|
||
for k in DB_HOST DB_USER DB_PASS DB_NAME; do
|
||
v="$(get_env_value "$k" "$ENV_FILE")"
|
||
if [ -z "$v" ] || is_placeholder "$v"; then return 1; fi
|
||
done
|
||
p="$(get_env_value DB_PORT "$ENV_FILE")"
|
||
if [ -n "$p" ] && ! is_port "$p"; then return 1; fi
|
||
p="$(get_env_value PORT "$ENV_FILE")"
|
||
[ -n "$p" ] || p="$(get_env_value API_PORT "$ENV_FILE")"
|
||
if [ -n "$p" ] && ! is_port "$p"; then return 1; fi
|
||
p="$(get_env_value ADMIN_PORT "$ENV_FILE")"
|
||
if [ -n "$p" ] && ! is_port "$p"; then return 1; fi
|
||
return 0
|
||
}
|
||
|
||
INTERACTIVE=0
|
||
detect_interactive && INTERACTIVE=1
|
||
|
||
# ---------- 交互式引导 ----------
|
||
if [ "$INTERACTIVE" = 1 ]; then
|
||
if [ "$FORCE" = 1 ]; then
|
||
log "交互式引导(--force:重新询问全部字段,回车保留当前值)"
|
||
elif current_complete; then
|
||
log "$ENV_FILE 已存在且必填项齐全,跳过询问(如需重新填写请加 --force)"
|
||
else
|
||
log "交互式引导:逐项填写(回车保留方括号中的当前/默认值)"
|
||
fi
|
||
|
||
need_prompt=1
|
||
if [ "$FORCE" != 1 ] && current_complete; then need_prompt=0; fi
|
||
|
||
if [ "$need_prompt" = 1 ]; then
|
||
# DB_HOST
|
||
prompt_field DB_HOST "现有 MySQL / MariaDB 实例地址(本脚本不安装数据库)" \
|
||
"$(default_for DB_HOST)" 1 0
|
||
DB_HOST_V="$ANS"
|
||
|
||
# DB_PORT
|
||
prompt_field DB_PORT "数据库端口(1..65535)" \
|
||
"$(default_for DB_PORT)" 0 0
|
||
[ -n "$ANS" ] || ANS="3306"
|
||
DB_PORT_V="$ANS"
|
||
|
||
# DB_USER
|
||
prompt_field DB_USER "数据库账号" "$(default_for DB_USER)" 1 0
|
||
DB_USER_V="$ANS"
|
||
|
||
# DB_PASS(不回显)
|
||
prompt_field DB_PASS "数据库口令(输入不回显,写盘不加引号)" \
|
||
"$(default_for DB_PASS)" 1 1
|
||
DB_PASS_V="$ANS"
|
||
|
||
# DB_NAME
|
||
prompt_field DB_NAME "数据库名" "$(default_for DB_NAME)" 1 0
|
||
DB_NAME_V="$ANS"
|
||
|
||
# PORT
|
||
prompt_field PORT "软件 API 端口(Windows/Godot/Android 客户端调用)" \
|
||
"${CUR_PORT:-3000}" 0 0
|
||
[ -n "$ANS" ] || ANS="3000"
|
||
PORT_V="$ANS"
|
||
|
||
# ADMIN_PORT:默认 PORT+1;原值为“旧 PORT+1/旧 PORT”时跟随新 PORT,
|
||
# 显式自定义过的值则原样保留
|
||
old_port="${CUR_PORT:-3000}"
|
||
is_port "$old_port" || old_port=3000 # 旧 .env 里 PORT 非法时按默认算
|
||
if [ -z "$CUR_ADMIN" ] || ! is_port "$CUR_ADMIN"; then adm_default=$((PORT_V + 1))
|
||
elif [ "$CUR_ADMIN" = "$old_port" ]; then adm_default="$PORT_V"
|
||
elif [ "$CUR_ADMIN" = "$((old_port + 1))" ]; then adm_default=$((PORT_V + 1))
|
||
else adm_default="$CUR_ADMIN"; fi
|
||
prompt_field ADMIN_PORT "管理端端口(浏览器管理界面,默认 PORT+1;与 PORT 相同=单端口模式)" \
|
||
"$adm_default" 0 0
|
||
[ -n "$ANS" ] || ANS="$adm_default"
|
||
ADMIN_PORT_V="$ANS"
|
||
|
||
# HOST
|
||
prompt_field HOST "软件 API 监听地址(0.0.0.0 对外 / 127.0.0.1 仅本机)" \
|
||
"${CUR_HOST:-0.0.0.0}" 0 0
|
||
[ -n "$ANS" ] || ANS="0.0.0.0"
|
||
HOST_V="$ANS"
|
||
else
|
||
# 已有且完整:用现值,不写盘
|
||
DB_HOST_V="$CUR_DB_HOST"; DB_PORT_V="${CUR_DB_PORT:-3306}"
|
||
DB_USER_V="$CUR_DB_USER"; DB_PASS_V="$CUR_DB_PASS"
|
||
DB_NAME_V="$CUR_DB_NAME"; PORT_V="${CUR_PORT:-3000}"
|
||
ADMIN_PORT_V="$CUR_ADMIN"
|
||
if [ -z "$ADMIN_PORT_V" ] && is_port "$PORT_V"; then ADMIN_PORT_V="$((PORT_V + 1))"; fi
|
||
HOST_V="${CUR_HOST:-0.0.0.0}"
|
||
|
||
if [ "$SKIP_DB_CHECK" = 1 ]; then
|
||
log "已跳过数据库连通性检查(--skip-db-check / OPENDANCE_SKIP_DB_CHECK)"
|
||
else
|
||
db_check "$DB_HOST_V" "$DB_PORT_V" "$DB_USER_V" "$DB_PASS_V" "$DB_NAME_V" || true
|
||
fi
|
||
log "未做任何修改。"
|
||
exit 0
|
||
fi
|
||
else
|
||
# ---------- 非交互 / --check:只校验,不提问 ----------
|
||
# 非交互只校验「当前 .env 实际有什么」,绝不从模板补默认值,
|
||
# 否则模板里的 DB_USER 会被当成已填写,缺字段就报不出来了。
|
||
# 端口/HOST 属于有内置默认值的项,缺省不算错误。
|
||
DB_HOST_V="$(get_env_value DB_HOST "$ENV_FILE")"
|
||
DB_PORT_V="$(get_env_value DB_PORT "$ENV_FILE")"; [ -n "$DB_PORT_V" ] || DB_PORT_V="3306"
|
||
DB_USER_V="$(get_env_value DB_USER "$ENV_FILE")"
|
||
DB_PASS_V="$(get_env_value DB_PASS "$ENV_FILE")"
|
||
DB_NAME_V="$(get_env_value DB_NAME "$ENV_FILE")"
|
||
PORT_V="$(get_env_value PORT "$ENV_FILE")"
|
||
[ -n "$PORT_V" ] || PORT_V="$(get_env_value API_PORT "$ENV_FILE")"
|
||
[ -n "$PORT_V" ] || PORT_V="3000"
|
||
ADMIN_PORT_V="$(get_env_value ADMIN_PORT "$ENV_FILE")"
|
||
# PORT 非法时不做算术,交给下面的端口校验报错,避免 set -u 下报 caculate 错误
|
||
if [ -z "$ADMIN_PORT_V" ] && is_port "$PORT_V"; then ADMIN_PORT_V="$((PORT_V + 1))"; fi
|
||
HOST_V="$(get_env_value HOST "$ENV_FILE")"; [ -n "$HOST_V" ] || HOST_V="0.0.0.0"
|
||
fi
|
||
|
||
# ---------- 汇总校验(两种模式共用) ----------
|
||
ERRORS=()
|
||
add_error() { ERRORS+=("$1"); }
|
||
|
||
for pair in "DB_HOST:$DB_HOST_V" "DB_USER:$DB_USER_V" "DB_PASS:$DB_PASS_V" "DB_NAME:$DB_NAME_V"; do
|
||
k="${pair%%:*}"; v="${pair#*:}"
|
||
m="$(validate_field "$k" "$v" 1)"
|
||
[ -n "$m" ] && add_error "$m"
|
||
done
|
||
for pair in "DB_PORT:$DB_PORT_V" "PORT:$PORT_V" "ADMIN_PORT:$ADMIN_PORT_V"; do
|
||
k="${pair%%:*}"; v="${pair#*:}"
|
||
m="$(validate_field "$k" "$v" 0)"
|
||
[ -n "$m" ] && add_error "$m"
|
||
done
|
||
|
||
if [ "${#ERRORS[@]}" -gt 0 ]; then
|
||
if [ "$MODE_CHECK" = 1 ]; then
|
||
err "$ENV_FILE 校验未通过(--check 未修改任何文件):"
|
||
else
|
||
err "$ENV_FILE 校验未通过:"
|
||
fi
|
||
for m in "${ERRORS[@]}"; do err " ✗ $m"; done
|
||
err "请填写后重试:bash deploy/init-env.sh"
|
||
exit 1
|
||
fi
|
||
|
||
# 端口相同 → 单端口模式(允许,仅提示)
|
||
if [ "$ADMIN_PORT_V" = "$PORT_V" ]; then
|
||
warn "ADMIN_PORT 与 PORT 相同($PORT_V):单端口模式,管理页面与软件接口共用同一端口。"
|
||
fi
|
||
|
||
# 口令不安全的字符提示(按需求不加引号写盘)
|
||
if unsafe_for_unquoted "$DB_PASS_V"; then
|
||
warn "DB_PASS 含 shell 特殊字符(& \$ \` \\ 空格 ; | < > ( ) !)。"
|
||
warn "本脚本按约定不加引号写盘,但 install.sh 的 \`set -a; . .env\` 可能解析失败,"
|
||
warn "建议改用仅含字母/数字/*/%/-/_/. 的口令。"
|
||
fi
|
||
|
||
# ---------- --check:到此为止 ----------
|
||
if [ "$MODE_CHECK" = 1 ]; then
|
||
ok "$ENV_FILE 校验通过(未做任何修改)"
|
||
printf ' DB_HOST=%s DB_PORT=%s DB_USER=%s DB_PASS=*** DB_NAME=%s\n' \
|
||
"$DB_HOST_V" "$DB_PORT_V" "$DB_USER_V" "$DB_NAME_V"
|
||
printf ' PORT=%s ADMIN_PORT=%s HOST=%s\n' "$PORT_V" "$ADMIN_PORT_V" "$HOST_V"
|
||
exit 0
|
||
fi
|
||
|
||
# ---------- 非交互:只报“校验通过”,不写盘 ----------
|
||
if [ "$INTERACTIVE" != 1 ]; then
|
||
ok "$ENV_FILE 校验通过(非交互模式,未做任何修改)"
|
||
printf ' DB_HOST=%s DB_PORT=%s DB_USER=%s DB_PASS=*** DB_NAME=%s\n' \
|
||
"$DB_HOST_V" "$DB_PORT_V" "$DB_USER_V" "$DB_NAME_V"
|
||
printf ' PORT=%s ADMIN_PORT=%s HOST=%s\n' "$PORT_V" "$ADMIN_PORT_V" "$HOST_V"
|
||
exit 0
|
||
fi
|
||
|
||
# ---------- 交互:数据库自检 ----------
|
||
db_check "$DB_HOST_V" "$DB_PORT_V" "$DB_USER_V" "$DB_PASS_V" "$DB_NAME_V" || true
|
||
|
||
# ---------- 写回(保留注释,只改这些键) ----------
|
||
log "写回 $ENV_FILE(保留模板注释与其它字段)…"
|
||
write_key() {
|
||
local k="$1" v="$2"
|
||
set_env_value "$k" "$v" "$ENV_FILE" || die "写入 $ENV_FILE 失败(键:$k)"
|
||
local back=""
|
||
back="$(get_env_value "$k" "$ENV_FILE")"
|
||
[ "$back" = "$v" ] || warn "$k 写回后校验不一致(写入:$v,读回:$back)"
|
||
}
|
||
|
||
CHANGED=()
|
||
record_change() {
|
||
local k="$1" old="$2" new="$3"
|
||
[ "$old" = "$new" ] && return 0
|
||
if [ "$k" = DB_PASS ]; then
|
||
if [ -z "$old" ]; then CHANGED+=("DB_PASS: (空)→ ***")
|
||
else CHANGED+=("DB_PASS: *** → ***(已更新)"); fi
|
||
else
|
||
CHANGED+=("$k: ${old:-(空)} → ${new:-(空)}")
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
record_change DB_HOST "$CUR_DB_HOST" "$DB_HOST_V"
|
||
record_change DB_PORT "$CUR_DB_PORT" "$DB_PORT_V"
|
||
record_change DB_USER "$CUR_DB_USER" "$DB_USER_V"
|
||
record_change DB_PASS "$CUR_DB_PASS" "$DB_PASS_V"
|
||
record_change DB_NAME "$CUR_DB_NAME" "$DB_NAME_V"
|
||
record_change PORT "$CUR_PORT" "$PORT_V"
|
||
record_change ADMIN_PORT "$CUR_ADMIN" "$ADMIN_PORT_V"
|
||
record_change HOST "$CUR_HOST" "$HOST_V"
|
||
|
||
write_key DB_HOST "$DB_HOST_V"
|
||
write_key DB_PORT "$DB_PORT_V"
|
||
write_key DB_USER "$DB_USER_V"
|
||
write_key DB_PASS "$DB_PASS_V"
|
||
write_key DB_NAME "$DB_NAME_V"
|
||
write_key PORT "$PORT_V"
|
||
write_key ADMIN_PORT "$ADMIN_PORT_V"
|
||
write_key HOST "$HOST_V"
|
||
|
||
chmod 600 "$ENV_FILE" 2>/dev/null || warn "无法将 $ENV_FILE 权限设为 600"
|
||
|
||
# ---------- 摘要 ----------
|
||
cat <<EOF
|
||
|
||
============================================================
|
||
deploy/.env 已写入:$ENV_FILE
|
||
============================================================
|
||
DB_HOST = $DB_HOST_V
|
||
DB_PORT = $DB_PORT_V
|
||
DB_USER = $DB_USER_V
|
||
DB_PASS = ***
|
||
DB_NAME = $DB_NAME_V
|
||
PORT = $PORT_V
|
||
ADMIN_PORT = $ADMIN_PORT_V$([ "$ADMIN_PORT_V" = "$PORT_V" ] && printf ' (单端口模式)')
|
||
HOST = $HOST_V
|
||
------------------------------------------------------------
|
||
EOF
|
||
if [ "${#CHANGED[@]}" -gt 0 ]; then
|
||
echo "本次变更:"
|
||
for c in "${CHANGED[@]}"; do printf ' %s\n' "$c"; done
|
||
else
|
||
echo "本次变更:无(与写入前一致)"
|
||
fi
|
||
cat <<EOF
|
||
------------------------------------------------------------
|
||
提示:数据库口令不要加引号(当前文件按 \$DB_PASS=值 原样写入)。
|
||
如需复核:bash deploy/init-env.sh --check
|
||
如需重填:bash deploy/init-env.sh --force
|
||
============================================================
|
||
EOF
|
||
|
||
ok "初始化完成,可以继续执行:bash deploy/install.sh"
|
||
exit 0
|