mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 11:54:50 +08:00
fix(plugin): avoid refreshing all repositories on update
This commit is contained in:
@@ -94,7 +94,9 @@ class PluginInstallGateway:
|
||||
) -> PluginInstallResult:
|
||||
"""读取冻结库存并执行一次不能绕过来源身份的插件写入。"""
|
||||
try:
|
||||
inventory = await self.__inventory(force)
|
||||
# 安装 ``force`` 只控制载荷覆盖;远程市场索引由市场手动刷新和
|
||||
# 定时缓存任务维护,不能让单个插件更新放大全部仓库请求。
|
||||
inventory = await self.__inventory(False)
|
||||
async with plugin_lifecycle.hold(plugin_id, startup_token):
|
||||
identity = await self.__identity(plugin_id)
|
||||
admission = admit_plugin_install(
|
||||
|
||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from app.application.plugin.catalog import apply_declared_metadata_fallback
|
||||
from app.application.plugin.identity import PluginIdentity
|
||||
from app.application.plugin.identity import PluginIdentity, PluginPayloadSourceType
|
||||
from app.schemas.plugin import Plugin
|
||||
|
||||
MarketPluginLoader = Callable[[str, str | None, bool], Awaitable[list[Plugin] | None]]
|
||||
@@ -77,16 +77,26 @@ class PluginReleaseService:
|
||||
(plugin for plugin in self._local_repo_plugins() if plugin.id == plugin_id),
|
||||
None,
|
||||
)
|
||||
if local_plugin is not None:
|
||||
return _merge_market_metadata(installed_plugin, local_plugin)
|
||||
if identity is not None and identity.payload_source_type is PluginPayloadSourceType.LOCAL:
|
||||
return (
|
||||
_merge_market_metadata(installed_plugin, local_plugin)
|
||||
if local_plugin is not None
|
||||
else installed_plugin
|
||||
)
|
||||
|
||||
repo_url = _trusted_repo_url(identity)
|
||||
if repo_url is None:
|
||||
return installed_plugin
|
||||
market_plugin = await self._market_plugin(plugin_id, repo_url, force)
|
||||
if repo_url is not None:
|
||||
market_plugin = await self._market_plugin(plugin_id, repo_url, force)
|
||||
return (
|
||||
_merge_market_metadata(installed_plugin, market_plugin)
|
||||
if market_plugin is not None
|
||||
else installed_plugin
|
||||
)
|
||||
|
||||
# 尚未建立来源身份的本地插件继续使用本地仓库元数据,兼容首次迁移前的运行态。
|
||||
return (
|
||||
_merge_market_metadata(installed_plugin, market_plugin)
|
||||
if market_plugin is not None
|
||||
_merge_market_metadata(installed_plugin, local_plugin)
|
||||
if local_plugin is not None
|
||||
else installed_plugin
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager, nullcontext
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
@@ -396,6 +397,84 @@ def test_plugin_history_merges_remote_metadata():
|
||||
plugin_manager.async_get_plugins_from_market.assert_awaited_once_with(SOURCE_URL, settings.VERSION_FLAG, True)
|
||||
|
||||
|
||||
def test_plugin_history_uses_payload_source_when_local_candidate_also_exists():
|
||||
"""在线运行载荷不得被同 ID 的本地仓库历史覆盖。"""
|
||||
installed_plugin = schemas.Plugin(
|
||||
id="DemoPlugin",
|
||||
plugin_name="Demo Plugin",
|
||||
plugin_version="1.0.0",
|
||||
installed=True,
|
||||
history={},
|
||||
)
|
||||
local_plugin = schemas.Plugin(
|
||||
id="DemoPlugin",
|
||||
repo_url="local://DemoPlugin?path=/plugins&version=v3",
|
||||
history={"v0.9.0": "本地旧版说明"},
|
||||
)
|
||||
market_plugin = schemas.Plugin(
|
||||
id="DemoPlugin",
|
||||
repo_url=SOURCE_URL,
|
||||
history={"v1.1.0": "在线新版说明"},
|
||||
)
|
||||
plugin_manager = MagicMock()
|
||||
plugin_manager.get_installed_plugins.return_value = [installed_plugin]
|
||||
plugin_manager.get_local_repo_plugins.return_value = [local_plugin]
|
||||
plugin_manager.async_get_plugins_from_market = AsyncMock(return_value=[market_plugin])
|
||||
metadata = PluginDeclaredMetadata.from_package(
|
||||
{"name": "Demo Plugin", "v3": True},
|
||||
declaration_version="1.0.0",
|
||||
manifest_matches_payload=True,
|
||||
)
|
||||
identity = _plugin_identity(metadata=metadata)
|
||||
release_service = _release_service(plugin_manager, persistence=_persistence(identity))
|
||||
|
||||
result = asyncio.run(release_service.history("DemoPlugin", force=True))
|
||||
|
||||
assert result is not None
|
||||
assert result.repo_url == SOURCE_URL
|
||||
assert result.history == {"v1.1.0": "在线新版说明"}
|
||||
plugin_manager.async_get_plugins_from_market.assert_awaited_once_with(SOURCE_URL, settings.VERSION_FLAG, True)
|
||||
|
||||
|
||||
def test_plugin_history_uses_local_metadata_for_local_payload():
|
||||
"""本地运行载荷继续读取同一本地仓库的历史。"""
|
||||
installed_plugin = schemas.Plugin(
|
||||
id="DemoPlugin",
|
||||
plugin_name="Demo Plugin",
|
||||
plugin_version="1.0.0",
|
||||
installed=True,
|
||||
history={},
|
||||
)
|
||||
local_plugin = schemas.Plugin(
|
||||
id="DemoPlugin",
|
||||
repo_url="local://DemoPlugin?path=/plugins&version=v3",
|
||||
history={"v1.1.0": "本地新版说明"},
|
||||
)
|
||||
plugin_manager = MagicMock()
|
||||
plugin_manager.get_installed_plugins.return_value = [installed_plugin]
|
||||
plugin_manager.get_local_repo_plugins.return_value = [local_plugin]
|
||||
plugin_manager.async_get_plugins_from_market = AsyncMock()
|
||||
metadata = PluginDeclaredMetadata.from_package(
|
||||
{"name": "Demo Plugin", "v3": True},
|
||||
declaration_version="1.0.0",
|
||||
manifest_matches_payload=True,
|
||||
)
|
||||
identity = _plugin_identity(metadata=metadata)
|
||||
identity = replace(
|
||||
identity,
|
||||
payload_source_type=PluginPayloadSourceType.LOCAL,
|
||||
payload_source_key=None,
|
||||
)
|
||||
release_service = _release_service(plugin_manager, persistence=_persistence(identity))
|
||||
|
||||
result = asyncio.run(release_service.history("DemoPlugin", force=True))
|
||||
|
||||
assert result is not None
|
||||
assert result.repo_url == local_plugin.repo_url
|
||||
assert result.history == {"v1.1.0": "本地新版说明"}
|
||||
plugin_manager.async_get_plugins_from_market.assert_not_awaited()
|
||||
|
||||
|
||||
def test_plugin_history_falls_back_to_backward_compatible_package():
|
||||
"""已绑定来源的插件在当前索引缺失时仍读取向后兼容代际的历史。"""
|
||||
installed_plugin = schemas.Plugin(
|
||||
|
||||
@@ -76,6 +76,37 @@ async def test_gateway_freezes_admission_before_executing_transaction() -> None:
|
||||
assert admission.expected_revision is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_install_reuses_cached_candidate_inventory() -> None:
|
||||
"""强制覆盖载荷时不得连带强刷全部远程插件仓库。"""
|
||||
inventory = AsyncMock(return_value=_inventory())
|
||||
executor = AsyncMock()
|
||||
executor.execute.return_value = type(
|
||||
"Result",
|
||||
(),
|
||||
{"success": True, "message": ""},
|
||||
)()
|
||||
gateway = PluginInstallGateway(
|
||||
inventory=inventory,
|
||||
identity=AsyncMock(return_value=None),
|
||||
candidate_compatibility=lambda _candidate: (True, ""),
|
||||
executor=executor,
|
||||
clock=lambda: NOW,
|
||||
)
|
||||
|
||||
result = await gateway.install(
|
||||
plugin_id="DemoPlugin",
|
||||
repo_url=REPO_URL,
|
||||
package_version="v3",
|
||||
force=True,
|
||||
explicit_source=True,
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
inventory.assert_awaited_once_with(False)
|
||||
assert executor.execute.await_args.kwargs["force"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_only_requires_explicit_online_binding() -> None:
|
||||
"""本地专属身份即使发现唯一在线来源,也只能由管理员显式绑定。"""
|
||||
|
||||
Reference in New Issue
Block a user