mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/jxxghp/MoviePilot.git
synced 2026-09-20 08:03:34 +08:00
233 lines
8.4 KiB
Bash
Executable File
233 lines
8.4 KiB
Bash
Executable File
#!/bin/bash
|
||
# shellcheck shell=bash
|
||
|
||
set -u
|
||
|
||
# root bootstrap 只使用镜像系统工具;业务入口保留用户 PATH 优先级,同时确保镜像系统工具可用。
|
||
LAUNCHER_INHERITED_PATH="${PATH:-}"
|
||
LAUNCHER_ENTRYPOINT_PATH="${LAUNCHER_INHERITED_PATH:+${LAUNCHER_INHERITED_PATH}:}/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||
export PATH
|
||
|
||
SOURCE_CONTROL_DIR="${MOVIEPILOT_SOURCE_CONTROL_DIR:-/app/docker}"
|
||
IMAGE_CONTROL_DIR="${MOVIEPILOT_IMAGE_CONTROL_DIR:-/usr/local/lib/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_REQUIRED_FILES=(entrypoint.sh update.sh browser.sh cert.sh)
|
||
|
||
function collect_control_files() {
|
||
local control_dir="$1"
|
||
local path
|
||
local file
|
||
|
||
CONTROL_FILES=()
|
||
for path in "${control_dir}"/*.sh; do
|
||
[ -e "${path}" ] || [ -L "${path}" ] || continue
|
||
file="$(basename "${path}")"
|
||
[ "${file}" = "launcher.sh" ] && continue
|
||
[ -f "${path}" ] || return 1
|
||
[ ! -L "${path}" ] || return 1
|
||
done
|
||
while IFS= read -r path; do
|
||
CONTROL_FILES+=("$(basename "${path}")")
|
||
done < <(find "${control_dir}" -maxdepth 1 -type f -name '*.sh' ! -name 'launcher.sh' -print | LC_ALL=C sort)
|
||
for path in "${CONTROL_REQUIRED_FILES[@]}"; do
|
||
[ -f "${control_dir}/${path}" ] || return 1
|
||
[ ! -L "${control_dir}/${path}" ] || return 1
|
||
[[ " ${CONTROL_FILES[*]} " = *" ${path} "* ]] || return 1
|
||
done
|
||
[ "${#CONTROL_FILES[@]}" -ge "${#CONTROL_REQUIRED_FILES[@]}" ]
|
||
}
|
||
|
||
function control_bundle_generation() {
|
||
local control_dir="$1"
|
||
local file
|
||
local checksum_line
|
||
local digest
|
||
local manifest=""
|
||
|
||
collect_control_files "${control_dir}" || return 1
|
||
for file in "${CONTROL_FILES[@]}"; do
|
||
[ -f "${control_dir}/${file}" ] || return 1
|
||
[ ! -L "${control_dir}/${file}" ] || return 1
|
||
bash -n "${control_dir}/${file}" || return 1
|
||
done
|
||
|
||
for file in "${CONTROL_FILES[@]}"; do
|
||
checksum_line="$(sha256sum "${control_dir}/${file}")" || return 1
|
||
digest="${checksum_line%% *}"
|
||
[[ "${digest}" =~ ^[0-9a-f]{64}$ ]] || return 1
|
||
manifest+="${file} ${digest}"$'\n'
|
||
done
|
||
|
||
checksum_line="$(printf '%s' "${manifest}" | sha256sum)" || return 1
|
||
digest="${checksum_line%% *}"
|
||
[[ "${digest}" =~ ^[0-9a-f]{64}$ ]] || return 1
|
||
printf '%s\n' "${digest}"
|
||
}
|
||
|
||
function source_bundle_is_trusted() {
|
||
local control_dir="$1"
|
||
local path
|
||
local mode
|
||
local ancestor
|
||
|
||
[ -d "${control_dir}" ] || return 1
|
||
ancestor="${control_dir}"
|
||
while [ "${ancestor}" != "/" ]; do
|
||
[ ! -L "${ancestor}" ] || return 1
|
||
[ "$(stat -c '%u' "${ancestor}" 2>/dev/null || stat -f '%u' "${ancestor}")" = "0" ] || return 1
|
||
mode="$(stat -c '%a' "${ancestor}" 2>/dev/null || stat -f '%Lp' "${ancestor}")"
|
||
(( (8#${mode} & 8#022) == 0 )) || return 1
|
||
ancestor="$(dirname "${ancestor}")"
|
||
done
|
||
|
||
collect_control_files "${control_dir}" || return 1
|
||
for path in "${CONTROL_FILES[@]/#/${control_dir}/}"; do
|
||
[ -f "${path}" ] || return 1
|
||
[ ! -L "${path}" ] || return 1
|
||
[ "$(stat -c '%u' "${path}" 2>/dev/null || stat -f '%u' "${path}")" = "0" ] || return 1
|
||
mode="$(stat -c '%a' "${path}" 2>/dev/null || stat -f '%Lp' "${path}")"
|
||
(( (8#${mode} & 8#022) == 0 )) || return 1
|
||
done
|
||
|
||
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)
|
||
[ -e "${UPDATE_PREVIOUS_APP}" ] || return 1
|
||
if source_bundle_is_trusted "${previous_control_dir}"; then
|
||
printf '%s\n' "${previous_control_dir}"
|
||
else
|
||
# pending 恢复只能使用旧代或镜像控制脚本,不能执行尚未提交的新源码控制脚本。
|
||
printf '%s\n' "MoviePilot 旧代控制脚本不完整或不可信,回退到镜像内置版本。" >&2
|
||
printf '%s\n' "${IMAGE_CONTROL_DIR}"
|
||
fi
|
||
return 0
|
||
;;
|
||
dependencies|blocked)
|
||
# 依赖阶段可能已切换到包含新迁移的当前 /app;必须使用当前或镜像控制脚本,
|
||
# 让 update.sh 保留新代际,而不是先执行旧代控制脚本触发数据库不兼容回退。
|
||
return 1
|
||
;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
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
|
||
printf '%s\n' "${SOURCE_CONTROL_DIR}"
|
||
return 0
|
||
fi
|
||
|
||
printf '%s\n' "MoviePilot 源码控制脚本不完整或不可信,回退到镜像内置版本。" >&2
|
||
control_bundle_generation "${IMAGE_CONTROL_DIR}" >/dev/null || {
|
||
printf '%s\n' "MoviePilot 镜像内置控制脚本无效,无法启动。" >&2
|
||
return 1
|
||
}
|
||
printf '%s\n' "${IMAGE_CONTROL_DIR}"
|
||
}
|
||
|
||
function materialize_control_bundle() {
|
||
local source_dir="$1"
|
||
local generation="$2"
|
||
local runtime_dir="${RUNTIME_ROOT}/${generation}"
|
||
local staging_dir="${runtime_dir}.tmp.$$"
|
||
local file
|
||
local staged_generation
|
||
|
||
collect_control_files "${source_dir}" || return 1
|
||
mkdir -p "${RUNTIME_ROOT}" || return 1
|
||
rm -rf "${staging_dir}" || return 1
|
||
mkdir -m 0700 "${staging_dir}" || return 1
|
||
for file in "${CONTROL_FILES[@]}"; do
|
||
if ! cp "${source_dir}/${file}" "${staging_dir}/${file}" \
|
||
|| ! chmod 0500 "${staging_dir}/${file}"; then
|
||
rm -rf "${staging_dir}"
|
||
return 1
|
||
fi
|
||
done
|
||
if ! staged_generation="$(control_bundle_generation "${staging_dir}")" \
|
||
|| [ "${staged_generation}" != "${generation}" ]; then
|
||
rm -rf "${staging_dir}"
|
||
return 1
|
||
fi
|
||
if ! rm -rf "${runtime_dir}" || ! mv "${staging_dir}" "${runtime_dir}"; then
|
||
rm -rf "${staging_dir}"
|
||
return 1
|
||
fi
|
||
printf '%s\n' "${runtime_dir}"
|
||
}
|
||
|
||
function launcher_main() {
|
||
if [ "${1:-}" = "--source-generation" ]; then
|
||
source_bundle_is_trusted "${SOURCE_CONTROL_DIR}" || return 1
|
||
control_bundle_generation "${SOURCE_CONTROL_DIR}"
|
||
return $?
|
||
fi
|
||
|
||
local selected_dir
|
||
local generation
|
||
local runtime_dir
|
||
selected_dir="$(select_control_dir)" || return 1
|
||
generation="$(control_bundle_generation "${selected_dir}")" || return 1
|
||
if ! runtime_dir="$(materialize_control_bundle "${selected_dir}" "${generation}")"; then
|
||
[ "${selected_dir}" != "${IMAGE_CONTROL_DIR}" ] || return 1
|
||
printf '%s\n' "MoviePilot 源码控制脚本快照失败,回退到镜像内置版本。" >&2
|
||
generation="$(control_bundle_generation "${IMAGE_CONTROL_DIR}")" || return 1
|
||
runtime_dir="$(materialize_control_bundle "${IMAGE_CONTROL_DIR}" "${generation}")" || return 1
|
||
fi
|
||
|
||
export MP_CONTROL_DIR="${runtime_dir}"
|
||
export MP_CONTROL_GENERATION="${generation}"
|
||
PATH="${LAUNCHER_ENTRYPOINT_PATH}" exec /bin/bash "${runtime_dir}/entrypoint.sh" "$@"
|
||
}
|
||
|
||
function launch_image_control_fallback() {
|
||
local image_entrypoint="${IMAGE_CONTROL_DIR}/entrypoint.sh"
|
||
|
||
# 运行时快照不可用时仍允许镜像自带版本启动,避免临时目录故障阻断容器恢复。
|
||
control_bundle_generation "${IMAGE_CONTROL_DIR}" >/dev/null || return 1
|
||
printf '%s\n' "MoviePilot 控制脚本快照不可用,直接使用镜像内置版本启动。" >&2
|
||
unset MP_CONTROL_DIR MP_CONTROL_GENERATION
|
||
PATH="${LAUNCHER_ENTRYPOINT_PATH}" exec /bin/bash "${image_entrypoint}" "$@"
|
||
}
|
||
|
||
function launch_with_fallback() {
|
||
if [ "${1:-}" = "--post-update-reexec" ]; then
|
||
export MOVIEPILOT_BOOTSTRAP_UPDATE_DONE=1
|
||
export MOVIEPILOT_BOOTSTRAP_REEXECUTED=1
|
||
shift
|
||
else
|
||
unset MOVIEPILOT_BOOTSTRAP_UPDATE_DONE MOVIEPILOT_BOOTSTRAP_REEXECUTED
|
||
fi
|
||
|
||
launcher_main "$@" || launch_image_control_fallback "$@"
|
||
}
|
||
|
||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||
if [ "${1:-}" = "--source-generation" ]; then
|
||
launcher_main "$@"
|
||
else
|
||
launch_with_fallback "$@"
|
||
fi
|
||
fi
|