Files
MoviePilot/app/application/plugin/config.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

228 lines
9.8 KiB
Python
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.
"""
插件配置保存、重置和运行态重建应用用例。
"""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, ContextManager
from app.schemas.exception import PluginMutationRejectedError
from app.schemas.plugin import validate_plugin_id
@dataclass(frozen=True, slots=True)
class PluginConfigResult:
"""
描述插件配置写操作是否成功及提示信息。
:param success: 操作是否完成其预期目标
:param message: 操作结果或失败原因的说明,默认空字符串
"""
success: bool
message: str = ""
@dataclass(frozen=True, slots=True)
class PluginPurgeScope:
"""
彻底清理一个实例时用户选定的删除范围。
四项各自独立,由用户逐项勾选:清理动作不可逆,替用户把范围扩大到他没选的
东西上,比让他多点一下要糟得多。
:param config: 是否清理插件配置
:param plugin_data: 是否清理宿主数据库中的插件键值数据
:param own_database: 是否清理插件自建数据库
:param data_directory: 是否清理插件专属数据目录
"""
config: bool = False
plugin_data: bool = False
own_database: bool = False
data_directory: bool = False
@property
def is_empty(self) -> bool:
"""
是否一项都没选。
"""
return not any(
(self.config, self.plugin_data, self.own_database, self.data_directory)
)
@dataclass(frozen=True, slots=True)
class PluginPurgeResult:
"""
描述彻底清理的执行结果与实际清掉的范围。
:param success: 操作是否完成其预期目标
:param message: 操作结果或失败原因的说明,默认空字符串
:param purged: 本次实际完成清理的范围名称
:param instance_removed: 本次是否移除了插件分身实例
"""
success: bool
message: str = ""
purged: tuple[str, ...] = ()
instance_removed: bool = False
class PluginConfigCommand:
"""
协调插件配置持久化、实例初始化和运行时注册刷新。
"""
def __init__(
self,
*,
save_config: Callable[[str, dict[str, object], bool], bool],
initialize: Callable[[str, dict[str, object]], Any],
stop: Callable[[str], Any],
delete_config: Callable[[str, bool], bool],
delete_data: Callable[[str, bool], bool],
reload_runtime: Callable[[str], Any],
publish_reset: Callable[[str], Any],
refresh_registrations: Callable[[str], Any],
mutation: Callable[[str], ContextManager[None]],
delete_plugin_data_rows: Callable[[str], Any],
destroy_own_database: Callable[[str], Any],
delete_data_directory: Callable[[str], bool],
purge_instance: Callable[[str], bool],
is_clone: Callable[[str], bool],
purge_invalid_empty_host: Callable[[str], bool] | None = None,
) -> None:
"""
保存插件管理 Facade 和运行时注册刷新端口。
"""
self._save_config = save_config
self._initialize = initialize
self._stop = stop
self._delete_config = delete_config
self._delete_data = delete_data
self._reload_runtime = reload_runtime
self._publish_reset = publish_reset
self._refresh_registrations = refresh_registrations
self._mutation = mutation
self._delete_plugin_data_rows = delete_plugin_data_rows
self._destroy_own_database = destroy_own_database
self._delete_data_directory = delete_data_directory
self._purge_instance = purge_instance
self._is_clone = is_clone
self._purge_invalid_empty_host = purge_invalid_empty_host
def update(self, plugin_id: str, config: dict[str, object]) -> PluginConfigResult:
"""
保存配置并按既有顺序重新初始化实例及运行时注册。
"""
try:
with self._mutation(f"更新插件 {plugin_id} 配置"):
if not self._save_config(plugin_id, config, False):
return PluginConfigResult(False, "插件配置保存失败")
self._initialize(plugin_id, config)
self._refresh_registrations(plugin_id)
return PluginConfigResult(True)
except PluginMutationRejectedError as error:
return PluginConfigResult(False, str(error))
def purge(self, instance_id: str, scope: PluginPurgeScope) -> PluginPurgeResult:
"""
按用户选定的范围彻底清理一个实例,逐项执行且互不代劳。
与停用的区别在于不可逆:停用只把启用位置假、配置与展示信息原样留着,这里才
真正删除用户数据。因而范围完全由调用方给定,一项没选就直接拒绝,不做「那就
全清吧」这种猜测。
实例 ID 先过格式校验再动任何存储:它会被拼进插件数据目录路径,放行一个带
``..`` 的 ID 会让后续的整目录删除落到插件数据根之外。校验在最前面,是为了让
非法 ID 在停止实例、删配置行之前就被拒绝,而不是删掉一半才发现。唯一例外是
只选 config 时,可通过数据库专用端口原子回收非法 ID 的空本体行;该路径不
停止实例、不清配置、不触碰文件或自有数据库,带任何设置的行均保留。
分身在清理后连同实例行一并消失——分身的存在本身就由那一行表达;本体的行则
保留,它还承载着这个插件的启用状态与展示覆盖,删掉等于把装着的插件静默停用,
想卸载插件是另一条路径。
自有数据库与数据目录有先后:库文件就落在该目录下,不先销毁句柄直接删目录,
Windows 下会因文件占用失败,Linux 下则留下一个仍被写入的已删除文件。选了
删目录就必然先销毁库,不管用户有没有单独勾选它。
这一串动作跨配置行、业务数据表、库文件与目录四种存储,**不是数据库事务**:
中途失败时前面几步已经生效且无法回滚,因而失败回报只承诺「这次没做完」,不
承诺「什么都没动」。目录删除端口在目标不是该实例自己的目录时拒绝执行并抛出
``ValueError``,这里把它收成失败结果而不是让它变成 500。
:param instance_id: 实例 ID
:param scope: 用户选定的删除范围
:return: 执行结果与实际清掉的范围
"""
if scope.is_empty:
return PluginPurgeResult(False, "请至少选择一项要清理的内容")
try:
validate_plugin_id(instance_id)
except ValueError as error:
return self._purge_invalid_host(instance_id, scope, str(error))
try:
with self._mutation(f"彻底清理插件实例 {instance_id}"):
self._stop(instance_id)
purged: list[str] = []
if scope.config:
self._delete_config(instance_id, True)
purged.append("config")
if scope.plugin_data:
self._delete_plugin_data_rows(instance_id)
purged.append("plugin_data")
if scope.own_database or scope.data_directory:
self._destroy_own_database(instance_id)
if scope.own_database:
purged.append("own_database")
if scope.data_directory and self._delete_data_directory(instance_id):
purged.append("data_directory")
instance_removed = (
self._purge_instance(instance_id) if self._is_clone(instance_id) else False
)
return PluginPurgeResult(
True,
purged=tuple(purged),
instance_removed=instance_removed,
)
except PluginMutationRejectedError as error:
return PluginPurgeResult(False, str(error))
except ValueError as error:
return PluginPurgeResult(False, f"清理插件实例 {instance_id} 失败:{error}")
def _purge_invalid_host(
self, instance_id: str, scope: PluginPurgeScope, validation_error: str,
) -> PluginPurgeResult:
"""
仅在用户只选配置且数据库确认是纯残留时回收非法实例行。
"""
if scope != PluginPurgeScope(config=True) or self._purge_invalid_empty_host is None:
return PluginPurgeResult(False, f"实例 ID 非法:{validation_error}")
try:
with self._mutation(f"清理历史无效插件实例 {instance_id}"):
if not self._purge_invalid_empty_host(instance_id):
return PluginPurgeResult(False, "历史无效实例不存在或不是可清理的空本体行")
return PluginPurgeResult(True, purged=("config",), instance_removed=True)
except PluginMutationRejectedError as error:
return PluginPurgeResult(False, str(error))
def reset(self, plugin_id: str) -> PluginConfigResult:
"""
通知插件补偿后停止实例、删除配置数据并重建运行态。
"""
try:
with self._mutation(f"重置插件 {plugin_id} 配置和数据"):
self._publish_reset(plugin_id)
self._stop(plugin_id)
self._delete_config(plugin_id, True)
self._delete_data(plugin_id, True)
self._reload_runtime(plugin_id)
self._refresh_registrations(plugin_id)
return PluginConfigResult(True)
except PluginMutationRejectedError as error:
return PluginConfigResult(False, str(error))