release: sync public installer asset install.sh

This commit is contained in:
2026-09-03 08:06:56 +00:00
parent f173caeb86
commit cf79c6a1ac
+195 -43
View File
@@ -287,8 +287,75 @@ persist_installer_log() {
chmod 600 "$INSTALL_DIR/install.log" 2>/dev/null || true chmod 600 "$INSTALL_DIR/install.log" 2>/dev/null || true
} }
die() { printf '\n\033[31mxymediavault installer: %s\033[0m\n' "$*" >&2; persist_installer_log; [[ -f ${INSTALL_DIR:-}/install.log ]] && printf '详情日志:%s/install.log\n' "$INSTALL_DIR" >&2 || { [[ -n $INSTALL_LOG ]] && printf '详情日志:%s\n' "$INSTALL_LOG" >&2; }; exit 1; } die() { printf '\n\033[31mxymediavault installer: %s\033[0m\n' "$*" >&2; persist_installer_log; [[ -f ${INSTALL_DIR:-}/install.log ]] && printf '详情日志:%s/install.log\n' "$INSTALL_DIR" >&2 || { [[ -n $INSTALL_LOG ]] && printf '详情日志:%s\n' "$INSTALL_LOG" >&2; }; exit 1; }
component_cache_test_mode() {
local catalog=${XYMEDIA_INSTALL_TEST_CATALOG:?} component platform version url sha archive marker_version marker_sha stage downloaded tarball actual
mkdir -p "$INSTALL_DIR/components" "$INSTALL_DIR/state"
for component in tmm title; do
if [[ $component == tmm ]]; then platform=linux-any
elif [[ -n ${XYMEDIA_INSTALL_TEST_PLATFORM:-} ]]; then platform=$XYMEDIA_INSTALL_TEST_PLATFORM
else case "$(uname -m)" in x86_64|amd64) platform=linux-amd64;; aarch64|arm64) platform=linux-arm64;; *) die "unsupported host architecture: $(uname -m)";; esac
fi
IFS=$'\t' read -r version url sha < <(python3 - "$catalog" "$component" "$platform" <<'PY'
import json, re, sys
from urllib.parse import urlsplit
c = json.load(open(sys.argv[1])); name, platform = sys.argv[2:]
e = c['components'][name]['artifacts'].get(platform) or c['components'][name]['artifacts'].get('linux-any')
if not re.fullmatch(r'[0-9a-fA-F]{64}', e.get('sha256', '')): raise SystemExit(1)
url = e.get('asset_url', '')
parsed = urlsplit(url)
try: port = parsed.port
except ValueError: raise SystemExit(1)
if (parsed.scheme != 'https' or parsed.netloc.lower() != 'git.keeper.work' or
parsed.username is not None or parsed.password is not None or port is not None or
parsed.query or parsed.fragment or '%' in (parsed.path or '') or
re.fullmatch(r'/api/packages/[^/]+/generic/[^/]+/[^/]+/[^/]+', parsed.path or '') is None):
raise SystemExit(1)
print(e['version'], e['asset_url'], e['sha256'].lower(), sep='\t')
PY
) || die '公开目录缺少带 SHA-256 的组件制品'
archive="$INSTALL_DIR/components/$component.tar.zst"; marker_version="$INSTALL_DIR/components/$component.version"; marker_sha="$INSTALL_DIR/components/$component.sha256"
if [[ -s $archive && -f $marker_version && -f $marker_sha && $(<"$marker_version") == "$version" && $(<"$marker_sha") == "$sha" && $(sha256sum "$archive" | cut -d' ' -f1) == "$sha" ]]; then
printf '%s %s 已安装且完整,跳过下载\n' "$([[ $component == tmm ]] && printf TMM || printf Title)" "$version"; continue
fi
stage=$(mktemp -d "$INSTALL_DIR/state/.$component.XXXXXX")
downloaded="$stage/$component.tar.zst"; tarball="$stage/$component.tar"
curl --connect-timeout 10 --max-time 300 --fail --silent --show-error --location --proto '=https' --tlsv1.2 -o "$downloaded" "$url" || die "$component 下载失败"
actual=$(sha256sum "$downloaded" | cut -d' ' -f1); [[ $actual == "$sha" ]] || die "$component SHA256 校验失败"
zstd -dc "$downloaded" >"$tarball" || die "$component 解压失败"
python3 - "$tarball" "$stage/root" <<'PY'
import pathlib, sys, tarfile
archive, target = sys.argv[1:]; pathlib.Path(target).mkdir()
with tarfile.open(archive) as tf:
members = tf.getmembers()
for member in members:
name = member.name.lstrip('./')
if not name or name.startswith('/') or '..' in pathlib.PurePosixPath(name).parts or member.issym() or member.islnk() or not (member.isfile() or member.isdir()): raise SystemExit(1)
member.name = name
tf.extractall(target, members=members, filter='data')
PY
cp "$downloaded" "$archive.tmp"; printf '%s\n' "$version" >"$marker_version.tmp"; printf '%s\n' "$sha" >"$marker_sha.tmp"
mv -f "$archive.tmp" "$archive"; mv -f "$marker_version.tmp" "$marker_version"; mv -f "$marker_sha.tmp" "$marker_sha"; rm -rf "$stage"
done
exit 0
}
if [[ ${XYMEDIA_INSTALL_TEST_COMPONENT_CACHE_ONLY:-0} == 1 ]]; then
INSTALL_DIR=${XYMEDIA_INSTALL_DIR:?XYMEDIA_INSTALL_DIR is required}; component_cache_test_mode
fi
INSTALL_TMP_DIR= INSTALL_TMP_DIR=
cleanup_installer_tmp() { persist_installer_log; [[ -z ${INSTALL_TMP_DIR:-} ]] || rm -rf -- "$INSTALL_TMP_DIR"; } RELEASE_ROLLBACK_ACTIVE=0
RELEASE_TARGET=
RELEASE_BACKUP=
cleanup_installer_tmp() {
if (( RELEASE_ROLLBACK_ACTIVE )) && [[ -n $RELEASE_BACKUP && -d $RELEASE_BACKUP ]]; then
rm -rf -- "$RELEASE_TARGET" 2>/dev/null || true
mv -- "$RELEASE_BACKUP" "$RELEASE_TARGET" 2>/dev/null || true
ln -sfn "releases/$(basename "$RELEASE_TARGET")" "$INSTALL_DIR/releases/current" 2>/dev/null || true
printf '%s\n' '应用 release 失败,已尝试恢复旧 release' >>"${INSTALL_LOG:-/dev/null}"
"${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" stop app >/dev/null 2>&1 || true
"${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" up -d app >/dev/null 2>&1 || true
fi
persist_installer_log; [[ -z ${INSTALL_TMP_DIR:-} ]] || rm -rf -- "$INSTALL_TMP_DIR"
}
trap cleanup_installer_tmp EXIT trap cleanup_installer_tmp EXIT
init_installer_log() { init_installer_log() {
local tmp_parent=${TMPDIR:-/tmp} mode local tmp_parent=${TMPDIR:-/tmp} mode
@@ -356,7 +423,6 @@ tty_out=${XYMEDIA_INSTALL_TEST_TTY_OUTPUT:-/dev/tty}
emit() { emit() {
printf '%s\n' "$1" printf '%s\n' "$1"
printf '%s\n' "$1" >>"$install_log" 2>/dev/null || true printf '%s\n' "$1" >>"$install_log" 2>/dev/null || true
printf '%s\n' "$1" >>"$tty_out" 2>/dev/null || true
} }
emit '正在启动 XyMediaVault 应用...' emit '正在启动 XyMediaVault 应用...'
@@ -390,8 +456,13 @@ if [[ ${XYMEDIA_INSTALL_TEST_POST_MIGRATION_ONLY:-0} == 1 ]]; then
printf '[安装流程] 已完成数据库迁移,继续启动应用\n' >>"$INSTALL_LOG" printf '[安装流程] 已完成数据库迁移,继续启动应用\n' >>"$INSTALL_LOG"
printf '数据库初始化完成\n' printf '数据库初始化完成\n'
printf '数据库初始化完成\n' >>"$INSTALL_LOG" printf '数据库初始化完成\n' >>"$INSTALL_LOG"
if [[ $MENU_PROFILES == *xiaoya-control* ]]; then
printf '正在将本机小雅 Controller 同步到管理平台数据库...\n'
printf '正在将本机小雅 Controller 同步到管理平台数据库...\n' >>"$INSTALL_LOG"
run_visible_logged "${COMPOSE[@]}" --profile migration --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" "${COMPOSE_FUSE[@]}" run --rm -T --no-deps app-migrate bootstrap-controller --config /app/config.yaml </dev/null
fi
progress '启动 XyMediaVault 服务' || true progress '启动 XyMediaVault 服务' || true
start_app_after_migration "${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" "${COMPOSE_FUSE[@]}" up -d app start_app_after_migration "${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" "${COMPOSE_FUSE[@]}" up -d --force-recreate app
app_start_rc=$? app_start_rc=$?
if (( app_start_rc != 0 )); then if (( app_start_rc != 0 )); then
persist_installer_log || true persist_installer_log || true
@@ -954,13 +1025,16 @@ mv -f "$env_tmp" "$INSTALL_DIR/.env"
progress '下载公开版本目录(读取 App、TMM、Title 当前版本)' progress '下载公开版本目录(读取 App、TMM、Title 当前版本)'
catalog_sep='?' catalog_sep='?'
case "$CATALOG_URL" in *\?*) catalog_sep='&';; esac case "$CATALOG_URL" in *\?*) catalog_sep='&';; esac
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 "${CATALOG_URL}${catalog_sep}installer=$INSTALL_NONCE" -o "$INSTALL_DIR/catalog-v1.json" curl --connect-timeout 10 --max-time 60 --fail --silent --show-error --location --proto '=https' --tlsv1.2 "${CATALOG_URL}${catalog_sep}installer=$INSTALL_NONCE" -o "$INSTALL_DIR/catalog-v1.json"
python3 - "$INSTALL_DIR/catalog-v1.json" <<'PY' || die 'invalid public catalog' python3 - "$INSTALL_DIR/catalog-v1.json" <<'PY' || die 'invalid public catalog'
import json, sys import json, sys
import re
catalog = json.load(open(sys.argv[1])) catalog = json.load(open(sys.argv[1]))
assert catalog.get("schema_version") == 1 assert catalog.get("schema_version") == 1
for component in ("app", "tmm", "title", "controller"): for component in ("app", "tmm", "title", "controller"):
assert isinstance(catalog.get("components", {}).get(component, {}).get("artifacts"), dict) assert isinstance(catalog.get("components", {}).get(component, {}).get("artifacts"), dict)
for entry in catalog["components"][component]["artifacts"].values():
assert isinstance(entry, dict) and re.fullmatch(r"[0-9a-fA-F]{64}", entry.get("sha256", ""))
PY PY
case "$(uname -m)" in x86_64|amd64) PLATFORM=linux-amd64;; aarch64|arm64) PLATFORM=linux-arm64;; *) die "unsupported host architecture: $(uname -m)";; esac case "$(uname -m)" in x86_64|amd64) PLATFORM=linux-amd64;; aarch64|arm64) PLATFORM=linux-arm64;; *) die "unsupported host architecture: $(uname -m)";; esac
artifact() { artifact() {
@@ -969,19 +1043,66 @@ import json, sys
catalog, component, platform = sys.argv[1:] catalog, component, platform = sys.argv[1:]
artifacts = json.load(open(catalog))["components"][component]["artifacts"] artifacts = json.load(open(catalog))["components"][component]["artifacts"]
entry = artifacts.get(platform) or artifacts.get("linux-any") entry = artifacts.get(platform) or artifacts.get("linux-any")
if not isinstance(entry, dict) or not isinstance(entry.get("version"), str) or not isinstance(entry.get("asset_url"), str): import re
if not isinstance(entry, dict) or not isinstance(entry.get("version"), str) or not isinstance(entry.get("asset_url"), str) or not isinstance(entry.get("sha256"), str) or re.fullmatch(r"[0-9a-fA-F]{64}", entry["sha256"]) is None:
raise SystemExit(1) raise SystemExit(1)
print(entry["version"] + "\t" + entry["asset_url"]) url = entry["asset_url"]
from urllib.parse import urlsplit
parsed = urlsplit(url)
try:
port = parsed.port
except ValueError:
raise SystemExit(1)
if (
parsed.scheme != "https"
or parsed.netloc.lower() != "git.keeper.work"
or parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
or port is not None
or "%" in (parsed.path or "")
or re.fullmatch(r"/api/packages/[^/]+/generic/[^/]+/[^/]+/[^/]+", parsed.path or "") is None
):
raise SystemExit(1)
print(entry["version"] + "\t" + url + "\t" + entry["sha256"].lower())
PY PY
} }
download_extract() { download_extract() {
local component=$1 local component=$1
local version=$2 local version=$2
local url=$3 local url=$3
local target=$4 local expected_sha=$4
local archive="$INSTALL_DIR/state/$component-$version.archive" local target=$5
local work archive tarball result effective status actual
work=$(mktemp -d "$INSTALL_TMP_DIR/${component}.XXXXXX")
archive="$work/$component.archive"
printf ' 正在下载 %s %s%s\n' "$component" "$version" "${url##*/}" printf ' 正在下载 %s %s%s\n' "$component" "$version" "${url##*/}"
curl --fail --show-error --location --proto '=https' --tlsv1.2 --progress-bar "$url" -o "$archive" result=$(curl --connect-timeout 10 --max-time 300 --fail --show-error --location --proto '=https' --tlsv1.2 --progress-bar -o "$archive" -w '%{url_effective}' "$url") || die "$component 下载失败"
effective=$result
python3 - "$effective" <<'PY' || die "$component 下载重定向不在 canonical Generic Package URL"
import re, sys
from urllib.parse import urlsplit
url = sys.argv[1]
parsed = urlsplit(url)
try:
port = parsed.port
except ValueError:
raise SystemExit(1)
if (
parsed.scheme != "https"
or parsed.netloc.lower() != "git.keeper.work"
or parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
or port is not None
or "%" in (parsed.path or "")
or re.fullmatch(r"/api/packages/[^/]+/generic/[^/]+/[^/]+/[^/]+", parsed.path or "") is None
):
raise SystemExit(1)
PY
actual=$(sha256sum "$archive" | cut -d' ' -f1); [[ $actual == "$expected_sha" ]] || die "$component SHA256 校验失败"
printf ' 正在解压 %s %s\n' "$component" "$version" printf ' 正在解压 %s %s\n' "$component" "$version"
local tarball="$archive.tar" local tarball="$archive.tar"
if [[ $url == *.tar.gz || $url == *.tgz ]]; then gzip -dc "$archive" >"$tarball"; else zstd -dc "$archive" >"$tarball"; fi if [[ $url == *.tar.gz || $url == *.tgz ]]; then gzip -dc "$archive" >"$tarball"; else zstd -dc "$archive" >"$tarball"; fi
@@ -998,9 +1119,11 @@ with tarfile.open(archive, 'r:*') as tf:
if m.issym() or m.islnk() or not (m.isfile() or m.isdir()): if m.issym() or m.islnk() or not (m.isfile() or m.isdir()):
raise SystemExit('archive contains link or special file') raise SystemExit('archive contains link or special file')
m.name = name m.name = name
tf.extractall(target, members=members) tf.extractall(target, members=members, filter='data')
PY PY
cp "$archive" "$target/.download.archive"
rm -f "$tarball" rm -f "$tarball"
rm -rf "$work"
} }
install_controller_binary() { install_controller_binary() {
local source=$1 target="$INSTALL_DIR/controller/xymedia-edge" staged local source=$1 target="$INSTALL_DIR/controller/xymedia-edge" staged
@@ -1017,9 +1140,9 @@ install_controller_binary() {
chmod 755 "$target" chmod 755 "$target"
} }
if (( CONTROLLER_ONLY )); then if (( CONTROLLER_ONLY )); then
IFS=$'\t' read -r controller_version controller_url < <(artifact controller "$PLATFORM") || die '公开目录没有当前架构的 controller 制品' IFS=$'\t' read -r controller_version controller_url controller_sha < <(artifact controller "$PLATFORM") || die '公开目录没有当前架构的 controller 制品'
progress "下载小雅控制器 $controller_version ($PLATFORM)" progress "下载小雅控制器 $controller_version ($PLATFORM)"
download_extract controller "$controller_version" "$controller_url" "$INSTALL_DIR/controller-stage" download_extract controller "$controller_version" "$controller_url" "$controller_sha" "$INSTALL_DIR/controller-stage"
edge=$(find "$INSTALL_DIR/controller-stage" -type f -name xymedia-edge -perm -u+x -print -quit) edge=$(find "$INSTALL_DIR/controller-stage" -type f -name xymedia-edge -perm -u+x -print -quit)
[[ -n $edge ]] || die 'controller archive lacks executable xymedia-edge' [[ -n $edge ]] || die 'controller archive lacks executable xymedia-edge'
install_controller_binary "$edge" install_controller_binary "$edge"
@@ -1030,27 +1153,50 @@ if (( CONTROLLER_ONLY )); then
printf '小雅控制器已安装,监听地址:%s:%s;请按防火墙配置访问,控制器密钥不会显示。\n' "$CONTROLLER_BIND_ADDRESS" "$CONTROLLER_PORT" printf '小雅控制器已安装,监听地址:%s:%s;请按防火墙配置访问,控制器密钥不会显示。\n' "$CONTROLLER_BIND_ADDRESS" "$CONTROLLER_PORT"
exit 0 exit 0
fi fi
IFS=$'\t' read -r APP_VERSION APP_URL < <(artifact app "$PLATFORM") IFS=$'\t' read -r APP_VERSION APP_URL APP_SHA < <(artifact app "$PLATFORM")
APP_STAGE="$INSTALL_DIR/state/app-$APP_VERSION" APP_STAGE=$(mktemp -d "$INSTALL_TMP_DIR/app.XXXXXX")
progress "下载应用包 $APP_VERSION ($PLATFORM)" progress "下载应用包 $APP_VERSION ($PLATFORM)"
download_extract app "$APP_VERSION" "$APP_URL" "$APP_STAGE" download_extract app "$APP_VERSION" "$APP_URL" "$APP_SHA" "$APP_STAGE"
APP_ROOT="$APP_STAGE/xymediavault-$APP_VERSION-$PLATFORM" APP_ROOT="$APP_STAGE/xymediavault-$APP_VERSION-$PLATFORM"
[[ -x $APP_ROOT/bin/xymediavault && -x $APP_ROOT/bin/xymedia-supervisor && -d $APP_ROOT/web/dist && -f $APP_ROOT/release.json ]] || die "app package $APP_VERSION lacks the required xymedia-supervisor; use a newer public app package" [[ -x $APP_ROOT/bin/xymediavault && -x $APP_ROOT/bin/xymedia-supervisor && -d $APP_ROOT/web/dist && -f $APP_ROOT/release.json ]] || die "app package $APP_VERSION lacks the required xymedia-supervisor; use a newer public app package"
if [[ -x $APP_ROOT/bin/xymedia-edge ]]; then if [[ -x $APP_ROOT/bin/xymedia-edge ]]; then
install_controller_binary "$APP_ROOT/bin/xymedia-edge" install_controller_binary "$APP_ROOT/bin/xymedia-edge"
fi fi
mkdir -p "$INSTALL_DIR/releases/releases" mkdir -p "$INSTALL_DIR/releases/releases"
[[ ! -e "$INSTALL_DIR/releases/releases/$APP_VERSION" ]] && mv "$APP_ROOT" "$INSTALL_DIR/releases/releases/$APP_VERSION" if [[ -e "$INSTALL_DIR/releases/releases/$APP_VERSION" ]]; then
progress '停止旧版本应用以替换同版本 release'
run_logged "${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" "${COMPOSE_FUSE[@]}" stop app || die '无法停止旧版本应用'
fi
release_target="$INSTALL_DIR/releases/releases/$APP_VERSION"
release_backup="$INSTALL_DIR/releases/.${APP_VERSION}.old.$$"
RELEASE_TARGET=$release_target
RELEASE_BACKUP=$release_backup
[[ ! -L "$release_target" ]] || die 'release 目录不能是符号链接'
mv "$APP_ROOT" "$release_target.new.$$" || die '无法准备 release 替换'
if [[ -e "$release_target" ]]; then
mv "$release_target" "$release_backup" || die '无法备份旧 release'
fi
mv "$release_target.new.$$" "$release_target" || { mv "$release_backup" "$release_target" 2>/dev/null || true; die '无法安装新 release'; }
RELEASE_ROLLBACK_ACTIVE=1
ln -sfn "releases/$APP_VERSION" "$INSTALL_DIR/releases/current" ln -sfn "releases/$APP_VERSION" "$INSTALL_DIR/releases/current"
if (( ! SKIP_COMPONENTS )); then if (( ! SKIP_COMPONENTS )); then
progress '下载 TMM 与 Title 组件包' progress '下载 TMM 与 Title 组件包'
for component in tmm title; do for component in tmm title; do
platform=$([[ $component == tmm ]] && printf linux-any || printf "$PLATFORM") platform=$([[ $component == tmm ]] && printf linux-any || printf "$PLATFORM")
IFS=$'\t' read -r version url < <(artifact "$component" "$platform") IFS=$'\t' read -r version url sha < <(artifact "$component" "$platform")
download_extract "$component" "$version" "$url" "$INSTALL_DIR/state/$component-$version" archive="$INSTALL_DIR/components/$component.tar.zst"
archive="$INSTALL_DIR/state/$component-$version.archive" marker_version="$INSTALL_DIR/components/$component.version"
[[ -s $archive ]] || die "$component archive was not downloaded" marker_sha="$INSTALL_DIR/components/$component.sha256"
component_root="$INSTALL_DIR/state/$component-$version" if [[ -s $archive && -f $marker_version && -f $marker_sha ]] && [[ $(<"$marker_version") == "$version" ]] && [[ $(<"$marker_sha") == "$sha" ]] && [[ $(sha256sum "$archive" | cut -d' ' -f1) == "$sha" ]]; then
printf '%s %s 已安装且完整,跳过下载\n' "$([[ $component == tmm ]] && printf TMM || printf Title)" "$version"
continue
fi
component_stage=$(mktemp -d "$INSTALL_TMP_DIR/$component.XXXXXX")
download_extract "$component" "$version" "$url" "$sha" "$component_stage"
archive_tmp="$INSTALL_TMP_DIR/$component.archive"
cp "$component_stage/.download.archive" "$archive_tmp"
[[ -s $archive_tmp ]] || die "$component archive was not retained"
component_root="$component_stage"
if [[ ! -d "$component_root/payload" ]]; then if [[ ! -d "$component_root/payload" ]]; then
component_root=$(find "$component_root" -mindepth 1 -maxdepth 1 -type d | head -n 1) component_root=$(find "$component_root" -mindepth 1 -maxdepth 1 -type d | head -n 1)
[[ -n $component_root && -d "$component_root/payload" ]] || die "$component archive has no payload directory" [[ -n $component_root && -d "$component_root/payload" ]] || die "$component archive has no payload directory"
@@ -1061,7 +1207,10 @@ if (( ! SKIP_COMPONENTS )); then
else else
for required in payload/python/bin/python payload/service/entrypoint.py payload/service/app.py payload/service/engine.json payload/NOTICE payload/licenses/guessit-LICENSE.txt payload/licenses/flask-LICENSE.txt payload/licenses/gunicorn-LICENSE.txt; do [[ -f "$component_root/$required" ]] || die "Title archive is missing $required"; done for required in payload/python/bin/python payload/service/entrypoint.py payload/service/app.py payload/service/engine.json payload/NOTICE payload/licenses/guessit-LICENSE.txt payload/licenses/flask-LICENSE.txt payload/licenses/gunicorn-LICENSE.txt; do [[ -f "$component_root/$required" ]] || die "Title archive is missing $required"; done
fi fi
cp "$archive" "$INSTALL_DIR/components/$component.tar.zst" cp "$archive_tmp" "$archive.tmp"
printf '%s\n' "$version" >"$marker_version.tmp"
printf '%s\n' "$sha" >"$marker_sha.tmp"
mv -f "$archive.tmp" "$archive"; mv -f "$marker_version.tmp" "$marker_version"; mv -f "$marker_sha.tmp" "$marker_sha"
done done
fi fi
CONTROLLER_URL="$CONTROLLER_URL" XIAOYA_CONTAINER="$XIAOYA_CONTAINER" PG_HOST="$PG_HOST" PG_PORT="$PG_PORT" PG_DB="$PG_DB" PG_USER="$PG_USER" python3 - "$CONFIG_TEMPLATE" "$INSTALL_DIR/config.yaml" <<'PY' CONTROLLER_URL="$CONTROLLER_URL" XIAOYA_CONTAINER="$XIAOYA_CONTAINER" PG_HOST="$PG_HOST" PG_PORT="$PG_PORT" PG_DB="$PG_DB" PG_USER="$PG_USER" python3 - "$CONFIG_TEMPLATE" "$INSTALL_DIR/config.yaml" <<'PY'
@@ -1245,10 +1394,6 @@ run_migration() {
set +e set +e
run_migration run_migration
migration_rc=$? migration_rc=$?
printf '数据库迁移退出码:%s\n' "$migration_rc"
printf '数据库迁移退出码:%s\n' "$migration_rc" >>"$INSTALL_LOG"
printf '[安装流程] 已完成数据库迁移,继续启动应用\n'
printf '[安装流程] 已完成数据库迁移,继续启动应用\n' >>"$INSTALL_LOG"
if [[ $migration_rc -ne 0 ]]; then if [[ $migration_rc -ne 0 ]]; then
exit_code=1 exit_code=1
persist_installer_log || true persist_installer_log || true
@@ -1259,10 +1404,28 @@ if [[ $migration_rc -ne 0 ]]; then
fi fi
exit "$exit_code" exit "$exit_code"
fi fi
printf '[安装流程] 已完成数据库迁移,继续启动应用\n'
printf '[安装流程] 已完成数据库迁移,继续启动应用\n' >>"$INSTALL_LOG"
printf '数据库初始化完成\n' printf '数据库初始化完成\n'
printf '数据库初始化完成\n' >>"$INSTALL_LOG" printf '数据库初始化完成\n' >>"$INSTALL_LOG"
bootstrap_local_controller() {
[[ $MENU_PROFILES == *xiaoya-control* ]] || return 0
(( REMOTE_MODE == 0 )) || return 0
printf '正在将本机小雅 Controller 同步到管理平台数据库...\n'
printf '正在将本机小雅 Controller 同步到管理平台数据库...\n' >>"$INSTALL_LOG"
if ! run_visible_logged "${COMPOSE[@]}" --profile migration --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" "${COMPOSE_FUSE[@]}" run --rm -T --no-deps app-migrate bootstrap-controller --config /app/config.yaml </dev/null; then
printf '本机小雅 Controller 数据库同步失败,安装未完成。\n' >&2
return 1
fi
}
if bootstrap_local_controller; then
:
else
persist_installer_log || true
exit 1
fi
progress '启动 XyMediaVault 服务' || true progress '启动 XyMediaVault 服务' || true
start_app_after_migration "${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" "${COMPOSE_FUSE[@]}" up -d app start_app_after_migration "${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" "${COMPOSE_FUSE[@]}" up -d --force-recreate app
app_start_rc=$? app_start_rc=$?
if (( app_start_rc != 0 )); then if (( app_start_rc != 0 )); then
exit_code=1 exit_code=1
@@ -1305,24 +1468,13 @@ else
persist_installer_log || true persist_installer_log || true
exit 1 exit 1
fi fi
bootstrap_local_controller() { if (( RELEASE_ROLLBACK_ACTIVE )); then rm -rf -- "$release_backup"; RELEASE_ROLLBACK_ACTIVE=0; fi
[[ $MENU_PROFILES == *xiaoya-control* ]] || return 0
(( REMOTE_MODE == 0 )) || return 0
printf '正在将本机小雅 Controller 同步到管理平台数据库...\n'
if ! run_visible_logged docker exec xymedia-app /releases/current/bin/xymediavault bootstrap-controller --config /app/config.yaml; then
printf '本机小雅 Controller 数据库同步失败,安装未完成。\n' >&2
return 1
fi
}
if bootstrap_local_controller; then
:
else
persist_installer_log || true
exit 1
fi
cp "$INSTALL_LOG" "$INSTALL_DIR/install.log" 2>/dev/null || true cp "$INSTALL_LOG" "$INSTALL_DIR/install.log" 2>/dev/null || true
printf '\033[2K\r[%d/%d] 安装完成:XyMediaVault 已就绪\n' "$TOTAL_STEPS" "$TOTAL_STEPS" printf '\033[2K\r[%d/%d] 安装完成:XyMediaVault 已就绪\n' "$TOTAL_STEPS" "$TOTAL_STEPS"
printf '管理地址:http://127.0.0.1:%s\n' "$API_PORT" management_address=$(printf '%s\n' "$HOST_ADDRESSES_INITIAL" | head -n 1)
[[ -n $management_address ]] || management_address=127.0.0.1
if [[ $management_address == *:* ]]; then management_address="[$management_address]"; fi
printf '管理地址:http://%s:%s\n' "$management_address" "$API_PORT"
if [[ $MENU_PROFILES == *xiaoya-control* ]]; then if [[ $MENU_PROFILES == *xiaoya-control* ]]; then
if print_controller_info; then if print_controller_info; then
: :