mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-28 19:47:41 +08:00
feat(system): add staged release updates
This commit is contained in:
+18
-2
@@ -317,8 +317,8 @@ class ConfigModel(BaseModel):
|
||||
ALIPAN_APP_ID: str = "ac1bf04dc9fd4d9aaabb65b4a668d403"
|
||||
|
||||
# ==================== 系统升级配置 ====================
|
||||
# 重启自动升级
|
||||
MOVIEPILOT_AUTO_UPDATE: str = "release"
|
||||
# 开发版仍可在启动时跟踪 v3 分支;Release 更新由后台更新服务管理。
|
||||
MOVIEPILOT_AUTO_UPDATE: str = "false"
|
||||
# 自动检查和更新站点资源包(站点索引、认证等)
|
||||
AUTO_UPDATE_RESOURCE: bool = True
|
||||
|
||||
@@ -899,6 +899,22 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
# Release 已迁移到后台状态机,历史 release/true 不能继续启用启动时更新。
|
||||
if "MOVIEPILOT_AUTO_UPDATE" in data:
|
||||
original_update_mode = data["MOVIEPILOT_AUTO_UPDATE"]
|
||||
normalized_update_mode = (
|
||||
"dev"
|
||||
if str(original_update_mode or "").strip().lower() == "dev"
|
||||
else "false"
|
||||
)
|
||||
if normalized_update_mode != str(original_update_mode):
|
||||
cls.update_env_config(
|
||||
"MOVIEPILOT_AUTO_UPDATE",
|
||||
original_update_mode,
|
||||
normalized_update_mode,
|
||||
)
|
||||
data["MOVIEPILOT_AUTO_UPDATE"] = normalized_update_mode
|
||||
|
||||
# 处理 API_TOKEN 特殊验证
|
||||
if "API_TOKEN" in data:
|
||||
converted_value, needs_update = cls.validate_api_token(
|
||||
|
||||
+31
-99
@@ -23,7 +23,6 @@ class SystemHelper(ConfigReloadMixin):
|
||||
"""
|
||||
系统工具类,提供系统相关的操作和判断
|
||||
"""
|
||||
AUTO_UPDATE_ENABLED_VALUES = {"release", "dev"}
|
||||
CONFIG_WATCH = {
|
||||
"DEBUG",
|
||||
"LOG_LEVEL",
|
||||
@@ -36,7 +35,7 @@ class SystemHelper(ConfigReloadMixin):
|
||||
__system_flag_file = "/var/log/nginx/__moviepilot__"
|
||||
__local_backend_runtime_file = settings.TEMP_PATH / "moviepilot.runtime.json"
|
||||
__local_restart_log_file = settings.LOG_PATH / "moviepilot.restart.stdout.log"
|
||||
__one_shot_update_flag_file = settings.TEMP_PATH / "moviepilot.pending_update"
|
||||
__one_shot_dev_update_flag_file = settings.TEMP_PATH / "moviepilot.pending_dev_update"
|
||||
__docker_restart_intent_file = settings.TEMP_PATH / "moviepilot.intentional_restart"
|
||||
__graceful_shutdown_monitor_lock = threading.Lock()
|
||||
__graceful_shutdown_monitor: Optional[threading.Thread] = None
|
||||
@@ -97,94 +96,41 @@ class SystemHelper(ConfigReloadMixin):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def normalize_auto_update_mode(mode: Optional[str]) -> str:
|
||||
"""
|
||||
统一自动升级模式值,兼容历史 true 表示 release。
|
||||
"""
|
||||
normalized = str(mode or "").strip().lower()
|
||||
return "release" if normalized == "true" else normalized
|
||||
|
||||
@staticmethod
|
||||
def get_auto_update_mode() -> str:
|
||||
"""
|
||||
获取当前配置中的自动升级模式。
|
||||
"""
|
||||
return SystemHelper.normalize_auto_update_mode(
|
||||
settings.MOVIEPILOT_AUTO_UPDATE
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_auto_update_enabled(mode: Optional[str] = None) -> bool:
|
||||
"""
|
||||
判断给定模式或当前配置是否启用了启动时自动升级。
|
||||
"""
|
||||
effective_mode = (
|
||||
SystemHelper.get_auto_update_mode()
|
||||
if mode is None
|
||||
else SystemHelper.normalize_auto_update_mode(mode)
|
||||
)
|
||||
return effective_mode in SystemHelper.AUTO_UPDATE_ENABLED_VALUES
|
||||
|
||||
@staticmethod
|
||||
def queue_one_shot_update(mode: str = "release") -> Tuple[bool, str]:
|
||||
"""
|
||||
写入一次性升级标记,供重启后的启动流程消费。
|
||||
"""
|
||||
effective_mode = SystemHelper.normalize_auto_update_mode(mode)
|
||||
if effective_mode not in SystemHelper.AUTO_UPDATE_ENABLED_VALUES:
|
||||
return False, "升级模式仅支持 release 或 dev"
|
||||
|
||||
def queue_one_shot_dev_update() -> Tuple[bool, str]:
|
||||
"""写入一次性 Dev 更新标记,供本次重启的启动流程消费。"""
|
||||
try:
|
||||
SystemHelper.__one_shot_update_flag_file.parent.mkdir(
|
||||
SystemHelper.__one_shot_dev_update_flag_file.parent.mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
SystemHelper.__one_shot_update_flag_file.write_text(
|
||||
effective_mode, encoding="utf-8"
|
||||
SystemHelper.__one_shot_dev_update_flag_file.write_text(
|
||||
"dev", encoding="utf-8"
|
||||
)
|
||||
logger.info(f"已写入一次性升级标记,模式: {effective_mode}")
|
||||
return True, ""
|
||||
except OSError as err:
|
||||
logger.error(f"写入一次性升级标记失败: {err}")
|
||||
return False, f"写入一次性升级标记失败:{err}"
|
||||
logger.error(f"写入一次性 Dev 更新标记失败: {err}")
|
||||
return False, f"写入一次性 Dev 更新标记失败:{err}"
|
||||
|
||||
@staticmethod
|
||||
def consume_one_shot_update_mode() -> Optional[str]:
|
||||
"""
|
||||
读取并清除一次性升级标记,避免后续启动重复执行。
|
||||
"""
|
||||
path = SystemHelper.__one_shot_update_flag_file
|
||||
def consume_one_shot_dev_update() -> bool:
|
||||
"""读取并删除一次性 Dev 更新标记,确保普通重启不会重复更新。"""
|
||||
path = SystemHelper.__one_shot_dev_update_flag_file
|
||||
if not path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
raw_mode = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError as err:
|
||||
logger.warning(f"读取一次性升级标记失败: {err}")
|
||||
raw_mode = ""
|
||||
|
||||
return False
|
||||
try:
|
||||
mode = path.read_text(encoding="utf-8", errors="replace").strip().lower()
|
||||
path.unlink(missing_ok=True)
|
||||
except OSError as err:
|
||||
logger.warning(f"删除一次性升级标记失败: {err}")
|
||||
|
||||
effective_mode = SystemHelper.normalize_auto_update_mode(raw_mode)
|
||||
if effective_mode not in SystemHelper.AUTO_UPDATE_ENABLED_VALUES:
|
||||
if raw_mode:
|
||||
logger.warning(f"忽略无效的一次性升级模式: {raw_mode}")
|
||||
return None
|
||||
|
||||
logger.info(f"检测到一次性升级标记,模式: {effective_mode}")
|
||||
return effective_mode
|
||||
logger.warning(f"消费一次性 Dev 更新标记失败: {err}")
|
||||
return False
|
||||
return mode == "dev"
|
||||
|
||||
@staticmethod
|
||||
def clear_one_shot_update_flag() -> None:
|
||||
"""
|
||||
删除一次性升级标记。
|
||||
"""
|
||||
def clear_one_shot_dev_update() -> None:
|
||||
"""重启失败时撤销尚未消费的一次性 Dev 更新。"""
|
||||
try:
|
||||
SystemHelper.__one_shot_update_flag_file.unlink(missing_ok=True)
|
||||
SystemHelper.__one_shot_dev_update_flag_file.unlink(missing_ok=True)
|
||||
except OSError as err:
|
||||
logger.warning(f"删除一次性升级标记失败: {err}")
|
||||
logger.warning(f"清理一次性 Dev 更新标记失败: {err}")
|
||||
|
||||
@staticmethod
|
||||
def _spawn_local_restart_helper() -> None:
|
||||
@@ -332,32 +278,18 @@ class SystemHelper(ConfigReloadMixin):
|
||||
return SystemHelper._docker_api_restart()
|
||||
|
||||
@staticmethod
|
||||
def upgrade(mode: str = "release") -> Tuple[bool, str]:
|
||||
"""
|
||||
触发升级并重启。
|
||||
|
||||
- 已开启自动升级时,直接重启,沿用当前配置。
|
||||
- 未开启自动升级时,写入一次性升级标记,供下次启动时执行升级。
|
||||
"""
|
||||
current_mode = SystemHelper.get_auto_update_mode()
|
||||
if SystemHelper.is_auto_update_enabled(current_mode):
|
||||
ret, msg = SystemHelper.restart()
|
||||
if not ret:
|
||||
return ret, msg
|
||||
if current_mode == "dev":
|
||||
return True, "已检测到自动升级模式 dev,正在重启并执行升级"
|
||||
return True, "已检测到自动升级已开启,正在重启并执行升级"
|
||||
|
||||
queued, message = SystemHelper.queue_one_shot_update(mode)
|
||||
if not queued:
|
||||
return False, message
|
||||
|
||||
ret, msg = SystemHelper.restart()
|
||||
def upgrade_dev() -> Tuple[bool, str]:
|
||||
"""保留原 Dev 模式:重启后跟踪当前 v3 开发分支。"""
|
||||
configured_mode = str(settings.MOVIEPILOT_AUTO_UPDATE or "").strip().lower()
|
||||
if configured_mode != "dev":
|
||||
queued, message = SystemHelper.queue_one_shot_dev_update()
|
||||
if not queued:
|
||||
return False, message
|
||||
ret, message = SystemHelper.restart()
|
||||
if not ret:
|
||||
SystemHelper.clear_one_shot_update_flag()
|
||||
return ret, msg
|
||||
effective_mode = SystemHelper.normalize_auto_update_mode(mode)
|
||||
return True, f"已安排一次性 {effective_mode} 升级并重启"
|
||||
SystemHelper.clear_one_shot_dev_update()
|
||||
return False, message
|
||||
return True, "已安排 Dev 更新并重启"
|
||||
|
||||
@staticmethod
|
||||
def _start_graceful_shutdown_monitor():
|
||||
|
||||
Reference in New Issue
Block a user