mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
Merge branch 'v3' of https://github.com/jxxghp/MoviePilot into v3
This commit is contained in:
+14
-1
@@ -577,7 +577,15 @@ render_nginx_config
|
|||||||
cd /
|
cd /
|
||||||
if [ "${MOVIEPILOT_BOOTSTRAP_UPDATE_DONE:-0}" != "1" ]; then
|
if [ "${MOVIEPILOT_BOOTSTRAP_UPDATE_DONE:-0}" != "1" ]; then
|
||||||
source "${MP_CONTROL_DIR:-/usr/local/lib/moviepilot/control}/update.sh"
|
source "${MP_CONTROL_DIR:-/usr/local/lib/moviepilot/control}/update.sh"
|
||||||
run_moviepilot_update
|
if ! recover_pending_update; then
|
||||||
|
ERROR "→ 上一次容器更新未能恢复,容器将保持运行以便执行 moviepilot doctor。"
|
||||||
|
diagnostic_keepalive 1
|
||||||
|
fi
|
||||||
|
if [ "${UPDATE_RECOVERY_COMPLETED:-false}" = "true" ]; then
|
||||||
|
INFO "→ 已恢复到更新前版本,本次启动跳过自动更新。"
|
||||||
|
else
|
||||||
|
run_moviepilot_update
|
||||||
|
fi
|
||||||
export MOVIEPILOT_BOOTSTRAP_UPDATE_DONE=1
|
export MOVIEPILOT_BOOTSTRAP_UPDATE_DONE=1
|
||||||
else
|
else
|
||||||
MOVIEPILOT_UPDATE_RESULT="noop"
|
MOVIEPILOT_UPDATE_RESULT="noop"
|
||||||
@@ -586,6 +594,11 @@ if [ "${ONE_SHOT_UPDATE_APPLIED}" = "true" ]; then
|
|||||||
MOVIEPILOT_AUTO_UPDATE="${MOVIEPILOT_AUTO_UPDATE_ORIGINAL}"
|
MOVIEPILOT_AUTO_UPDATE="${MOVIEPILOT_AUTO_UPDATE_ORIGINAL}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "${UPDATE_RECOVERY_REQUIRED:-false}" = "true" ]; then
|
||||||
|
ERROR "→ 容器更新回滚未完成,容器将保持运行以便执行 moviepilot doctor。"
|
||||||
|
diagnostic_keepalive 1
|
||||||
|
fi
|
||||||
|
|
||||||
maybe_reexec_control_bundle
|
maybe_reexec_control_bundle
|
||||||
cd /app || exit
|
cd /app || exit
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export PATH
|
|||||||
SOURCE_CONTROL_DIR="${MOVIEPILOT_SOURCE_CONTROL_DIR:-/app/docker}"
|
SOURCE_CONTROL_DIR="${MOVIEPILOT_SOURCE_CONTROL_DIR:-/app/docker}"
|
||||||
IMAGE_CONTROL_DIR="${MOVIEPILOT_IMAGE_CONTROL_DIR:-/usr/local/lib/moviepilot/control}"
|
IMAGE_CONTROL_DIR="${MOVIEPILOT_IMAGE_CONTROL_DIR:-/usr/local/lib/moviepilot/control}"
|
||||||
RUNTIME_ROOT="${MOVIEPILOT_RUNTIME_CONTROL_ROOT:-/run/moviepilot/control}"
|
RUNTIME_ROOT="${MOVIEPILOT_RUNTIME_CONTROL_ROOT:-/run/moviepilot/control}"
|
||||||
|
UPDATE_PENDING_FILE="${CONFIG_DIR:-/config}/temp/__update_pending__"
|
||||||
|
UPDATE_PREVIOUS_APP="/app.__update_previous__"
|
||||||
CONTROL_FILES=()
|
CONTROL_FILES=()
|
||||||
CONTROL_REQUIRED_FILES=(entrypoint.sh update.sh browser.sh cert.sh)
|
CONTROL_REQUIRED_FILES=(entrypoint.sh update.sh browser.sh cert.sh)
|
||||||
|
|
||||||
@@ -94,7 +96,33 @@ function source_bundle_is_trusted() {
|
|||||||
control_bundle_generation "${control_dir}" >/dev/null
|
control_bundle_generation "${control_dir}" >/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pending_update_state() {
|
||||||
|
[ -f "${UPDATE_PENDING_FILE}" ] || return 1
|
||||||
|
tr -d '\r\n' < "${UPDATE_PENDING_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function pending_recovery_control_dir() {
|
||||||
|
local state
|
||||||
|
local previous_control_dir="${UPDATE_PREVIOUS_APP}/docker"
|
||||||
|
|
||||||
|
state="$(pending_update_state 2>/dev/null || true)"
|
||||||
|
case "${state}" in
|
||||||
|
prepared|dependencies)
|
||||||
|
[ -e "${UPDATE_PREVIOUS_APP}" ] || return 1
|
||||||
|
source_bundle_is_trusted "${previous_control_dir}" || return 1
|
||||||
|
printf '%s\n' "${previous_control_dir}"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
function select_control_dir() {
|
function select_control_dir() {
|
||||||
|
local recovery_dir
|
||||||
|
if recovery_dir="$(pending_recovery_control_dir)"; then
|
||||||
|
printf '%s\n' "${recovery_dir}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
if source_bundle_is_trusted "${SOURCE_CONTROL_DIR}"; then
|
if source_bundle_is_trusted "${SOURCE_CONTROL_DIR}"; then
|
||||||
printf '%s\n' "${SOURCE_CONTROL_DIR}"
|
printf '%s\n' "${SOURCE_CONTROL_DIR}"
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+265
-83
@@ -26,6 +26,11 @@ export PATH="${VENV_PATH}/bin:$PATH"
|
|||||||
UV_BIN="${UV_BIN:-/usr/local/bin/uv}"
|
UV_BIN="${UV_BIN:-/usr/local/bin/uv}"
|
||||||
|
|
||||||
CONFIG_DIR="${CONFIG_DIR:-/config}"
|
CONFIG_DIR="${CONFIG_DIR:-/config}"
|
||||||
|
APP_DIR=/app
|
||||||
|
PUBLIC_DIR=/public
|
||||||
|
UPDATE_PENDING_FILE="${CONFIG_DIR}/temp/__update_pending__"
|
||||||
|
UPDATE_PREVIOUS_APP="${APP_DIR}.__update_previous__"
|
||||||
|
UPDATE_PREVIOUS_PUBLIC="${PUBLIC_DIR}.__update_previous__"
|
||||||
|
|
||||||
function apply_package_cache_env() {
|
function apply_package_cache_env() {
|
||||||
PACKAGE_CACHE_ROOT="${PACKAGE_CACHE_ROOT:-${CONFIG_DIR}/.cache}"
|
PACKAGE_CACHE_ROOT="${PACKAGE_CACHE_ROOT:-${CONFIG_DIR}/.cache}"
|
||||||
@@ -39,6 +44,10 @@ apply_package_cache_env
|
|||||||
PACKAGE_ENV=()
|
PACKAGE_ENV=()
|
||||||
UV_OPTIONS=()
|
UV_OPTIONS=()
|
||||||
MOVIEPILOT_UPDATE_RESULT="noop"
|
MOVIEPILOT_UPDATE_RESULT="noop"
|
||||||
|
UPDATE_RECOVERY_REQUIRED="false"
|
||||||
|
UPDATE_RECOVERY_COMPLETED="false"
|
||||||
|
DEPENDENCY_SYNC_ATTEMPTED="false"
|
||||||
|
PACKAGE_ROUTE_READY="false"
|
||||||
|
|
||||||
function set_package_proxy_env() {
|
function set_package_proxy_env() {
|
||||||
PACKAGE_ENV=()
|
PACKAGE_ENV=()
|
||||||
@@ -81,20 +90,10 @@ function download_and_unzip() {
|
|||||||
function sync_project_dependencies() {
|
function sync_project_dependencies() {
|
||||||
INFO "检测到依赖变化,正在更新虚拟环境..."
|
INFO "检测到依赖变化,正在更新虚拟环境..."
|
||||||
configure_package_route || return 1
|
configure_package_route || return 1
|
||||||
|
PACKAGE_ROUTE_READY="true"
|
||||||
|
DEPENDENCY_SYNC_ATTEMPTED="true"
|
||||||
INFO "依赖源:${PACKAGE_LOG}"
|
INFO "依赖源:${PACKAGE_LOG}"
|
||||||
local -a uv_cmd=(
|
if ! sync_project_dependencies_for "${TMP_PATH}/App"; then
|
||||||
"${UV_BIN}" sync
|
|
||||||
--project "${TMP_PATH}/App"
|
|
||||||
--locked
|
|
||||||
--inexact
|
|
||||||
--no-dev
|
|
||||||
--no-install-project
|
|
||||||
--python "${VENV_PATH}/bin/python3"
|
|
||||||
)
|
|
||||||
uv_cmd+=("${UV_OPTIONS[@]}")
|
|
||||||
if ! env "${PACKAGE_ENV[@]}" \
|
|
||||||
"UV_PROJECT_ENVIRONMENT=${VENV_PATH}" \
|
|
||||||
"UV_LINK_MODE=copy" "${uv_cmd[@]}"; then
|
|
||||||
ERROR "依赖同步失败,当前程序依赖未完成更新"
|
ERROR "依赖同步失败,当前程序依赖未完成更新"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -102,8 +101,226 @@ function sync_project_dependencies() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dependency_manifests_changed() {
|
function dependency_manifests_changed() {
|
||||||
! cmp -s /app/pyproject.toml "${TMP_PATH}/App/pyproject.toml" \
|
! cmp -s "${APP_DIR}/pyproject.toml" "${TMP_PATH}/App/pyproject.toml" \
|
||||||
|| ! cmp -s /app/uv.lock "${TMP_PATH}/App/uv.lock"
|
|| ! cmp -s "${APP_DIR}/uv.lock" "${TMP_PATH}/App/uv.lock"
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_update_pending() {
|
||||||
|
local state="${1:-prepared}"
|
||||||
|
local pending_dir
|
||||||
|
local pending_tmp
|
||||||
|
|
||||||
|
pending_dir="$(dirname "${UPDATE_PENDING_FILE}")"
|
||||||
|
pending_tmp="${pending_dir}/.__update_pending__.tmp.$$"
|
||||||
|
mkdir -p "${pending_dir}" \
|
||||||
|
&& printf '%s\n' "${state}" > "${pending_tmp}" \
|
||||||
|
&& mv -f "${pending_tmp}" "${UPDATE_PENDING_FILE}" || {
|
||||||
|
rm -f "${pending_tmp}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear_update_pending() {
|
||||||
|
rm -f "${UPDATE_PENDING_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_pending_state() {
|
||||||
|
[ -f "${UPDATE_PENDING_FILE}" ] || return 1
|
||||||
|
tr -d '\r\n' < "${UPDATE_PENDING_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function sync_project_dependencies_for() {
|
||||||
|
local project_dir="$1"
|
||||||
|
local -a uv_cmd=(
|
||||||
|
"${UV_BIN}" sync
|
||||||
|
--project "${project_dir}"
|
||||||
|
--locked
|
||||||
|
--inexact
|
||||||
|
--no-dev
|
||||||
|
--no-install-project
|
||||||
|
--python "${VENV_PATH}/bin/python3"
|
||||||
|
)
|
||||||
|
uv_cmd+=("${UV_OPTIONS[@]}")
|
||||||
|
env "${PACKAGE_ENV[@]}" \
|
||||||
|
"UV_PROJECT_ENVIRONMENT=${VENV_PATH}" \
|
||||||
|
"UV_LINK_MODE=copy" "${uv_cmd[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore_project_dependencies() {
|
||||||
|
if [ "${PACKAGE_ROUTE_READY}" != "true" ]; then
|
||||||
|
configure_package_route || return 1
|
||||||
|
PACKAGE_ROUTE_READY="true"
|
||||||
|
fi
|
||||||
|
INFO "→ 正在恢复更新前的程序依赖..."
|
||||||
|
if ! sync_project_dependencies_for "${APP_DIR}"; then
|
||||||
|
ERROR "依赖回滚失败,保留更新事务标记以便下次启动继续恢复"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
INFO "→ 更新前的程序依赖已恢复"
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup_previous_payload() {
|
||||||
|
rm -rf "${UPDATE_PREVIOUS_APP}" "${UPDATE_PREVIOUS_PUBLIC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore_previous_payload() {
|
||||||
|
local failed="false"
|
||||||
|
|
||||||
|
if [ -e "${UPDATE_PREVIOUS_APP}" ]; then
|
||||||
|
if [ -e "${APP_DIR}" ] && ! rm -rf "${APP_DIR}"; then
|
||||||
|
failed="true"
|
||||||
|
elif ! mv "${UPDATE_PREVIOUS_APP}" "${APP_DIR}"; then
|
||||||
|
failed="true"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -e "${UPDATE_PREVIOUS_PUBLIC}" ]; then
|
||||||
|
if [ -e "${PUBLIC_DIR}" ] && ! rm -rf "${PUBLIC_DIR}"; then
|
||||||
|
failed="true"
|
||||||
|
elif ! mv "${UPDATE_PREVIOUS_PUBLIC}" "${PUBLIC_DIR}"; then
|
||||||
|
failed="true"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ "${failed}" = "false" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollback_update_transaction() {
|
||||||
|
local failed="false"
|
||||||
|
|
||||||
|
if ! restore_previous_payload; then
|
||||||
|
failed="true"
|
||||||
|
fi
|
||||||
|
if [ "${DEPENDENCY_SYNC_ATTEMPTED}" = "true" ] && ! restore_project_dependencies; then
|
||||||
|
failed="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${failed}" = "true" ]; then
|
||||||
|
UPDATE_RECOVERY_REQUIRED="true"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
clear_update_pending
|
||||||
|
cleanup_previous_payload
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function recover_pending_update() {
|
||||||
|
local state
|
||||||
|
state="$(update_pending_state 2>/dev/null || true)"
|
||||||
|
[ -n "${state}" ] || return 0
|
||||||
|
|
||||||
|
if [ "${state}" = "committed" ]; then
|
||||||
|
INFO "→ 清理已完成的容器更新事务"
|
||||||
|
cleanup_previous_payload
|
||||||
|
clear_update_pending
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
WARN "→ 检测到未完成的容器更新事务,正在恢复旧版本"
|
||||||
|
if [ "${state}" = "dependencies" ]; then
|
||||||
|
DEPENDENCY_SYNC_ATTEMPTED="true"
|
||||||
|
fi
|
||||||
|
rollback_update_transaction || return 1
|
||||||
|
UPDATE_RECOVERY_COMPLETED="true"
|
||||||
|
INFO "→ 未完成的容器更新事务已恢复"
|
||||||
|
}
|
||||||
|
|
||||||
|
function existing_resource_dir() {
|
||||||
|
local resource_source_dir="${APP_DIR}/app/application/site"
|
||||||
|
for legacy_resource_dir in "${APP_DIR}/app/infrastructure" "${APP_DIR}/app/adapters/network" "${APP_DIR}/app/helper"; do
|
||||||
|
if [ ! -d "${resource_source_dir}" ] && [ -d "${legacy_resource_dir}" ]; then
|
||||||
|
resource_source_dir="${legacy_resource_dir}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf '%s\n' "${resource_source_dir}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function download_staged_resource() {
|
||||||
|
local url="$1"
|
||||||
|
local destination="$2"
|
||||||
|
local fallback="$3"
|
||||||
|
local label="$4"
|
||||||
|
|
||||||
|
if curl ${CURL_OPTIONS} --fail "${url}" -o "${destination}" \
|
||||||
|
&& [ -s "${destination}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
rm -f "${destination}"
|
||||||
|
if [ -f "${fallback}" ]; then
|
||||||
|
cp -a "${fallback}" "${destination}"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
ERROR "${label} 下载失败且没有可用旧资源"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function stage_runtime_payload() {
|
||||||
|
local stage_app="${TMP_PATH}/App"
|
||||||
|
local stage_plugin_dir="${stage_app}/app/plugins"
|
||||||
|
local stage_resource_dir="${stage_app}/app/application/site"
|
||||||
|
local resource_source_dir
|
||||||
|
local resource_file
|
||||||
|
local python_version
|
||||||
|
local arch
|
||||||
|
local arch_suffix
|
||||||
|
local sites_file
|
||||||
|
|
||||||
|
[ -f "${stage_app}/version.py" ] || return 1
|
||||||
|
[ -f "${stage_app}/pyproject.toml" ] || return 1
|
||||||
|
[ -f "${stage_app}/uv.lock" ] || return 1
|
||||||
|
[ -f "${TMP_PATH}/dist/index.html" ] || return 1
|
||||||
|
|
||||||
|
if [ -d "${APP_DIR}/app/plugins" ]; then
|
||||||
|
rm -rf "${stage_plugin_dir}" || return 1
|
||||||
|
mkdir -p "${stage_plugin_dir}" || return 1
|
||||||
|
if ! cp -a "${APP_DIR}/app/plugins/." "${stage_plugin_dir}/"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
mkdir -p "${stage_plugin_dir}" || return 1
|
||||||
|
fi
|
||||||
|
rm -f "${stage_plugin_dir}/__init__.py"
|
||||||
|
|
||||||
|
resource_source_dir="$(existing_resource_dir)"
|
||||||
|
mkdir -p "${stage_resource_dir}" || return 1
|
||||||
|
if [ -f "${resource_source_dir}/user.sites.v3.bin" ] \
|
||||||
|
&& ! cp -a "${resource_source_dir}/user.sites.v3.bin" "${stage_resource_dir}/"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
for resource_file in "${resource_source_dir}"/sites.cp*; do
|
||||||
|
[ -f "${resource_file}" ] || continue
|
||||||
|
cp -a "${resource_file}" "${stage_resource_dir}/" || return 1
|
||||||
|
done
|
||||||
|
|
||||||
|
python_version="$(python3 -c 'import sys; print(f"cpython-{sys.version_info.major}{sys.version_info.minor}")')" || return 1
|
||||||
|
arch="$(uname -m)"
|
||||||
|
if [ "${arch}" = "aarch64" ]; then
|
||||||
|
arch_suffix="aarch64-linux-gnu"
|
||||||
|
else
|
||||||
|
arch_suffix="x86_64-linux-gnu"
|
||||||
|
fi
|
||||||
|
sites_file="sites.${python_version}-${arch_suffix}.so"
|
||||||
|
download_staged_resource \
|
||||||
|
"${GITHUB_PROXY}https://raw.githubusercontent.com/jxxghp/MoviePilot-Resources/main/resources.v3/user.sites.v3.bin" \
|
||||||
|
"${stage_resource_dir}/user.sites.v3.bin" \
|
||||||
|
"${resource_source_dir}/user.sites.v3.bin" \
|
||||||
|
"user.sites.v3.bin" || return 1
|
||||||
|
download_staged_resource \
|
||||||
|
"${GITHUB_PROXY}https://raw.githubusercontent.com/jxxghp/MoviePilot-Resources/main/resources.v3/${sites_file}" \
|
||||||
|
"${stage_resource_dir}/${sites_file}" \
|
||||||
|
"${resource_source_dir}/${sites_file}" \
|
||||||
|
"${sites_file}" || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function swap_staged_payload() {
|
||||||
|
cleanup_previous_payload || return 1
|
||||||
|
mv "${APP_DIR}" "${UPDATE_PREVIOUS_APP}" || return 1
|
||||||
|
if ! mv "${PUBLIC_DIR}" "${UPDATE_PREVIOUS_PUBLIC}"; then
|
||||||
|
mv "${UPDATE_PREVIOUS_APP}" "${APP_DIR}" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! mv "${TMP_PATH}/App" "${APP_DIR}" || ! mv "${TMP_PATH}/dist" "${PUBLIC_DIR}"; then
|
||||||
|
restore_previous_payload || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# 下载程序资源,$1: 后端版本路径
|
# 下载程序资源,$1: 后端版本路径
|
||||||
@@ -115,11 +332,12 @@ function install_backend_and_download_resources() {
|
|||||||
fi
|
fi
|
||||||
INFO "后端程序下载成功"
|
INFO "后端程序下载成功"
|
||||||
|
|
||||||
# 检查依赖是否有变化
|
# 检查依赖清单,实际同步延后到所有运行载荷准备完成之后。
|
||||||
INFO "→ 检查依赖变化..."
|
INFO "→ 检查依赖变化..."
|
||||||
|
local dependencies_changed="false"
|
||||||
if [ -f "${TMP_PATH}/App/pyproject.toml" ] && [ -f "${TMP_PATH}/App/uv.lock" ]; then
|
if [ -f "${TMP_PATH}/App/pyproject.toml" ] && [ -f "${TMP_PATH}/App/uv.lock" ]; then
|
||||||
if dependency_manifests_changed; then
|
if dependency_manifests_changed; then
|
||||||
sync_project_dependencies || return 1
|
dependencies_changed="true"
|
||||||
else
|
else
|
||||||
INFO "依赖无变化,跳过依赖更新"
|
INFO "依赖无变化,跳过依赖更新"
|
||||||
fi
|
fi
|
||||||
@@ -157,82 +375,46 @@ function install_backend_and_download_resources() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
INFO "前端程序下载成功"
|
INFO "前端程序下载成功"
|
||||||
# 备份插件目录
|
INFO "→ 正在准备插件和站点资源..."
|
||||||
INFO "→ 正在备份插件目录..."
|
if ! stage_runtime_payload; then
|
||||||
if ! rm -rf /plugins \
|
ERROR "更新载荷准备失败,当前程序未替换"
|
||||||
|| ! mkdir -p /plugins \
|
|
||||||
|| ! cp -a /app/app/plugins/* /plugins/; then
|
|
||||||
ERROR "插件目录备份失败,终止更新"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
rm -f /plugins/__init__.py
|
|
||||||
# 备份站点资源
|
# 标记必须先于依赖同步和目录切换写入,进程在任一阶段中断后才能恢复旧代际。
|
||||||
INFO "→ 正在备份站点资源目录..."
|
if ! set_update_pending prepared; then
|
||||||
if ! rm -rf /resources_bakcup || ! mkdir /resources_bakcup; then
|
ERROR "无法记录更新事务,当前程序未替换"
|
||||||
ERROR "站点资源备份目录准备失败,终止更新"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
resource_source_dir=/app/app/application/site
|
|
||||||
for legacy_resource_dir in /app/app/infrastructure /app/app/adapters/network /app/app/helper; do
|
if [ "${dependencies_changed}" = "true" ]; then
|
||||||
if [ ! -d "${resource_source_dir}" ] && [ -d "${legacy_resource_dir}" ]; then
|
if ! set_update_pending dependencies; then
|
||||||
resource_source_dir="${legacy_resource_dir}"
|
ERROR "无法记录依赖更新事务,当前程序未替换"
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
done
|
if ! sync_project_dependencies; then
|
||||||
if [ -f "${resource_source_dir}/user.sites.v3.bin" ]; then
|
ERROR "依赖同步失败,正在恢复更新前的运行环境"
|
||||||
cp -a "${resource_source_dir}/user.sites.v3.bin" /resources_bakcup
|
rollback_update_transaction || true
|
||||||
fi
|
return 1
|
||||||
for resource_file in "${resource_source_dir}"/sites.cp*; do
|
|
||||||
[ -f "${resource_file}" ] && cp -a "${resource_file}" /resources_bakcup
|
|
||||||
done
|
|
||||||
# 清空程序目录
|
|
||||||
if ! rm -rf /app \
|
|
||||||
|| ! mkdir -p /app \
|
|
||||||
|| ! cp -a ${TMP_PATH}/App/* /app/ \
|
|
||||||
|| ! rm -rf /public \
|
|
||||||
|| ! mkdir -p /public \
|
|
||||||
|| ! cp -a ${TMP_PATH}/dist/* /public/; then
|
|
||||||
ERROR "程序文件替换失败,更新未完成"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
INFO "程序部分更新成功,前端版本:${frontend_version},后端版本:${1}"
|
|
||||||
# 恢复插件目录
|
|
||||||
if ! cp -a /plugins/* /app/app/plugins/; then
|
|
||||||
ERROR "插件目录恢复失败,更新未完成"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
# 更新站点资源
|
|
||||||
INFO "→ 开始更新站点资源..."
|
|
||||||
python_version=$(python3 -c 'import sys; print(f"cpython-{sys.version_info.major}{sys.version_info.minor}")')
|
|
||||||
arch=$(uname -m)
|
|
||||||
if [ "$arch" = "aarch64" ]; then
|
|
||||||
arch_suffix="aarch64-linux-gnu"
|
|
||||||
else
|
|
||||||
arch_suffix="x86_64-linux-gnu"
|
|
||||||
fi
|
|
||||||
INFO "当前 Python 版本:${python_version},架构:${arch}"
|
|
||||||
if ! mkdir -p /app/app/application/site; then
|
|
||||||
ERROR "站点资源目录创建失败,更新未完成"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
# 下载 V3 站点索引
|
|
||||||
if ! curl ${CURL_OPTIONS} "${GITHUB_PROXY}https://raw.githubusercontent.com/jxxghp/MoviePilot-Resources/main/resources.v3/user.sites.v3.bin" -o /app/app/application/site/user.sites.v3.bin; then
|
|
||||||
if [ -f /resources_bakcup/user.sites.v3.bin ]; then
|
|
||||||
cp -a /resources_bakcup/user.sites.v3.bin /app/app/application/site/
|
|
||||||
fi
|
fi
|
||||||
WARN "user.sites.v3.bin 下载失败,继续使用旧的资源来启动..."
|
|
||||||
fi
|
fi
|
||||||
# 下载对应平台的 sites 文件
|
|
||||||
sites_file="sites.${python_version}-${arch_suffix}.so"
|
if ! swap_staged_payload; then
|
||||||
if ! curl ${CURL_OPTIONS} "${GITHUB_PROXY}https://raw.githubusercontent.com/jxxghp/MoviePilot-Resources/main/resources.v3/${sites_file}" -o "/app/app/application/site/${sites_file}"; then
|
ERROR "程序文件切换失败,正在恢复更新前的运行环境"
|
||||||
if [ -f "/resources_bakcup/${sites_file}" ]; then
|
rollback_update_transaction || true
|
||||||
cp -a "/resources_bakcup/${sites_file}" /app/app/application/site/
|
return 1
|
||||||
fi
|
|
||||||
WARN "${sites_file} 下载失败,继续使用旧的资源来启动..."
|
|
||||||
fi
|
fi
|
||||||
INFO "站点资源更新成功"
|
if ! set_update_pending committed; then
|
||||||
# 清理临时目录
|
ERROR "无法确认更新事务,正在恢复更新前的运行环境"
|
||||||
|
rollback_update_transaction || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
clear_update_pending
|
||||||
|
cleanup_previous_payload || WARN "更新完成,但旧程序备份清理失败"
|
||||||
rm -rf "${TMP_PATH}"
|
rm -rf "${TMP_PATH}"
|
||||||
MOVIEPILOT_UPDATE_RESULT="updated"
|
MOVIEPILOT_UPDATE_RESULT="updated"
|
||||||
|
INFO "程序更新成功,前端版本:${frontend_version},后端版本:${1}"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+333
-13
@@ -106,6 +106,50 @@ def test_launcher_prefers_complete_trusted_source_bundle(tmp_path: Path) -> None
|
|||||||
assert result.stdout == "source\n"
|
assert result.stdout == "source\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_launcher_uses_previous_generation_during_pending_recovery(tmp_path: Path) -> None:
|
||||||
|
source = tmp_path / "source"
|
||||||
|
image = tmp_path / "image"
|
||||||
|
previous = tmp_path / "previous-app"
|
||||||
|
config = tmp_path / "config"
|
||||||
|
_write_bundle(source, "new")
|
||||||
|
_write_bundle(image, "image")
|
||||||
|
_write_bundle(previous / "docker", "old")
|
||||||
|
(config / "temp").mkdir(parents=True)
|
||||||
|
(config / "temp" / "__update_pending__").write_text("prepared\n", encoding="utf-8")
|
||||||
|
script = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
source {LAUNCHER!s}
|
||||||
|
SOURCE_CONTROL_DIR="$1"
|
||||||
|
IMAGE_CONTROL_DIR="$2"
|
||||||
|
RUNTIME_ROOT="$3"
|
||||||
|
UPDATE_PENDING_FILE="$4"
|
||||||
|
UPDATE_PREVIOUS_APP="$5"
|
||||||
|
source_bundle_is_trusted() {{ control_bundle_generation "$1" >/dev/null; }}
|
||||||
|
launcher_main
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"/bin/bash",
|
||||||
|
"-c",
|
||||||
|
script,
|
||||||
|
"pending-launcher-test",
|
||||||
|
str(source),
|
||||||
|
str(image),
|
||||||
|
str(tmp_path / "run"),
|
||||||
|
str(config / "temp" / "__update_pending__"),
|
||||||
|
str(previous),
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert result.stdout == "old\n"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("missing_file", BASE_CONTROL_FILES)
|
@pytest.mark.parametrize("missing_file", BASE_CONTROL_FILES)
|
||||||
def test_launcher_falls_back_when_source_bundle_is_incomplete(
|
def test_launcher_falls_back_when_source_bundle_is_incomplete(
|
||||||
tmp_path: Path, missing_file: str
|
tmp_path: Path, missing_file: str
|
||||||
@@ -843,15 +887,37 @@ def test_failed_dependency_sync_does_not_replace_program_files(tmp_path: Path) -
|
|||||||
uv_bin = tmp_path / "bin" / "uv"
|
uv_bin = tmp_path / "bin" / "uv"
|
||||||
uv_bin.parent.mkdir(parents=True)
|
uv_bin.parent.mkdir(parents=True)
|
||||||
uv_bin.write_text(
|
uv_bin.write_text(
|
||||||
"#!/bin/bash\nprintf '%s\\n' \"$*\" >> \"${UV_LOG}\"\nexit 1\n",
|
"#!/bin/bash\n"
|
||||||
|
"count_file=\"${UV_COUNT_FILE}\"\n"
|
||||||
|
"count=$(cat \"${count_file}\" 2>/dev/null || printf '0')\n"
|
||||||
|
"count=$((count + 1))\n"
|
||||||
|
"printf '%s' \"${count}\" > \"${count_file}\"\n"
|
||||||
|
"printf '%s\\n' \"$*\" >> \"${UV_LOG}\"\n"
|
||||||
|
"[ \"${count}\" -gt 1 ]\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
uv_bin.chmod(0o755)
|
uv_bin.chmod(0o755)
|
||||||
|
live_app = tmp_path / "app"
|
||||||
|
live_public = tmp_path / "public"
|
||||||
|
(live_app / "app" / "plugins").mkdir(parents=True)
|
||||||
|
(live_app / "app" / "application" / "site").mkdir(parents=True)
|
||||||
|
live_public.mkdir()
|
||||||
|
(live_app / "app" / "old.py").write_text("old", encoding="utf-8")
|
||||||
|
(live_app / "app" / "plugins" / "plugin.py").write_text("plugin", encoding="utf-8")
|
||||||
|
(live_app / "app" / "application" / "site" / "user.sites.v3.bin").write_text(
|
||||||
|
"sites", encoding="utf-8"
|
||||||
|
)
|
||||||
|
(live_app / "pyproject.toml").write_text("old-project", encoding="utf-8")
|
||||||
|
(live_app / "uv.lock").write_text("old-lock", encoding="utf-8")
|
||||||
|
(live_public / "index.html").write_text("old-front", encoding="utf-8")
|
||||||
|
|
||||||
update_tree = tmp_path / "update" / "App"
|
update_tree = tmp_path / "update" / "App"
|
||||||
update_tree.mkdir(parents=True)
|
(update_tree / "app" / "plugins").mkdir(parents=True)
|
||||||
(update_tree / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
|
(update_tree / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
|
||||||
(update_tree / "uv.lock").write_text("version = 1\n", encoding="utf-8")
|
(update_tree / "uv.lock").write_text("version = 1\n", encoding="utf-8")
|
||||||
copy_log = tmp_path / "copies.log"
|
(update_tree / "version.py").write_text("FRONTEND_VERSION = 'v3.0.1'\n", encoding="utf-8")
|
||||||
|
(tmp_path / "update" / "dist").mkdir()
|
||||||
|
(tmp_path / "update" / "dist" / "index.html").write_text("new-front", encoding="utf-8")
|
||||||
uv_log = tmp_path / "uv.log"
|
uv_log = tmp_path / "uv.log"
|
||||||
script = textwrap.dedent(
|
script = textwrap.dedent(
|
||||||
f"""\
|
f"""\
|
||||||
@@ -859,21 +925,39 @@ def test_failed_dependency_sync_does_not_replace_program_files(tmp_path: Path) -
|
|||||||
VENV_PATH="$2"
|
VENV_PATH="$2"
|
||||||
TMP_PATH="$3"
|
TMP_PATH="$3"
|
||||||
UV_BIN="$4"
|
UV_BIN="$4"
|
||||||
COPY_LOG="$5"
|
|
||||||
PIP_PROXY= PROXY_HOST=
|
PIP_PROXY= PROXY_HOST=
|
||||||
|
GITHUB_PROXY= CURL_OPTIONS=
|
||||||
source {UPDATER!s}
|
source {UPDATER!s}
|
||||||
|
APP_DIR="$5"
|
||||||
|
PUBLIC_DIR="$6"
|
||||||
|
UPDATE_PREVIOUS_APP="${{APP_DIR}}.__update_previous__"
|
||||||
|
UPDATE_PREVIOUS_PUBLIC="${{PUBLIC_DIR}}.__update_previous__"
|
||||||
INFO() {{ :; }}
|
INFO() {{ :; }}
|
||||||
WARN() {{ :; }}
|
WARN() {{ :; }}
|
||||||
ERROR() {{ :; }}
|
ERROR() {{ :; }}
|
||||||
download_and_unzip() {{ return 0; }}
|
download_and_unzip() {{ return 0; }}
|
||||||
|
curl() {{
|
||||||
|
local output=""
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
if [ "$1" = "-o" ]; then
|
||||||
|
output="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf 'resource\n' > "${{output}}"
|
||||||
|
}}
|
||||||
cmp() {{ return 1; }}
|
cmp() {{ return 1; }}
|
||||||
cp() {{ printf '%s\n' "$*" >> "${{COPY_LOG}}"; }}
|
sed() {{
|
||||||
|
if [[ "$*" == *version.py* ]]; then printf 'v3.0.1\\n'; else command sed "$@"; fi
|
||||||
|
}}
|
||||||
configure_package_route() {{ PACKAGE_LOG=test; PACKAGE_ENV=(); UV_OPTIONS=(); }}
|
configure_package_route() {{ PACKAGE_LOG=test; PACKAGE_ENV=(); UV_OPTIONS=(); }}
|
||||||
install_backend_and_download_resources tags/v3.0.1.zip || true
|
install_backend_and_download_resources tags/v3.0.1.zip
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
subprocess.run(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
"bash",
|
"bash",
|
||||||
"-c",
|
"-c",
|
||||||
@@ -883,21 +967,257 @@ def test_failed_dependency_sync_does_not_replace_program_files(tmp_path: Path) -
|
|||||||
str(tmp_path / "venv"),
|
str(tmp_path / "venv"),
|
||||||
str(tmp_path / "update"),
|
str(tmp_path / "update"),
|
||||||
str(uv_bin),
|
str(uv_bin),
|
||||||
str(copy_log),
|
str(live_app),
|
||||||
|
str(live_public),
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
env={
|
||||||
|
**os.environ,
|
||||||
|
"UV_LOG": str(uv_log),
|
||||||
|
"UV_COUNT_FILE": str(tmp_path / "uv-count"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode != 0
|
||||||
|
assert (live_app / "app" / "old.py").exists()
|
||||||
|
assert (live_public / "index.html").read_text(encoding="utf-8") == "old-front"
|
||||||
|
assert not (tmp_path / "config" / "temp" / "__update_pending__").exists()
|
||||||
|
assert uv_log.exists(), f"stdout={result.stdout!r} stderr={result.stderr!r}"
|
||||||
|
assert uv_log.read_text(encoding="utf-8").splitlines() == [
|
||||||
|
f"sync --project {update_tree} --locked --inexact --no-dev "
|
||||||
|
f"--no-install-project --python {tmp_path / 'venv' / 'bin' / 'python3'}",
|
||||||
|
f"sync --project {live_app} --locked --inexact --no-dev "
|
||||||
|
f"--no-install-project --python {tmp_path / 'venv' / 'bin' / 'python3'}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pending_update_recovers_previous_payload_on_next_start(tmp_path: Path) -> None:
|
||||||
|
live_app = tmp_path / "app"
|
||||||
|
live_public = tmp_path / "public"
|
||||||
|
previous_app = tmp_path / "previous-app"
|
||||||
|
previous_public = tmp_path / "previous-public"
|
||||||
|
(live_app / "app").mkdir(parents=True)
|
||||||
|
live_public.mkdir()
|
||||||
|
(previous_app / "app").mkdir(parents=True)
|
||||||
|
previous_public.mkdir()
|
||||||
|
(live_app / "app" / "new.py").write_text("new", encoding="utf-8")
|
||||||
|
(live_public / "index.html").write_text("new-front", encoding="utf-8")
|
||||||
|
(previous_app / "app" / "old.py").write_text("old", encoding="utf-8")
|
||||||
|
(previous_app / "pyproject.toml").write_text("old-project", encoding="utf-8")
|
||||||
|
(previous_app / "uv.lock").write_text("old-lock", encoding="utf-8")
|
||||||
|
(previous_public / "index.html").write_text("old-front", encoding="utf-8")
|
||||||
|
config_dir = tmp_path / "config"
|
||||||
|
(config_dir / "temp").mkdir(parents=True)
|
||||||
|
(config_dir / "temp" / "__update_pending__").write_text("dependencies\n", encoding="utf-8")
|
||||||
|
uv_bin = tmp_path / "uv"
|
||||||
|
uv_log = tmp_path / "uv.log"
|
||||||
|
uv_bin.write_text(
|
||||||
|
f"#!/bin/bash\nprintf '%s\\n' \"$*\" >> {shlex.quote(str(uv_log))}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
uv_bin.chmod(0o755)
|
||||||
|
script = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
CONFIG_DIR="$1"
|
||||||
|
VENV_PATH="$2"
|
||||||
|
UV_BIN="$7"
|
||||||
|
PIP_PROXY= PROXY_HOST= GITHUB_PROXY= CURL_OPTIONS=
|
||||||
|
source {UPDATER!s}
|
||||||
|
APP_DIR="$3"
|
||||||
|
PUBLIC_DIR="$4"
|
||||||
|
UPDATE_PREVIOUS_APP="$5"
|
||||||
|
UPDATE_PREVIOUS_PUBLIC="$6"
|
||||||
|
INFO() {{ :; }}
|
||||||
|
WARN() {{ :; }}
|
||||||
|
ERROR() {{ :; }}
|
||||||
|
configure_package_route() {{ PACKAGE_ENV=(); UV_OPTIONS=(); }}
|
||||||
|
recover_pending_update
|
||||||
|
printf '%s|%s|%s|%s\\n' \\
|
||||||
|
"$([[ -f "${{APP_DIR}}/app/old.py" ]] && printf old || printf missing)" \\
|
||||||
|
"$([[ -f "${{PUBLIC_DIR}}/index.html" ]] && head -n1 "${{PUBLIC_DIR}}/index.html" || printf missing)" \\
|
||||||
|
"$([[ -f "${{CONFIG_DIR}}/temp/__update_pending__" ]] && printf present || printf cleared)" \\
|
||||||
|
"${{UPDATE_RECOVERY_COMPLETED}}"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
script,
|
||||||
|
"pending-recovery-test",
|
||||||
|
str(config_dir),
|
||||||
|
str(tmp_path / "venv"),
|
||||||
|
str(live_app),
|
||||||
|
str(live_public),
|
||||||
|
str(previous_app),
|
||||||
|
str(previous_public),
|
||||||
|
str(uv_bin),
|
||||||
],
|
],
|
||||||
text=True,
|
text=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=True,
|
check=True,
|
||||||
env={**os.environ, "UV_LOG": str(uv_log)},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not copy_log.exists()
|
assert result.stdout == "old|old-front|cleared|true\n"
|
||||||
assert uv_log.read_text(encoding="utf-8") == (
|
assert uv_log.read_text(encoding="utf-8").startswith(
|
||||||
f"sync --project {update_tree} --locked --inexact --no-dev "
|
f"sync --project {live_app} --locked --inexact --no-dev"
|
||||||
f"--no-install-project --python {tmp_path / 'venv' / 'bin' / 'python3'}\n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_restore_does_not_nest_previous_app_when_current_removal_fails(tmp_path: Path) -> None:
|
||||||
|
live_app = tmp_path / "app"
|
||||||
|
live_public = tmp_path / "public"
|
||||||
|
previous_app = tmp_path / "previous-app"
|
||||||
|
previous_public = tmp_path / "previous-public"
|
||||||
|
(live_app / "app").mkdir(parents=True)
|
||||||
|
live_public.mkdir()
|
||||||
|
(previous_app / "app").mkdir(parents=True)
|
||||||
|
previous_public.mkdir()
|
||||||
|
(live_app / "app" / "new.py").write_text("new", encoding="utf-8")
|
||||||
|
(live_public / "index.html").write_text("new-front", encoding="utf-8")
|
||||||
|
(previous_app / "app" / "old.py").write_text("old", encoding="utf-8")
|
||||||
|
(previous_public / "index.html").write_text("old-front", encoding="utf-8")
|
||||||
|
script = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
source {UPDATER!s}
|
||||||
|
APP_DIR="$1"
|
||||||
|
PUBLIC_DIR="$2"
|
||||||
|
UPDATE_PREVIOUS_APP="$3"
|
||||||
|
UPDATE_PREVIOUS_PUBLIC="$4"
|
||||||
|
CONFIG_DIR="$5"
|
||||||
|
INFO() {{ :; }}
|
||||||
|
WARN() {{ :; }}
|
||||||
|
ERROR() {{ :; }}
|
||||||
|
rm() {{
|
||||||
|
if [[ "$*" == *"${{APP_DIR}}"* ]]; then return 1; fi
|
||||||
|
command rm "$@"
|
||||||
|
}}
|
||||||
|
restore_previous_payload || true
|
||||||
|
printf '%s|%s|%s|%s\n' \\
|
||||||
|
"$([[ -f "${{APP_DIR}}/app/new.py" ]] && printf current || printf missing)" \\
|
||||||
|
"$([[ -f "${{APP_DIR}}/app/old.py" ]] && printf restored || printf absent)" \\
|
||||||
|
"$([[ -d "${{UPDATE_PREVIOUS_APP}}" ]] && printf retained || printf moved)" \\
|
||||||
|
"$([[ -f "${{APP_DIR}}/${{UPDATE_PREVIOUS_APP##*/}}/app/old.py" ]] && printf nested || printf clean)"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"/bin/bash",
|
||||||
|
"-c",
|
||||||
|
script,
|
||||||
|
"rollback-removal-failure-test",
|
||||||
|
str(live_app),
|
||||||
|
str(live_public),
|
||||||
|
str(previous_app),
|
||||||
|
str(previous_public),
|
||||||
|
str(tmp_path / "config"),
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.stdout == "current|absent|retained|clean\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_staged_resource_download_rejects_empty_response_and_keeps_fallback(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
destination = tmp_path / "stage" / "sites.bin"
|
||||||
|
fallback = tmp_path / "live" / "sites.bin"
|
||||||
|
destination.parent.mkdir()
|
||||||
|
fallback.parent.mkdir()
|
||||||
|
fallback.write_text("old-resource\n", encoding="utf-8")
|
||||||
|
script = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
source {UPDATER!s}
|
||||||
|
INFO() {{ :; }}
|
||||||
|
WARN() {{ :; }}
|
||||||
|
ERROR() {{ :; }}
|
||||||
|
curl() {{ return 0; }}
|
||||||
|
download_staged_resource https://resources.example/sites.bin \\
|
||||||
|
"$1" "$2" sites.bin
|
||||||
|
cat "$1"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"/bin/bash",
|
||||||
|
"-c",
|
||||||
|
script,
|
||||||
|
"resource-download-test",
|
||||||
|
str(destination),
|
||||||
|
str(fallback),
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.stdout == "old-resource\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_staged_payload_swap_failure_restores_previous_generation(tmp_path: Path) -> None:
|
||||||
|
live_app = tmp_path / "app"
|
||||||
|
live_public = tmp_path / "public"
|
||||||
|
stage_app = tmp_path / "stage" / "App"
|
||||||
|
stage_public = tmp_path / "stage" / "dist"
|
||||||
|
(live_app / "app").mkdir(parents=True)
|
||||||
|
live_public.mkdir()
|
||||||
|
(stage_app / "app").mkdir(parents=True)
|
||||||
|
stage_public.mkdir(parents=True)
|
||||||
|
(live_app / "app" / "old.py").write_text("old", encoding="utf-8")
|
||||||
|
(live_public / "index.html").write_text("old-front", encoding="utf-8")
|
||||||
|
(stage_app / "app" / "new.py").write_text("new", encoding="utf-8")
|
||||||
|
(stage_public / "index.html").write_text("new-front", encoding="utf-8")
|
||||||
|
config_dir = tmp_path / "config"
|
||||||
|
uv_log = tmp_path / "uv.log"
|
||||||
|
script = textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
CONFIG_DIR="$1"
|
||||||
|
TMP_PATH="$2"
|
||||||
|
PIP_PROXY= PROXY_HOST= GITHUB_PROXY= CURL_OPTIONS=
|
||||||
|
source {UPDATER!s}
|
||||||
|
APP_DIR="$3"
|
||||||
|
PUBLIC_DIR="$4"
|
||||||
|
UPDATE_PREVIOUS_APP="${{APP_DIR}}.__update_previous__"
|
||||||
|
UPDATE_PREVIOUS_PUBLIC="${{PUBLIC_DIR}}.__update_previous__"
|
||||||
|
INFO() {{ :; }}
|
||||||
|
WARN() {{ :; }}
|
||||||
|
ERROR() {{ :; }}
|
||||||
|
mv() {{
|
||||||
|
if [[ "$*" == *"${{TMP_PATH}}/dist"* ]]; then return 1; fi
|
||||||
|
command mv "$@"
|
||||||
|
}}
|
||||||
|
set_update_pending prepared
|
||||||
|
swap_staged_payload || rollback_update_transaction
|
||||||
|
printf '%s|%s|%s\\n' \\
|
||||||
|
"$([[ -f "${{APP_DIR}}/app/old.py" ]] && printf old || printf missing)" \\
|
||||||
|
"$([[ -f "${{PUBLIC_DIR}}/index.html" ]] && head -n1 "${{PUBLIC_DIR}}/index.html" || printf missing)" \\
|
||||||
|
"$([[ -f "${{UPDATE_PENDING_FILE}}" ]] && printf present || printf cleared)"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
script,
|
||||||
|
"payload-swap-test",
|
||||||
|
str(config_dir),
|
||||||
|
str(tmp_path / "stage"),
|
||||||
|
str(live_app),
|
||||||
|
str(live_public),
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.stdout == "old|old-front|cleared\n"
|
||||||
|
|
||||||
|
|
||||||
def test_package_index_probe_is_cacheless_and_bounded(tmp_path: Path) -> None:
|
def test_package_index_probe_is_cacheless_and_bounded(tmp_path: Path) -> None:
|
||||||
timeout_log = tmp_path / "timeout.log"
|
timeout_log = tmp_path / "timeout.log"
|
||||||
script = textwrap.dedent(
|
script = textwrap.dedent(
|
||||||
|
|||||||
Reference in New Issue
Block a user