mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 07:56:52 +08:00
fix(docker): stabilize bootstrap self-updates (#6354)
This commit is contained in:
Executable
+194
@@ -0,0 +1,194 @@
|
||||
#!/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}"
|
||||
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 select_control_dir() {
|
||||
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
|
||||
Reference in New Issue
Block a user