mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 12:06:51 +08:00
195 lines
7.0 KiB
Bash
Executable File
195 lines
7.0 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}"
|
|
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
|