Files
MoviePilot/docker/launcher.sh

233 lines
8.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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