fix(deploy): install.sh 复制时同样误排除了 storage 目录

用户重装后仍报同一错误,且日志显示「复制 web-server … 174 个文件」。
经复现:发布包内 web-server 有 180 个文件(含 src/storage 2 个、
dist/storage 4 个),但 install.sh 复制后只剩 174 个——storage 被剔除了。

根因:上一轮只修了 package-release.sh,install.sh 里**还有一份独立的
copy_tree() 排除规则**,其中 'storage' 同样未锚定。install.sh 的
COPY_EXCLUDES 与 tar 回退分支都把 'storage' 当作任意层级模式,
于是 src/storage 与 dist/storage 在复制进 /opt/opendance 时被剔除。

这是同一类错误在本项目的第三次(.gitignore → package-release.sh →
install.sh)。因此这次不再只改模式,而是把语义显式化并加硬校验:

- 排除规则拆成三类常量,语义明确:
    TOP_DIRS  仅顶层:storage data tmp dist-release   → 必须锚定
    ANY_DIRS  任意层级:node_modules .git coverage .gradle
    TOP_FILES 仅顶层文件:.env .env.local
- rsync 用前导 '/' 锚定;tar 对 TOP_* 只给 './x'(绝不给 '*/storage'),
  对 ANY_* 才给 './x' + '*/x'
- 复制后硬校验关键产物(dist/main.js、dist/storage/storage.module.js、
  dist/app.module.js、admin/index.html),缺失即 die 并指出是排除规则问题

实测:
- rsync 路径:180 文件,src/storage 与 dist/storage 均在,
  运行时 storage 与 .env 未带入,.env.example 保留
- tar 回退路径(隔离 PATH 屏蔽 rsync 后真跑):同样 180 文件,存储目录完整
- git 检出布局:214 文件,嵌套 admin/node_modules 未带入,本地 .env 未带入
- 对照实验:把锚定改回未锚定 → 复制剩 174 文件 → 守卫立刻报
  「缺少关键文件 dist/storage/storage.module.js」并终止,不再拖到启动才炸
This commit is contained in:
2026-09-15 01:00:58 +08:00
parent caca7672cb
commit 87a1ded2d2
+42 -13
View File
@@ -151,26 +151,41 @@ esac
# 说明:在 git 检出目录直接执行时,web-server/ 里可能已有 200 MB+ 的 # 说明:在 git 检出目录直接执行时,web-server/ 里可能已有 200 MB+ 的
# node_modules(数万个小文件)。原实现用 cp -r 全量复制,会长时间无输出, # node_modules(数万个小文件)。原实现用 cp -r 全量复制,会长时间无输出,
# 看起来像卡死。这里统一排除,运行期依赖稍后由 npm ci 重新安装。 # 看起来像卡死。这里统一排除,运行期依赖稍后由 npm ci 重新安装。
# .env / .env.local 一并排除:deploy/.env 才是权威配置,源码树里的本地 .env # 复制排除规则 —— 必须区分「仅顶层」与「任意层级」:
# 若被复制进来,dotenv 会把它当成第二配置源,造成排障困惑。 #
COPY_EXCLUDES=(--exclude=node_modules --exclude=.git --exclude=storage # 仅顶层 (TOP_*):运行时产物与本地配置,只会出现在 web-server 根下。
--exclude=dist-release --exclude=coverage --exclude=.gradle # 必须锚定!未锚定的 'storage' 会匹配任意层级,把源码目录
--exclude=.env --exclude=.env.local) # src/storage/ 与编译产物 dist/storage/ 一起排除掉,导致安装后
# 启动即 MODULE_NOT_FOUND。本项目已在 .gitignore 与 package-release.sh
# 上各踩过一次,这里是第三处。
# 任意层级 (ANY_*):依赖与缓存目录,任何位置都不应该被复制。
#
TOP_DIRS="storage data tmp dist-release"
ANY_DIRS="node_modules .git coverage .gradle"
TOP_FILES=".env .env.local"
copy_tree() { copy_tree() {
local src="$1" dst="$2" local src="$1" dst="$2"
mkdir -p "$dst" mkdir -p "$dst"
if command -v rsync >/dev/null 2>&1; then if command -v rsync >/dev/null 2>&1; then
rsync -a --delete "${COPY_EXCLUDES[@]}" "$src/" "$dst/" # rsync 模式前导 '/' 表示锚定到传输根目录
local args=(-a --delete)
local d
for d in $TOP_DIRS; do args+=(--exclude="/$d/"); done
for d in $ANY_DIRS; do args+=(--exclude="$d/"); done
local f
for f in $TOP_FILES; do args+=(--exclude="/$f"); done
rsync "${args[@]}" "$src/" "$dst/"
else else
# 无 rsync 时用 tar 管道,同样支持排除。 # tar 的 --exclude 按完整成员名匹配:
# 注意:tar 的 --exclude 按完整成员名匹配,只写 "./node_modules" 无法匹配 # './storage' 只匹配顶层
# 嵌套的 "./admin/node_modules"(实测会漏掉 admin 下的 122MB 依赖)。 # './x' + '*/x' 才覆盖任意层级(如 ./admin/node_modules)
# 因此每个名字都要同时给出顶层与任意层级的模式。 # 因此 ANY_DIRS 给两个模式,TOP_* 只给 './' 一个模式——绝不能给 '*/storage'。
local tar_excludes=() local tar_excludes=()
for pattern in node_modules .git storage dist-release coverage .gradle .env .env.local; do local d f
tar_excludes+=(--exclude="./$pattern" --exclude="*/$pattern") for d in $TOP_DIRS; do tar_excludes+=(--exclude="./$d"); done
done for d in $ANY_DIRS; do tar_excludes+=(--exclude="./$d" --exclude="*/$d"); done
for f in $TOP_FILES; do tar_excludes+=(--exclude="./$f"); done
( cd "$src" && tar "${tar_excludes[@]}" -cf - . ) | ( cd "$dst" && tar -xf - ) ( cd "$src" && tar "${tar_excludes[@]}" -cf - . ) | ( cd "$dst" && tar -xf - )
fi fi
} }
@@ -218,6 +233,20 @@ cp -r "$SRC_DIR/deploy/." "$APP_DIR/deploy/"
if [ -f "$SRC_DIR/THIRD_PARTY.md" ]; then cp "$SRC_DIR/THIRD_PARTY.md" "$APP_DIR/"; fi if [ -f "$SRC_DIR/THIRD_PARTY.md" ]; then cp "$SRC_DIR/THIRD_PARTY.md" "$APP_DIR/"; fi
if [ -f "$SRC_DIR/README.txt" ]; then cp "$SRC_DIR/README.txt" "$APP_DIR/"; fi if [ -f "$SRC_DIR/README.txt" ]; then cp "$SRC_DIR/README.txt" "$APP_DIR/"; fi
# ---- 复制结果硬校验 ----
# 排除规则一旦写错,会在启动时才以 MODULE_NOT_FOUND 的形式暴露,
# 极难定位。这里就地检查关键的编译产物,把问题拦在部署阶段。
for required in \
"$APP_DIR/web-server/dist/main.js" \
"$APP_DIR/web-server/dist/storage/storage.module.js" \
"$APP_DIR/web-server/dist/app.module.js" \
"$APP_DIR/admin/index.html" ; do
[ -f "$required" ] || die "复制结果缺少关键文件:$required
这通常意味着复制时的排除规则误伤了源码目录,请检查 deploy/install.sh 中的
TOP_DIRS / ANY_DIRS 设置(注意 'storage' 必须锚定到顶层)。"
done
log "复制结果校验通过"
mkdir -p "$STORAGE_DIR" mkdir -p "$STORAGE_DIR"
log "设置目录属主($SERVICE_USER)" log "设置目录属主($SERVICE_USER)"
chown -R "$SERVICE_USER:$SERVICE_USER" "$APP_DIR" "$STORAGE_DIR" chown -R "$SERVICE_USER:$SERVICE_USER" "$APP_DIR" "$STORAGE_DIR"