release: sync public installer asset install.sh
This commit is contained in:
+195
-43
@@ -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
|
||||||
:
|
:
|
||||||
|
|||||||
Reference in New Issue
Block a user