feat: show installer progress and logs

This commit is contained in:
2026-08-30 13:05:40 +00:00
parent ef605f8a2d
commit 0537291c04
+48 -9
View File
@@ -15,7 +15,16 @@ EXISTING_DB=0
API_PORT=${XYMEDIA_API_PORT:-18080}
WEBDAV_PORT=${XYMEDIA_WEBDAV_PORT:-18081}
TVBOX_PORT=${XYMEDIA_TVBOX_PORT:-18082}
die() { printf 'xymediavault installer: %s\n' "$*" >&2; exit 1; }
TOTAL_STEPS=9
STEP=0
INSTALL_LOG=
die() { printf '\n\033[31mxymediavault installer: %s\033[0m\n' "$*" >&2; [[ -n $INSTALL_LOG ]] && printf '详情日志:%s\n' "$INSTALL_LOG" >&2; exit 1; }
progress() {
STEP=$((STEP + 1))
printf '\033[2K\r[%d/%d] %s\n' "$STEP" "$TOTAL_STEPS" "$1"
[[ -n $INSTALL_LOG ]] && printf '[%d/%d] %s\n' "$STEP" "$TOTAL_STEPS" "$1" >>"$INSTALL_LOG"
}
run_logged() { "$@" >>"$INSTALL_LOG" 2>&1; }
usage() { sed -n '1,20p' "$0"; }
while (($#)); do
case "$1" in
@@ -31,6 +40,7 @@ while (($#)); do
*) die "unknown option: $1";;
esac
done
progress '检查安装环境'
[[ $INSTALL_DIR = /* && $INSTALL_DIR != / ]] || die 'install directory must be an absolute non-root path'
command -v docker >/dev/null || die 'Docker is required'
if docker compose version >/dev/null 2>&1; then COMPOSE=(docker compose); elif command -v docker-compose >/dev/null; then COMPOSE=(docker-compose); else die 'Docker Compose is required'; fi
@@ -42,7 +52,11 @@ if [[ $PG_HOST != postgres && $EXISTING_DB != 1 ]]; then die 'an external databa
if [[ $PG_HOST != postgres && -z $PG_PASSWORD_FILE ]]; then die 'an external database requires --postgres-password-file'; fi
if ! [[ $API_PORT =~ ^[0-9]+$ && $WEBDAV_PORT =~ ^[0-9]+$ && $TVBOX_PORT =~ ^[0-9]+$ ]]; then die 'ports must be numeric'; fi
if ! [[ $PG_PORT =~ ^[0-9]+$ && $PG_DB =~ ^[A-Za-z0-9_]+$ && $PG_USER =~ ^[A-Za-z0-9_]+$ && $PG_HOST =~ ^[A-Za-z0-9._-]+$ ]]; then die 'invalid PostgreSQL connection values'; fi
progress '准备安装目录与数据库凭据'
mkdir -p "$INSTALL_DIR" "$INSTALL_DIR"/{data/postgres,data,state,components,releases,secrets,config}
INSTALL_LOG="$INSTALL_DIR/install.log"
: >"$INSTALL_LOG"
chmod 600 "$INSTALL_LOG"
chmod 700 "$INSTALL_DIR/secrets"
if [[ -n $PG_PASSWORD_FILE ]]; then
[[ -r $PG_PASSWORD_FILE ]] || die 'password file is not readable'
@@ -74,6 +88,7 @@ COMPOSE_TEMPLATE=$(template_file compose.yaml)
DOCKERFILE_TEMPLATE=$(template_file Dockerfile.bootstrap)
umask 077
printf 'XYMEDIA_INSTALL_DIR=%s\nXYMEDIA_POSTGRES_HOST=%s\nXYMEDIA_POSTGRES_PORT=%s\nXYMEDIA_POSTGRES_DATABASE=%s\nXYMEDIA_POSTGRES_USER=%s\nXYMEDIA_API_PORT=%s\nXYMEDIA_WEBDAV_PORT=%s\nXYMEDIA_TVBOX_PORT=%s\n' "$INSTALL_DIR" "$PG_HOST" "$PG_PORT" "$PG_DB" "$PG_USER" "$API_PORT" "$WEBDAV_PORT" "$TVBOX_PORT" >"$INSTALL_DIR/.env"
progress '下载公开版本目录'
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 "$CATALOG_URL" -o "$INSTALL_DIR/catalog-v1.json"
python3 - "$INSTALL_DIR/catalog-v1.json" <<'PY' || die 'invalid public catalog'
import json, sys
@@ -122,6 +137,7 @@ PY
}
IFS=$'\t' read -r APP_VERSION APP_URL < <(artifact app "$PLATFORM")
APP_STAGE="$INSTALL_DIR/state/app-$APP_VERSION"
progress "下载应用包 $APP_VERSION ($PLATFORM)"
download_extract app "$APP_VERSION" "$APP_URL" "$APP_STAGE"
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"
@@ -129,14 +145,18 @@ mkdir -p "$INSTALL_DIR/releases/releases"
[[ ! -e "$INSTALL_DIR/releases/releases/$APP_VERSION" ]] && mv "$APP_ROOT" "$INSTALL_DIR/releases/releases/$APP_VERSION"
ln -sfn "releases/$APP_VERSION" "$INSTALL_DIR/releases/current"
if (( ! SKIP_COMPONENTS )); then
progress '下载 TMM 与 Title 组件包'
for component in tmm title; do
platform=$([[ $component == tmm ]] && printf linux-any || printf "$PLATFORM")
IFS=$'\t' read -r version url < <(artifact "$component" "$platform")
download_extract "$component" "$version" "$url" "$INSTALL_DIR/state/$component-$version"
archive="$INSTALL_DIR/state/$component-$version.archive"
[[ -s $archive ]] || die "$component archive was not downloaded"
component_root=$(find "$INSTALL_DIR/state/$component-$version" -mindepth 1 -maxdepth 1 -type d | head -n 1)
[[ -n $component_root ]] || die "$component archive has no root directory"
component_root="$INSTALL_DIR/state/$component-$version"
if [[ ! -d "$component_root/payload" ]]; then
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"
fi
if [[ $component == tmm ]]; then
[[ -f "$component_root/payload/xymedia-api.jar" ]] || die 'TMM archive is missing payload/xymedia-api.jar'
compgen -G "$component_root/payload/lib/*" >/dev/null || die 'TMM archive is missing payload/lib files'
@@ -162,11 +182,30 @@ output.write_text(content)
PY
cp "$COMPOSE_TEMPLATE" "$DOCKERFILE_TEMPLATE" "$INSTALL_DIR/"
if [[ ! -f "$INSTALL_DIR/.bootstrap-dockerfile.sha256" || "$(sha256sum "$INSTALL_DIR/Dockerfile.bootstrap" | cut -d' ' -f1)" != "$(<"$INSTALL_DIR/.bootstrap-dockerfile.sha256")" ]]; then
"${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" build app
progress '构建首次 bootstrap runtime'
run_logged "${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" build app || die 'bootstrap runtime 构建失败'
sha256sum "$INSTALL_DIR/Dockerfile.bootstrap" | cut -d' ' -f1 >"$INSTALL_DIR/.bootstrap-dockerfile.sha256"
fi
if [[ $PG_HOST == postgres ]]; then "${COMPOSE[@]}" --profile local-db --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" up -d postgres; fi
if [[ $PG_HOST == postgres ]]; then for _ in {1..60}; do "${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" exec -T postgres pg_isready -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1 && break; sleep 2; done; fi
if (( EXISTING_DB )); then export XYMEDIA_EXTERNAL_MIGRATION_CONFIRM=YES; "${COMPOSE[@]}" --profile migration --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" run --rm --no-deps app-migrate migrate --config /app/config.yaml; else "${COMPOSE[@]}" --profile migration --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" run --rm --no-deps app-migrate migrate --new --config /app/config.yaml; fi
"${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" up -d app
for _ in {1..60}; do curl --fail --silent "http://127.0.0.1:$API_PORT/api/health" >/dev/null && printf 'XyMediaVault is ready at http://127.0.0.1:%s\n' "$API_PORT" && exit 0; sleep 2; done
progress '启动 PostgreSQL'
if [[ $PG_HOST == postgres ]]; then run_logged "${COMPOSE[@]}" --profile local-db --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" up -d postgres || die 'PostgreSQL 启动失败'; fi
if [[ $PG_HOST == postgres ]]; then
ready=0
for elapsed in $(seq 0 118 2); do
"${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" exec -T postgres pg_isready -U "$PG_USER" -d "$PG_DB" >/dev/null 2>&1 && { ready=1; break; }
printf '\033[2K\r[%d/%d] 等待 PostgreSQL 就绪(%s 秒)' "$STEP" "$TOTAL_STEPS" "$elapsed"
sleep 2
done
printf '\n'
[[ $ready = 1 ]] || die 'PostgreSQL 未在限定时间内就绪'
fi
progress '初始化数据库结构'
if (( EXISTING_DB )); then export XYMEDIA_EXTERNAL_MIGRATION_CONFIRM=YES; run_logged "${COMPOSE[@]}" --profile migration --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" run --rm --no-deps app-migrate migrate --config /app/config.yaml || die '已有数据库迁移失败'; else run_logged "${COMPOSE[@]}" --profile migration --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" run --rm --no-deps app-migrate migrate --new --config /app/config.yaml || die '数据库初始化失败'; fi
progress '启动 XyMediaVault 服务'
run_logged "${COMPOSE[@]}" --project-directory "$INSTALL_DIR" --env-file "$INSTALL_DIR/.env" -f "$INSTALL_DIR/compose.yaml" up -d app || die 'XyMediaVault 启动失败'
for elapsed in $(seq 0 118 2); do
curl --fail --silent "http://127.0.0.1:$API_PORT/api/health" >/dev/null && { printf '\033[2K\r[%d/%d] 安装完成:XyMediaVault 已就绪\n' "$TOTAL_STEPS" "$TOTAL_STEPS"; printf '管理地址:http://127.0.0.1:%s\n安装日志:%s\n' "$API_PORT" "$INSTALL_LOG"; exit 0; }
printf '\033[2K\r[%d/%d] 等待 XyMediaVault 健康检查(%s 秒)' "$STEP" "$TOTAL_STEPS" "$elapsed"
sleep 2
done
printf '\n'
die 'XyMediaVault 未在限定时间内通过健康检查'