物理正确性校验体系(common/conv_check.rs +494 行) - 新增 5 类硬门槛:能量守恒(.6)、温度结构(.7)、emflux 积分校验(.emflux,含全 NaN 判失败)、假收敛排查(itek 轨迹首末比)、b 因子合理性(.bfac) - runner 在 TLUSTY 阶段结束后执行全部校验,任一失败判 final_converged=false - GridConfig 新增 8 个可配阈值,经 scheduler→executor→runner 全链路透传 输入文件配置结构化重构(config.rs +1453 行) - TlustyInput 拆为 dot5/nst 分层结构,字段名严格映射 tlusty208.f READ 语句;SynspecInput 重构为 9 个 Fort55Line 子结构体 - 移除 ChainStep.metals 字段,元素集改由 dot5.atoms/ions 显式声明(gen_input5/nst_writer 同步重写为三源融合 / 分层覆盖) - fort.55 修复行结构 bug:补全分子表行(7→9 行),IDSTD 50→0 错位修正(影响全部光谱线强归一化,需重算 SYNSPEC 阶段) conv 诊断 DB 化与阶段归因修复(server) - 单点详情 conv 面板从磁盘 conv.json 改读 DB grid_points.summary_json;grid_points 新增 summary_json/last_elapsed_sec 两列(旧库幂等 ALTER) - record_task_report 阶段归因列加 CASE 守卫 + clear_synspec 对称处理,修复 synspec-only/TLUSTY-only 重跑污染统计 - 新增 summary_merge.rs 点级增量合并,避免重跑覆盖诊断字段 收敛性 ORELAX 修复与 seed_chain 可配(sdB_cno.yaml + node) - nl 阶段加 orelax=0.5、seed_nc 加 orelax=0.3,阻尼中温区 relc 振荡发散 - seed_chain 块可配,executor 优先采用用户配置而非内置默认链 导入工具下线 - 删除 import_results 客户端工具及 Windows 推送脚本;移除 /admin/import_seed 端点 - 改为服务端临时 migrate_conv 端点(扫 conv.json 增量合并入库,迁移后可删) 文档与分析 - 新增 1305 失败点根因分析、fort.14 全 NaN 物理含义分析两份深度文档 - spectrum_correctness_analysis 两次修订标注已修复项;fetch_results.sh 修 trap RETURN 的 set -u 报错
411 lines
17 KiB
Bash
Executable File
411 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# fetch_results.sh — 从全部计算节点同步 data/result 完整科学产物到本地备份
|
||
# ==============================================================================
|
||
# 背景:node 端 result 目录永久保留全部网格点的完整产物(.spec/.cont/.iden/
|
||
# 各阶段快照/conv.json/日志等,撤销了早期的 MAX_RESULT_MODELS=200 LRU 上限)。
|
||
# 为做异地备份、以及便于失败归因与收敛验证,把各节点 result 目录整树拉回本地,
|
||
# 按节点分目录存放。
|
||
#
|
||
# 用法:
|
||
# ./scripts/fetch_results.sh # 无参数 → 交互式向导选择节点
|
||
# ./scripts/fetch_results.sh -i # 显式进入交互式向导
|
||
# ./scripts/fetch_results.sh -n node-fmq-dckj-02 # 非交互:仅同步指定节点(DCTS_NODE_ID)
|
||
# ./scripts/fetch_results.sh -d /backup/dcts-salvage # 覆盖备份根目录
|
||
# ./scripts/fetch_results.sh --with-work # 连带拉取 data/work 沙盒残留
|
||
# ./scripts/fetch_results.sh --include-local # 把本机节点也复制进备份树
|
||
#
|
||
# 传输策略(两类节点都走增量,重复运行只补差异、不重拷全量):
|
||
# - 双方均可用 rsync → rsync 增量(断点续传、幂等,自动补新增/变更文件)
|
||
# - 否则(典型 Windows:无 rsync)→ scp 目录级增量:先列举远端顶层网格点子目录,
|
||
# 只 scp 本地缺失的目录,已存在的不重拷(避免 win-01 的 52GB 全量重拷)
|
||
# 远端 rsync/列目录探测均用多 shell 兼容命令(POSIX/command-v/where/Get-Command、
|
||
# ls/dir/Get-ChildItem),适配 Linux + Windows(cmd/PowerShell 默认 shell)。
|
||
# SSH 复用 deploy.sh 的 ControlMaster,多次执行不重复输密码。
|
||
# ==============================================================================
|
||
|
||
set -euo pipefail
|
||
|
||
GREEN='\033[0;32m'
|
||
BLUE='\033[0;34m'
|
||
RED='\033[0;31m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
|
||
# 定位至项目根目录(与 deploy.sh 一致)
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
WORK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||
cd "${WORK_DIR}"
|
||
|
||
# =============================================================================
|
||
# 默认配置(可被命令行参数覆盖)
|
||
# =============================================================================
|
||
SSH_CONTROL_DIR="${SSH_CONTROL_DIR:-$HOME/.ssh/cm}"
|
||
BACKUP_ROOT="${BACKUP_ROOT:-${WORK_DIR}/data/salvage}"
|
||
ONLY_NODE=""
|
||
WITH_WORK=false
|
||
INCLUDE_LOCAL=false
|
||
INTERACTIVE=false
|
||
HAD_ARGS=false
|
||
SELECTED_PROFILES=()
|
||
|
||
# =============================================================================
|
||
# 解析命令行参数
|
||
# =============================================================================
|
||
while [[ $# -gt 0 ]]; do
|
||
HAD_ARGS=true
|
||
case "$1" in
|
||
-n|--node) ONLY_NODE="$2"; shift 2 ;;
|
||
-d|--dest) BACKUP_ROOT="$2"; shift 2 ;;
|
||
-i|--interactive) INTERACTIVE=true; shift ;;
|
||
--with-work) WITH_WORK=true; shift ;;
|
||
--include-local) INCLUDE_LOCAL=true; shift ;;
|
||
-h|--help)
|
||
sed -n '2,32p' "$0"
|
||
exit 0 ;;
|
||
*) echo -e "${RED}未知参数: $1${NC}"; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
mkdir -p "${BACKUP_ROOT}"
|
||
|
||
# =============================================================================
|
||
# 工具函数
|
||
# =============================================================================
|
||
# 从 profile 文件读取键值(避免 source 造成变量污染;写法对齐 deploy.sh)
|
||
read_profile_var() { # $1=profile $2=键 $3=默认值
|
||
local p="$1" k="$2" d="${3:-}"
|
||
local v
|
||
v=$(grep -E "^${k}=" "$p" 2>/dev/null | head -n1 | cut -d'=' -f2- | tr -d '"' | tr -d "'" || true)
|
||
[ -n "$v" ] && printf '%s' "$v" || printf '%s' "$d"
|
||
}
|
||
|
||
# rsync 远端路径转义:含空格目录需单引号包住(现有路径均无空格,此处兜底)
|
||
rsync_remote_path() { # $1 = 远端绝对路径
|
||
local p="$1"
|
||
case "$p" in
|
||
*\ *) printf "'%s'" "$p" ;;
|
||
*) printf '%s' "$p" ;;
|
||
esac
|
||
}
|
||
|
||
# 远端目录列表(仅顶层条目名,用于 scp 目录级增量比对)。
|
||
# 按远端路径形态选择对应的列举命令:
|
||
# - 盘符开头(如 E:/...) → Windows:powershell Get-ChildItem(兼容 cmd/PowerShell 默认 shell)
|
||
# - 否则 → POSIX:ls -1
|
||
# 返回名字列表(每行一个),失败返回空。
|
||
remote_list_dir() { # $1=user $2=ip $3=port $4=ssh_opts $5=远端绝对目录
|
||
local u="$1" ip="$2" port="$3" opts="$4" rdir="$5"
|
||
# 统一 tr -d '\r':Windows powershell/openssh 输出带 CRLF,会让 grep -Fxq 精确匹配
|
||
# 永远失败(已同步目录每次重传)并产生带 CR 的畸形 scp 路径。
|
||
if [[ "${rdir}" =~ ^[A-Za-z]:[\\/] ]]; then
|
||
# Windows 路径:经 powershell 列举,-Name 直接返回每项一行
|
||
ssh -p "${port}" ${opts} "${u}@${ip}" \
|
||
"powershell -NoProfile -Command \"Get-ChildItem -Name -Path '${rdir}'\"" 2>/dev/null | tr -d '\r'
|
||
else
|
||
ssh -p "${port}" ${opts} "${u}@${ip}" "ls -1 '${rdir}'" 2>/dev/null | tr -d '\r'
|
||
fi
|
||
}
|
||
|
||
# scp 单个远端目录到本地,采用"先落地 .partial 再原子 mv"模式:
|
||
# 1. 目标目录 ${dest}/${d} 已存在 → 视为已同步,跳过(保持原增量语义)
|
||
# 2. 否则 scp 远端目录到 ${dest}/${d}.partial
|
||
# 3. scp 成功后 mv 为 ${dest}/${d}(同 dest 文件系统,原子语义)
|
||
# 4. scp 失败 → 清理残留 .partial,return 非零,由调用方决定是否继续
|
||
# 这样即便 scp 传输过程中断,也不会留下"伪完成"目录导致下次被跳过漏传。
|
||
scp_dir() { # $1=u $2=ip $3=port $4=ssh_opts $5=远端根目录 $6=本地目标 $7=目录名
|
||
local u="$1" ip="$2" port="$3" opts="$4" rdir="$5" dest="$6" d="$7"
|
||
local final="${dest}/${d}"
|
||
if [ -e "${final}" ]; then
|
||
return 0
|
||
fi
|
||
local tmp="${dest}/.${d}.partial"
|
||
# 兜底清理可能的历史残留(上次中断留下的半成品)
|
||
rm -rf "${tmp}"
|
||
if scp -P "${port}" ${opts} -r "${u}@${ip}:${rdir}/${d}" "${tmp}"; then
|
||
mv "${tmp}" "${final}"
|
||
else
|
||
echo -e " ${RED}[!]${NC} scp 失败: ${d},清理残留 ${d}.partial"
|
||
rm -rf "${tmp}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 传输一个目录树(rsync 优先,退化 scp 目录级增量)
|
||
sync_dir() { # $1=user $2=ip $3=port $4=远端绝对目录 $5=本地目标
|
||
local u="$1" ip="$2" port="$3" rdir="$4" dest="$5"
|
||
local ssh_opts="-o ControlMaster=auto -o ControlPath=${SSH_CONTROL_DIR}/cm-${u}@${ip}:${port} -o ControlPersist=1800"
|
||
|
||
mkdir -p "${dest}"
|
||
|
||
# 连通性探测(BatchMode=no 允许交互输密码)。
|
||
# 探测命令用 `echo ok` 而非 `true`:Windows OpenSSH 默认 shell 是 cmd/PowerShell,
|
||
# 没有 Unix 的 true,会导致"密码正确但仍判连接失败"。
|
||
if ! ssh -p "${port}" -o ConnectTimeout=8 ${ssh_opts} "${u}@${ip}" "echo ok" >/dev/null 2>&1; then
|
||
echo -e " ${RED}[!] 无法连接 ${u}@${ip},跳过本节点${NC}"
|
||
return 1
|
||
fi
|
||
|
||
# rsync 可用性(本机 + 远端)。远端探测按平台分支:
|
||
# Windows 路径 → powershell Get-Command;POSIX 路径 → command -v
|
||
local rs_ok=false
|
||
if command -v rsync >/dev/null 2>&1; then
|
||
local probe
|
||
if [[ "${rdir}" =~ ^[A-Za-z]:[\\/] ]]; then
|
||
probe="powershell -NoProfile -Command \"Get-Command rsync -ErrorAction SilentlyContinue | Out-Null\""
|
||
else
|
||
probe="command -v rsync"
|
||
fi
|
||
if ssh -p "${port}" ${ssh_opts} "${u}@${ip}" "${probe}" >/dev/null 2>&1; then
|
||
rs_ok=true
|
||
fi
|
||
fi
|
||
|
||
if [ "${rs_ok}" = "true" ]; then
|
||
# rsync 天然增量幂等:每次都跑,自动补齐新增/变更的文件。
|
||
echo -e " ${BLUE}[→]${NC} rsync 增量同步: ${u}@${ip}:${rdir}/ → ${dest}/"
|
||
rsync -a --partial --info=progress2 \
|
||
-e "ssh -p ${port} ${ssh_opts}" \
|
||
"${u}@${ip}:$(rsync_remote_path "${rdir}")/" "${dest}/"
|
||
else
|
||
# 无 rsync(典型 Windows):scp 目录级增量——只拉本地缺失的网格点子目录,
|
||
# 不重复拷贝已存在的目录(避免 win-01 的 52GB 全量重拷)。
|
||
local remote_dirs existing missing
|
||
remote_dirs=$(remote_list_dir "${u}" "${ip}" "${port}" "${ssh_opts}" "${rdir}")
|
||
if [ -z "${remote_dirs}" ]; then
|
||
echo -e " ${YELLOW}[!]${NC} 远端目录 ${rdir} 为空或无法列举,跳过"
|
||
return 1
|
||
fi
|
||
existing=$(ls -1 "${dest}" 2>/dev/null)
|
||
missing=""
|
||
local d
|
||
# 远端目录名白名单校验:scp 路径会把 ${d} 直接拼进 scp 的 remote 路径,
|
||
# 这里只允许字母/数字/._-,禁止 / 与 . .. —— 防御性过滤畸形或注入目录名。
|
||
while IFS= read -r d; do
|
||
[ -n "$d" ] || continue
|
||
case "$d" in
|
||
"."|"..")
|
||
echo -e " ${YELLOW}[!]${NC} 跳过可疑远端目录名: ${d}"
|
||
continue ;;
|
||
esac
|
||
if [[ "$d" == */* ]] || ! [[ "$d" =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||
echo -e " ${YELLOW}[!]${NC} 跳过不符合白名单的远端目录名: ${d}"
|
||
continue
|
||
fi
|
||
if ! grep -Fxq -- "$d" <<<"${existing}"; then
|
||
missing="${missing}${d}"$'\n'
|
||
fi
|
||
done <<<"${remote_dirs}"
|
||
|
||
local total new
|
||
total=$(printf '%s\n' "${remote_dirs}" | grep -c .)
|
||
new=$(printf '%s' "${missing}" | grep -c .)
|
||
echo -e " ${YELLOW}[→]${NC} scp 目录级增量: ${u}@${ip}:${rdir}/ 共 ${total} 个,缺失 ${new} 个 → ${dest}/"
|
||
if [ "${new}" -eq 0 ]; then
|
||
echo -e " ${GREEN}[√]${NC} 本地已是最新,无新增目录"
|
||
return 0
|
||
fi
|
||
# scp 原子写入:先落地到 .partial 临时目录,成功后同文件系统原子 mv 为最终目录名。
|
||
# 开拷前先清理可能的历史 .partial 残留(上次中断留下的半成品),scp_dir 自身失败
|
||
# 时也会清理各自的 .partial;这样避免下次因"目录已存在"误判为已同步而漏传。
|
||
# (原先用 trap RETURN 兜底,但 RETURN 陷阱会被外层 backup_node 继承,函数返回时
|
||
# dest 已出作用域,触发 set -u 的"未绑定变量"报错,故改为前置清理。)
|
||
rm -rf "${dest}"/.*.partial 2>/dev/null || true
|
||
while IFS= read -r d; do
|
||
[ -n "$d" ] || continue
|
||
echo -e " ${YELLOW}[→]${NC} scp 新增: ${d}"
|
||
scp_dir "${u}" "${ip}" "${port}" "${ssh_opts}" "${rdir}" "${dest}" "$d"
|
||
done <<<"${missing}"
|
||
fi
|
||
}
|
||
|
||
# 备份单个节点
|
||
backup_node() { # $1 = profile 文件
|
||
local p="$1" node_id role env_mode u ip port dir
|
||
node_id=$(read_profile_var "$p" DCTS_NODE_ID)
|
||
role=$(read_profile_var "$p" DEPLOY_ROLE node)
|
||
env_mode=$(read_profile_var "$p" DEPLOY_ENV "")
|
||
u=$(read_profile_var "$p" REMOTE_USER "fmq")
|
||
ip=$(read_profile_var "$p" REMOTE_IP "")
|
||
port=$(read_profile_var "$p" REMOTE_PORT 22)
|
||
dir=$(read_profile_var "$p" REMOTE_DIR "")
|
||
|
||
# 跳过 server 角色(server 的 result 不是计算产物,且 seeds 另行备份)
|
||
if [ "${role}" = "server" ]; then
|
||
echo -e "${YELLOW}== ${p##*/}: 角色 server,跳过(seed 备份不属本脚本职责)${NC}"
|
||
return 0
|
||
fi
|
||
# 本地节点:result 就在本机 ./data/result,默认不重复备份
|
||
if [ "${env_mode}" = "local" ]; then
|
||
if [ "${INCLUDE_LOCAL}" = "true" ]; then
|
||
[ -n "${node_id}" ] || node_id="node-local"
|
||
else
|
||
echo -e "${YELLOW}== ${p##*/}: 本地节点(${node_id:-localhost}),result 已在 ./data/result,跳过(--include-local 可纳入备份树)${NC}"
|
||
return 0
|
||
fi
|
||
fi
|
||
# 按节点过滤
|
||
if [ -n "${ONLY_NODE}" ] && [ "${node_id}" != "${ONLY_NODE}" ]; then
|
||
return 0
|
||
fi
|
||
if [ -z "${node_id}" ]; then
|
||
node_id="${u}@${ip}"
|
||
fi
|
||
|
||
local loc=""
|
||
if [ "${env_mode}" = "local" ]; then
|
||
loc="(本机)"
|
||
else
|
||
loc=" ${u}@${ip}:${dir}"
|
||
fi
|
||
echo -e "\n${GREEN}== ${p##*/} → 节点 ${node_id}${NC} ${loc}"
|
||
|
||
local dest_rc="${BACKUP_ROOT}/${node_id}/result"
|
||
if [ "${env_mode}" = "local" ]; then
|
||
# 本机节点:优先 rsync 增量(避免每次 cp -a 全量重拷),无 rsync 才退化 cp -a。
|
||
echo -e " ${BLUE}[→]${NC} 本机复制: ./data/result → ${dest_rc}"
|
||
mkdir -p "${dest_rc}"
|
||
if command -v rsync >/dev/null 2>&1; then
|
||
rsync -a --info=progress2 ./data/result/ "${dest_rc}/"
|
||
else
|
||
cp -a ./data/result/. "${dest_rc}/"
|
||
fi
|
||
else
|
||
sync_dir "${u}" "${ip}" "${port}" "${dir}/data/result" "${dest_rc}" || return 1
|
||
fi
|
||
echo -e " ${GREEN}[√]${NC} result 已同步,共 $(ls -1 "${dest_rc}" 2>/dev/null | grep -c . || true) 个网格点子目录"
|
||
|
||
# 可选:一并拉取 data/work 沙盒残留(未清理的计算现场,含完整过程文件)
|
||
if [ "${WITH_WORK}" = "true" ]; then
|
||
local dest_wk="${BACKUP_ROOT}/${node_id}/work"
|
||
if [ "${env_mode}" = "local" ]; then
|
||
echo -e " ${BLUE}[→]${NC} 本机复制: ./data/work → ${dest_wk}"
|
||
mkdir -p "${dest_wk}"
|
||
if command -v rsync >/dev/null 2>&1; then
|
||
rsync -a ./data/work/ "${dest_wk}/" 2>/dev/null || true
|
||
else
|
||
cp -a ./data/work/. "${dest_wk}/" 2>/dev/null || true
|
||
fi
|
||
else
|
||
sync_dir "${u}" "${ip}" "${port}" "${dir}/data/work" "${dest_wk}" || true
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# =============================================================================
|
||
# 交互式选择(无参数或 -i 时进入,风格对齐 deploy.sh 的 Profile 向导)
|
||
# =============================================================================
|
||
interactive_select() {
|
||
# 构建可同步列表:非 *.example 且 DEPLOY_ROLE=node 的 profile(server 不在菜单)
|
||
local menu=()
|
||
for p in deploy.env.d/*.env; do
|
||
[ -f "$p" ] || continue
|
||
[[ "$p" != *.example ]] || continue
|
||
if [ "$(read_profile_var "$p" DEPLOY_ROLE node)" = "node" ]; then
|
||
menu+=("$p")
|
||
fi
|
||
done
|
||
if [ ${#menu[@]} -eq 0 ]; then
|
||
echo -e "${RED}错误: deploy.env.d/ 下没有可用的 node profile${NC}"
|
||
exit 1
|
||
fi
|
||
|
||
echo -e "${BLUE}==============================================================${NC}"
|
||
echo -e "${CYAN} 🚀 DCTS 计算结果产物下载控制台 ${NC}"
|
||
echo -e "${BLUE}==============================================================${NC}"
|
||
echo -e "${YELLOW}【节点选择】检测到 ${#menu[@]} 个可用的计算节点 Profile 文件:${NC}"
|
||
local i p node_id env_mode ip user
|
||
i=1
|
||
for p in "${menu[@]}"; do
|
||
node_id=$(read_profile_var "$p" DCTS_NODE_ID)
|
||
env_mode=$(read_profile_var "$p" DEPLOY_ENV "")
|
||
ip=$(read_profile_var "$p" REMOTE_IP "")
|
||
user=$(read_profile_var "$p" REMOTE_USER "fmq")
|
||
[ -n "$node_id" ] || node_id="${user}@${ip:-localhost}"
|
||
local tag="远程" ipshow="$ip"
|
||
if [ "${env_mode}" = "local" ]; then tag="本机"; ipshow="(本机)"; fi
|
||
printf " %d) %s (%s) %s@%s [%s]\n" "$i" "$node_id" "${p##*/}" "$user" "$ipshow" "$tag"
|
||
((i++))
|
||
done
|
||
echo -e " a) [全部节点] 依次同步上述全部 ${#menu[@]} 个节点"
|
||
read -r -p "请选择节点 [序号如 1,2 或 1-2; 全部如 a; 默认 0=退出]: " CHOICE
|
||
CHOICE=${CHOICE:-0}
|
||
|
||
SELECTED_PROFILES=()
|
||
if [[ "${CHOICE,,}" == "a" ]] || [[ "${CHOICE,,}" == "all" ]]; then
|
||
SELECTED_PROFILES=("${menu[@]}")
|
||
elif [[ "${CHOICE}" =~ ^[0-9] ]]; then
|
||
local part from to n
|
||
IFS=',' read -r -a ADDR <<< "${CHOICE}"
|
||
for part in "${ADDR[@]}"; do
|
||
if [[ "$part" =~ ^([0-9]+)-([0-9]+)$ ]]; then
|
||
from="${BASH_REMATCH[1]}"; to="${BASH_REMATCH[2]}"
|
||
for ((n=from; n<=to; n++)); do
|
||
if [ "$n" -ge 1 ] && [ "$n" -le "${#menu[@]}" ]; then
|
||
SELECTED_PROFILES+=("${menu[$((n-1))]}")
|
||
fi
|
||
done
|
||
elif [[ "$part" =~ ^[0-9]+$ ]]; then
|
||
if [ "$part" -ge 1 ] && [ "$part" -le "${#menu[@]}" ]; then
|
||
SELECTED_PROFILES+=("${menu[$((part-1))]}")
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
|
||
if [ ${#SELECTED_PROFILES[@]} -eq 0 ]; then
|
||
echo -e "${YELLOW}未选择任何节点,退出。${NC}"
|
||
exit 0
|
||
fi
|
||
|
||
# 是否连带同步 data/work 沙盒残留
|
||
if [ "${WITH_WORK}" != "true" ]; then
|
||
read -r -p "是否连带同步 data/work 沙盒残留(未清理的计算现场,含完整过程文件)? [y/N]: " WK_CHOICE
|
||
[[ "${WK_CHOICE,,}" == "y" || "${WK_CHOICE,,}" == "yes" ]] && WITH_WORK=true
|
||
fi
|
||
|
||
# 用户显式勾选本机节点 → 自动纳入备份树(否则 backup_node 会跳过本机)
|
||
local p2 env2
|
||
for p2 in "${SELECTED_PROFILES[@]}"; do
|
||
env2=$(read_profile_var "$p2" DEPLOY_ENV "")
|
||
[ "${env2}" = "local" ] && INCLUDE_LOCAL=true
|
||
done
|
||
|
||
echo -e "${CYAN}将同步 ${#SELECTED_PROFILES[@]} 个节点 → ${BACKUP_ROOT}${NC}"
|
||
}
|
||
|
||
if [ "${HAD_ARGS}" = "false" ] || [ "${INTERACTIVE}" = "true" ]; then
|
||
interactive_select
|
||
fi
|
||
|
||
# =============================================================================
|
||
# 主流程:遍历 deploy.env.d/*.env(排除 *.example)
|
||
# =============================================================================
|
||
echo -e "${CYAN}备份根目录: ${BACKUP_ROOT}${NC}"
|
||
echo -e "${CYAN}SSH 复用目录: ${SSH_CONTROL_DIR}${NC}"
|
||
mkdir -p "${SSH_CONTROL_DIR}"
|
||
|
||
profiles=()
|
||
for p in deploy.env.d/*.env; do
|
||
[ -f "$p" ] || continue
|
||
[[ "$p" != *.example ]] || continue
|
||
profiles+=("$p")
|
||
done
|
||
|
||
if [ ${#profiles[@]} -eq 0 ]; then
|
||
echo -e "${RED}错误: deploy.env.d/ 下没有可用的 node profile${NC}"
|
||
exit 1
|
||
fi
|
||
|
||
# 交互式模式下仅同步用户勾选的节点
|
||
if [ ${#SELECTED_PROFILES[@]} -gt 0 ]; then
|
||
profiles=("${SELECTED_PROFILES[@]}")
|
||
fi
|
||
|
||
for p in "${profiles[@]}"; do
|
||
backup_node "$p" || true
|
||
done
|
||
|
||
echo -e "\n${GREEN}===== 全部节点处理完成,备份位于 ${BACKUP_ROOT} =====${NC}"
|