mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-28 19:47:41 +08:00
feat(plugin): 建立可信来源准入与安装恢复 (#6462)
This commit is contained in:
@@ -153,7 +153,7 @@ class PluginCatalogFacade:
|
||||
plugin_info=info,
|
||||
market=self._system().local_repo_url(
|
||||
plugin_id,
|
||||
info.get("repo_path"),
|
||||
None,
|
||||
package_version,
|
||||
),
|
||||
installed_apps=installed,
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from app.runtime.extensions.plugin.system import PluginSystemServices
|
||||
@@ -22,8 +21,7 @@ class PluginSyncService:
|
||||
local_plugins: Callable[[], list[Any]],
|
||||
merge_plugins: Callable[[list[Any], list[Any], list[Any]], list[Any]],
|
||||
plugin_exists: Callable[[str, Optional[str]], bool],
|
||||
install: Callable[[str, Optional[str], bool], tuple[bool, str]],
|
||||
report: Callable[..., Any],
|
||||
install: Callable[[str, Optional[str], bool, object | None], tuple[bool, str]],
|
||||
log: Any,
|
||||
) -> None:
|
||||
"""保存目录读取、包安装和持久化报告端口。"""
|
||||
@@ -34,24 +32,38 @@ class PluginSyncService:
|
||||
self._merge_plugins = merge_plugins
|
||||
self._plugin_exists = plugin_exists
|
||||
self._install = install
|
||||
self._report = report
|
||||
self._logger = log
|
||||
|
||||
def sync(self) -> list[str]:
|
||||
"""并发安装本地缺失或需要更新的已安装插件。"""
|
||||
def sync(
|
||||
self,
|
||||
startup_token: object | None = None,
|
||||
*,
|
||||
online_restore_plugins: set[str] | None = None,
|
||||
) -> list[str]:
|
||||
"""并发安装本地缺失、需要更新或应恢复在线载荷的插件。"""
|
||||
if self._frozen():
|
||||
return []
|
||||
|
||||
installed = self._installed_plugins()
|
||||
online = self._online_plugins()
|
||||
local = self._local_plugins()
|
||||
local_plugin_ids = {plugin.id.lower() for plugin in local}
|
||||
restore_plugin_ids = {
|
||||
plugin_id.lower()
|
||||
for plugin_id in (online_restore_plugins or set())
|
||||
} - local_plugin_ids
|
||||
candidates = self._merge_plugins(online + local, [], []) if online or local else []
|
||||
targets = [
|
||||
plugin
|
||||
for plugin in candidates
|
||||
if plugin.id in installed
|
||||
and plugin.system_version_compatible is not False
|
||||
and not self._plugin_exists(plugin.id, plugin.plugin_version)
|
||||
and (
|
||||
plugin.id.lower() in restore_plugin_ids
|
||||
or (
|
||||
plugin.system_version_compatible is not False
|
||||
and not self._plugin_exists(plugin.id, plugin.plugin_version)
|
||||
)
|
||||
)
|
||||
]
|
||||
if not targets:
|
||||
return []
|
||||
@@ -63,10 +75,14 @@ class PluginSyncService:
|
||||
def install_one(plugin: Any) -> None:
|
||||
"""安装一个插件并记录结果。"""
|
||||
started = time.time()
|
||||
state, message = self._install(plugin.id, plugin.repo_url, False)
|
||||
state, message = self._install(
|
||||
plugin.id,
|
||||
None,
|
||||
False,
|
||||
startup_token,
|
||||
)
|
||||
elapsed = time.time() - started
|
||||
if state:
|
||||
self._report(plugin_id=plugin.id, repo_url=plugin.repo_url)
|
||||
self._logger.info(
|
||||
f"插件 {plugin.plugin_name} 安装成功,版本:{plugin.plugin_version},"
|
||||
f"耗时:{elapsed:.2f} 秒"
|
||||
@@ -128,12 +144,24 @@ class LocalPluginSyncService:
|
||||
f"{candidate.get('skip_reason')}"
|
||||
)
|
||||
return False
|
||||
source_dir = Path(candidate.get("path"))
|
||||
repo_url = candidate.get("repo_url")
|
||||
if not isinstance(repo_url, str) or not repo_url.startswith("local://"):
|
||||
self._logger.error(f"本地插件 {plugin_id} 缺少可验证的本地来源标识")
|
||||
return False
|
||||
try:
|
||||
if not self._system().package.sync_local(plugin_id, source_dir):
|
||||
state, message = self._system().install_plugin(
|
||||
plugin_id=plugin_id,
|
||||
repo_url=repo_url,
|
||||
package_version=candidate.get("package_version") or None,
|
||||
force=True,
|
||||
local_sync=True,
|
||||
explicit_source=True,
|
||||
)
|
||||
if not state:
|
||||
self._logger.error(f"同步本地插件 {plugin_id} 失败:{message}")
|
||||
return False
|
||||
self._recent_sync[plugin_id] = time.time()
|
||||
self._logger.info(f"已同步本地插件 {plugin_id}:{source_dir}")
|
||||
self._logger.info(f"已同步本地插件 {plugin_id}")
|
||||
return True
|
||||
except Exception as error:
|
||||
self._logger.error(f"同步本地插件 {plugin_id} 失败:{error}")
|
||||
|
||||
@@ -19,14 +19,16 @@ class PluginSystemServices:
|
||||
dependency_manifest_status: Callable[[Path], Optional[bool]],
|
||||
compatible_flags: Callable[[Optional[str]], list[str]],
|
||||
frozen: Callable[[], bool],
|
||||
install: Callable[..., tuple[bool, str]],
|
||||
) -> None:
|
||||
"""记录市场、包、依赖和代际兼容计算端口。"""
|
||||
"""记录市场、包、安装 Gateway、依赖和代际兼容计算端口。"""
|
||||
self.market = market
|
||||
self.package = package
|
||||
self.dependency = dependency
|
||||
self.dependency_manifest_status = dependency_manifest_status
|
||||
self.compatible_flags = compatible_flags
|
||||
self.frozen = frozen
|
||||
self.install = install
|
||||
|
||||
def local_repo_paths(self) -> list[Path]:
|
||||
"""返回可监测的本地插件仓库路径。"""
|
||||
@@ -65,6 +67,30 @@ class PluginSystemServices:
|
||||
"""判断当前宿主是否为不可写的冻结运行模式。"""
|
||||
return self.frozen()
|
||||
|
||||
def install_plugin(
|
||||
self,
|
||||
*,
|
||||
plugin_id: str,
|
||||
repo_url: str | None,
|
||||
package_version: str | None = None,
|
||||
release_version: str | None = None,
|
||||
force: bool = False,
|
||||
local_sync: bool = False,
|
||||
explicit_source: bool = False,
|
||||
startup_token: object | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""从同步运行时线程进入宿主唯一安装 Gateway。"""
|
||||
return self.install(
|
||||
plugin_id=plugin_id,
|
||||
repo_url=repo_url,
|
||||
package_version=package_version,
|
||||
release_version=release_version,
|
||||
force=force,
|
||||
local_sync=local_sync,
|
||||
explicit_source=explicit_source,
|
||||
startup_token=startup_token,
|
||||
)
|
||||
|
||||
|
||||
_services: Optional[PluginSystemServices] = None
|
||||
|
||||
|
||||
@@ -68,7 +68,6 @@ from app.schemas.types import EventType, SystemConfigKey
|
||||
LegacyDiagnosticsConfigurator = Callable[..., None]
|
||||
LegacyImportScanner = Callable[..., None]
|
||||
LegacyPluginImportPreparer = Callable[..., None]
|
||||
PluginInstallReporter = Callable[..., None]
|
||||
SiteAuthLevelProvider = Callable[[], int]
|
||||
PluginCatalogFactory = Callable[["PluginManager"], Any]
|
||||
PluginRouteRefresher = Callable[[str], None]
|
||||
@@ -122,7 +121,6 @@ _legacy_import_scanner: LegacyImportScanner = _ignore_legacy_diagnostics
|
||||
_legacy_plugin_import_preparer: LegacyPluginImportPreparer = (
|
||||
_ignore_plugin_resource_imports
|
||||
)
|
||||
_plugin_install_reporter: PluginInstallReporter = _ignore_legacy_diagnostics
|
||||
_site_auth_level_provider: SiteAuthLevelProvider = _unavailable_site_auth_level
|
||||
_plugin_catalog_factory: PluginCatalogFactory = _unavailable_plugin_catalog_factory
|
||||
_plugin_route_refresher: PluginRouteRefresher = _unavailable_plugin_route_refresher
|
||||
@@ -147,12 +145,6 @@ def configure_plugin_resource_import_preparer(
|
||||
_legacy_plugin_import_preparer = preparer
|
||||
|
||||
|
||||
def configure_plugin_install_reporter(reporter: PluginInstallReporter) -> None:
|
||||
"""由启动组合根注入插件安装上报器,避免扩展层依赖远程服务。"""
|
||||
global _plugin_install_reporter
|
||||
_plugin_install_reporter = reporter
|
||||
|
||||
|
||||
def configure_site_auth_level_provider(provider: SiteAuthLevelProvider) -> None:
|
||||
"""由启动组合根注入站点认证等级,避免扩展运行时依赖应用服务。"""
|
||||
global _site_auth_level_provider
|
||||
@@ -317,12 +309,12 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
||||
plugin_id,
|
||||
version,
|
||||
),
|
||||
install=lambda plugin_id, repo_url, force: get_plugin_system().package.install(
|
||||
install=lambda plugin_id, repo_url, force, startup_token: get_plugin_system().install_plugin(
|
||||
plugin_id=plugin_id,
|
||||
repo_url=repo_url,
|
||||
force_install=force,
|
||||
force=force,
|
||||
startup_token=startup_token,
|
||||
),
|
||||
report=lambda **kwargs: _plugin_install_reporter(**kwargs),
|
||||
log=logger,
|
||||
)
|
||||
self._plugin_clone = PluginCloneService(
|
||||
@@ -826,13 +818,21 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
||||
log=logger,
|
||||
).clear_modules(plugin_id)
|
||||
|
||||
def sync(self) -> List[str]:
|
||||
def sync(
|
||||
self,
|
||||
startup_token: object | None = None,
|
||||
*,
|
||||
online_restore_plugins: set[str] | None = None,
|
||||
) -> List[str]:
|
||||
"""
|
||||
安装本地不存在或需要更新的插件
|
||||
"""
|
||||
|
||||
with self.mutation("同步插件包"):
|
||||
return self._plugin_sync.sync()
|
||||
return self._plugin_sync.sync(
|
||||
startup_token,
|
||||
online_restore_plugins=online_restore_plugins,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def install_plugin_missing_dependencies() -> List[str]:
|
||||
|
||||
Reference in New Issue
Block a user