#!/bin/bash # shellcheck shell=bash # shellcheck disable=SC2016 # shellcheck disable=SC2155 Green="\033[32m" Red="\033[31m" Yellow='\033[33m' Font="\033[0m" INFO="[${Green}INFO${Font}]" ERROR="[${Red}ERROR${Font}]" WARN="[${Yellow}WARN${Font}]" function INFO() { echo -e "${INFO} ${1}" } function ERROR() { echo -e "${ERROR} ${1}" } function WARN() { echo -e "${WARN} ${1}" } # 校正设置目录 CONFIG_DIR="${CONFIG_DIR:-/config}" # 记录非系统环境(docker容器表)提供的变量 declare -ga VARS_SET_BY_SCRIPT=() # 环境变量补全 # 优先级: 系统环境变量 -> .env 文件 (即使为空字符串) -> 预设默认值 # 精准适配 Python 端 set_key (quote_mode="always", 单引号包裹, \' 转义) function load_config_from_app_env() { local env_file="${CONFIG_DIR}/app.env" # 定义 ["变量名"]="预设默认值" # 禁止填入 CONFIG_DIR 变量,ACME_ENV_ 开头的变量暂时不处理,还是交由 cert.sh 处理 declare -A vars_and_default_values=( # update.sh ["PIP_PROXY"]="" ["GITHUB_PROXY"]="" ["PROXY_HOST"]="" ["GITHUB_TOKEN"]="" ["MOVIEPILOT_AUTO_UPDATE"]="release" # database ["DB_TYPE"]="sqlite" ["DB_POSTGRESQL_HOST"]="localhost" ["DB_POSTGRESQL_PORT"]="5432" ["DB_POSTGRESQL_DATABASE"]="moviepilot" ["DB_POSTGRESQL_USERNAME"]="moviepilot" ["DB_POSTGRESQL_PASSWORD"]="moviepilot" ["DB_POSTGRESQL_POOL_SIZE"]="20" ["DB_POSTGRESQL_MAX_OVERFLOW"]="30" # cert ["ENABLE_SSL"]="false" ["SSL_DOMAIN"]="" ["NGINX_PORT"]="3000" ["PORT"]="3001" ["NGINX_CLIENT_MAX_BODY_SIZE"]="10m" ) INFO "开始加载配置 (配置文件: ${env_file})..." shopt -s extglob declare -A values_from_env_file if [ -f "${env_file}" ]; then INFO "检测到 ${env_file} 文件,尝试解析..." while IFS= read -r line || [ -n "$line" ]; do if [[ "$line" =~ ^[[:space:]]*# || -z "$line" ]]; then continue fi local key_in_file value_raw_in_file if [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=(.*) ]]; then key_in_file="${BASH_REMATCH[1]}" value_raw_in_file="${BASH_REMATCH[2]}" if [[ -n "${vars_and_default_values[$key_in_file]+_}" ]]; then local temp_val_after_initial_trim temp_val_after_initial_trim="${value_raw_in_file#"${value_raw_in_file%%[![:space:]]*}"}" temp_val_after_initial_trim="${temp_val_after_initial_trim%"${temp_val_after_initial_trim##*[![:space:]]}"}" local val_before_quote_check="${temp_val_after_initial_trim}" if [[ ! ("${temp_val_after_initial_trim:0:1}" == "'" && "${temp_val_after_initial_trim: -1}" == "'") ]]; then if [[ "${temp_val_after_initial_trim}" =~ ^(.*)[[:space:]]+# ]]; then val_before_quote_check="${BASH_REMATCH[1]}" val_before_quote_check="${val_before_quote_check%%+([[:space:]])}" elif [[ "${temp_val_after_initial_trim:0:1}" == "#" ]]; then val_before_quote_check="" fi fi local parsed_value_from_file if [[ "${val_before_quote_check:0:1}" == "'" && "${val_before_quote_check: -1}" == "'" && ${#val_before_quote_check} -ge 2 ]]; then parsed_value_from_file="${val_before_quote_check:1:${#val_before_quote_check}-2}" parsed_value_from_file="${parsed_value_from_file//\\\'/__MP_PARSER_SQUOTE__}" parsed_value_from_file="${parsed_value_from_file//__MP_PARSER_SQUOTE__/\'}" elif [ -z "${val_before_quote_check}" ]; then parsed_value_from_file="" else WARN "位于 ${env_file} 中的键 ${key_in_file} 对应值 ${val_before_quote_check} 未按规范使用单引号包裹,将采用字面量解析。" parsed_value_from_file="${val_before_quote_check}" fi values_from_env_file["${key_in_file}"]="${parsed_value_from_file}" fi else WARN "跳过 ${env_file} 中格式不正确的行: $line" fi done < <(sed -e '1s/^\xEF\xBB\xBF//' -e 's/\r$//g' "${env_file}") INFO "${env_file} 解析完毕。" else INFO "${env_file} 文件不存在,跳过文件加载。" fi INFO "正在根据优先级确定并导出配置值..." for var_name in "${!vars_and_default_values[@]}"; do local fallback_value="${vars_and_default_values[$var_name]}" local final_value local value_source="未设置" # 标志变量是否来自初始环境 local set_by_initial_env=false # 检查变量是否在环境中已设置(可能为空) if eval "[ -n \"\${${var_name}+x}\" ]"; then # 获取其值 final_value="$(eval echo \"\$"${var_name}"\")" value_source="系统环境变量" set_by_initial_env=true elif [[ -n "${values_from_env_file["${var_name}"]+_}" ]]; then final_value="${values_from_env_file["${var_name}"]}" value_source=".env 文件" else final_value="${fallback_value}" value_source="内置默认值" fi # 不论来源如何,都导出变量,以便脚本的其余部分和子进程使用 # (例如 envsubst, mp_update.sh, cert.sh) if declare -gx "${var_name}=${final_value}"; then if [ -z "${final_value}" ]; then INFO "变量 ${var_name}, 值为空 (来源: ${value_source})。" else INFO "变量 ${var_name}, 值: ${final_value} (来源: ${value_source})。" fi # 如果变量不是来自初始环境变量,则记录下来以便稍后 unset if ! ${set_by_initial_env}; then # 检查是否已在数组中,避免重复添加 local found_in_script_vars=false for item in "${VARS_SET_BY_SCRIPT[@]}"; do if [[ "$item" == "$var_name" ]]; then found_in_script_vars=true break fi done if ! ${found_in_script_vars}; then VARS_SET_BY_SCRIPT+=("${var_name}") fi fi else ERROR "导出变量 ${var_name}, 值: '${final_value}'失败 (来源: ${value_source}) " fi done shopt -u extglob INFO "配置加载流程执行完毕。" } # 使用env配置 load_config_from_app_env # 生成HTTPS配置块 if [ "${ENABLE_SSL}" = "true" ]; then export HTTPS_SERVER_CONF=$(cat < /etc/nginx/nginx.conf # 自动更新 cd / source /usr/local/bin/mp_update.sh cd /app || exit # 更改 moviepilot userid 和 groupid groupmod -o -g "${PGID}" moviepilot usermod -o -u "${PUID}" moviepilot # 更改文件权限 chown -R moviepilot:moviepilot \ "${HOME}" \ /app \ /public \ "${CONFIG_DIR}" \ /var/lib/nginx \ /var/log/nginx chown moviepilot:moviepilot /etc/hosts /tmp # 下载浏览器内核 if [[ "$HTTPS_PROXY" =~ ^https?:// ]] || [[ "$HTTPS_PROXY" =~ ^https?:// ]] || [[ "$PROXY_HOST" =~ ^https?:// ]]; then HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$PROXY_HOST}}" gosu moviepilot:moviepilot playwright install chromium else gosu moviepilot:moviepilot playwright install chromium fi # 启动PostgreSQL服务 if [ "${DB_TYPE:-sqlite}" = "postgresql" ]; then INFO "→ 启动PostgreSQL服务..." # 查找PostgreSQL bin目录 POSTGRESQL_BIN_DIR=$(find /usr/lib/postgresql -name "bin" -type d 2>/dev/null | head -1) if [ -n "${POSTGRESQL_BIN_DIR}" ]; then INFO "找到PostgreSQL bin目录: ${POSTGRESQL_BIN_DIR}" else ERROR "未找到PostgreSQL bin目录!" exit 1 fi # 定义PostgreSQL命令执行函数 pg_exec() { su -s /bin/bash moviepilot -c "cd /tmp && PATH='${POSTGRESQL_BIN_DIR}:\$PATH' $*" } # 使用配置目录下的postgresql子目录作为数据目录 POSTGRESQL_DATA_DIR="${CONFIG_DIR}/postgresql" POSTGRESQL_LOG_DIR="${CONFIG_DIR}/logs/postgresql" INFO "数据目录: ${POSTGRESQL_DATA_DIR}" INFO "日志目录: ${POSTGRESQL_LOG_DIR}" # 初始化PostgreSQL数据目录 if [ ! -f "${POSTGRESQL_DATA_DIR}/PG_VERSION" ]; then INFO "初始化PostgreSQL数据目录..." # 确保目录存在 mkdir -p "${POSTGRESQL_DATA_DIR}" "${POSTGRESQL_LOG_DIR}" chown -R moviepilot:moviepilot "${POSTGRESQL_DATA_DIR}" "${POSTGRESQL_LOG_DIR}" INFO "执行 initdb 命令..." pg_exec "initdb -D '${POSTGRESQL_DATA_DIR}'" # 配置PostgreSQL INFO "复制PostgreSQL配置文件..." cp /app/docker/pg_hba.conf "${POSTGRESQL_DATA_DIR}/pg_hba.conf" # 使用envsubst处理postgresql.conf模板 export POSTGRESQL_LOG_DIR="${POSTGRESQL_LOG_DIR}" envsubst '${DB_POSTGRESQL_PORT}${POSTGRESQL_LOG_DIR}' < /app/docker/postgresql.conf.template > "${POSTGRESQL_DATA_DIR}/postgresql.conf" # 设置配置文件权限 chown moviepilot:moviepilot "${POSTGRESQL_DATA_DIR}/pg_hba.conf" "${POSTGRESQL_DATA_DIR}/postgresql.conf" chmod 600 "${POSTGRESQL_DATA_DIR}/pg_hba.conf" "${POSTGRESQL_DATA_DIR}/postgresql.conf" else INFO "PostgreSQL数据目录已存在,跳过初始化..." fi # 启动PostgreSQL服务 if ! pg_exec "pg_ctl -D '${POSTGRESQL_DATA_DIR}' -l '${POSTGRESQL_LOG_DIR}/postgresql.log' start"; then ERROR "PostgreSQL 服务启动失败!" ERROR "请检查日志文件: ${POSTGRESQL_LOG_DIR}/postgresql.log" if [ -f "${POSTGRESQL_LOG_DIR}/postgresql.log" ]; then ERROR "PostgreSQL 日志内容:" tail -20 "${POSTGRESQL_LOG_DIR}/postgresql.log" else ERROR "PostgreSQL 日志文件不存在,可能是权限问题或目录不存在" fi exit 1 fi # 等待PostgreSQL启动 INFO "等待PostgreSQL服务启动..." local wait_count=0 until pg_exec "pg_isready -h localhost -p ${DB_POSTGRESQL_PORT:-5432}"; do sleep 1 wait_count=$((wait_count + 1)) if [ $wait_count -gt 30 ]; then ERROR "PostgreSQL 服务启动超时,请检查日志文件: ${POSTGRESQL_LOG_DIR}/postgresql.log" if [ -f "${POSTGRESQL_LOG_DIR}/postgresql.log" ]; then ERROR "PostgreSQL 日志内容:" tail -20 "${POSTGRESQL_LOG_DIR}/postgresql.log" fi exit 1 fi done # 创建数据库和用户 INFO "创建PostgreSQL数据库和用户..." pg_exec "psql -h localhost -p ${DB_POSTGRESQL_PORT:-5432} -U postgres -c \"CREATE USER ${DB_POSTGRESQL_USERNAME:-moviepilot} WITH PASSWORD '${DB_POSTGRESQL_PASSWORD:-moviepilot}';\" 2>/dev/null || true" pg_exec "psql -h localhost -p ${DB_POSTGRESQL_PORT:-5432} -U postgres -c \"CREATE DATABASE ${DB_POSTGRESQL_DATABASE:-moviepilot} OWNER ${DB_POSTGRESQL_USERNAME:-moviepilot};\" 2>/dev/null || true" pg_exec "psql -h localhost -p ${DB_POSTGRESQL_PORT:-5432} -U postgres -c \"GRANT ALL PRIVILEGES ON DATABASE ${DB_POSTGRESQL_DATABASE:-moviepilot} TO ${DB_POSTGRESQL_USERNAME:-moviepilot};\" 2>/dev/null || true" INFO "PostgreSQL服务启动完成" fi # 证书管理 source /app/docker/cert.sh # 启动前端nginx服务 INFO "→ 启动前端nginx服务..." nginx # 启动docker http proxy nginx if [ -S "/var/run/docker.sock" ]; then INFO "→ 启动 Docker Proxy..." nginx -c /etc/nginx/docker_http_proxy.conf # 上面nginx是通过root启动的,会将目录权限改成root,所以需要重新再设置一遍权限 chown -R moviepilot:moviepilot \ /var/lib/nginx \ /var/log/nginx fi # 设置后端服务权限掩码 umask "${UMASK}" # 清除非系统环境导入的变量,保证转移到 dumb-init 的时候,不会带入不必要的环境变量 INFO "准备为 Python 应用清理的非系统环境导入的变量..." if [ ${#VARS_SET_BY_SCRIPT[@]} -gt 0 ]; then for var_to_unset in "${VARS_SET_BY_SCRIPT[@]}"; do # 再次确认变量确实存在于当前环境中(虽然理论上应该存在) if eval "[ -n \"\${${var_to_unset}+x}\" ]"; then INFO "取消设置环境变量: ${var_to_unset}" unset "${var_to_unset}" else WARN "变量 ${var_to_unset} 已不存在,无需取消设置。" fi done else INFO "没有由非系统环境导入的变量需要清理。" fi # 启动后端服务 INFO "→ 启动后端服务..." exec dumb-init gosu moviepilot:moviepilot python3 app/main.py