mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 22:04:57 +08:00
fix(plugin): invalidate federation static resource cache (#6748)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import mimetypes
|
||||
from typing import Annotated, Any, Dict, List, Optional
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import aiofiles
|
||||
from anyio import Path as AsyncPath
|
||||
@@ -155,7 +156,7 @@ def _is_plugin_auth_remote_file(plugin_id: str, filepath: str) -> bool:
|
||||
remote = provider.get("remote") or {}
|
||||
if str(remote.get("id") or "").lower() != normalized_plugin_id:
|
||||
continue
|
||||
remote_path = str(remote.get("url") or "").lstrip("/")
|
||||
remote_path = urlsplit(str(remote.get("url") or "")).path.lstrip("/")
|
||||
remote_path_lower = remote_path.lower()
|
||||
expected_prefix = f"plugin/file/{normalized_plugin_id}/"
|
||||
if not remote_path_lower.startswith(expected_prefix):
|
||||
@@ -756,7 +757,10 @@ async def plugin_static_file(
|
||||
return StreamingResponse(
|
||||
file_generator(),
|
||||
media_type=response_type,
|
||||
headers={"Content-Disposition": f"inline; filename={plugin_file_path.name}"},
|
||||
headers={
|
||||
"Content-Disposition": f"inline; filename={plugin_file_path.name}",
|
||||
"Cache-Control": "no-cache, must-revalidate",
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import inspect
|
||||
from typing import Any, Callable, Dict, List, Mapping, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from app.runtime.extensions.plugin.contracts import (
|
||||
PluginDashboardError,
|
||||
@@ -177,13 +178,19 @@ class PluginProjection:
|
||||
"""构造联邦远程入口描述,分身额外带出其源插件 ID。
|
||||
|
||||
分身与本体共享同一份前端产物,只有源插件名下才有产物目录;前端联邦加载器
|
||||
拿不到源插件 ID 就只能按分身 ID 去取,必然落空。
|
||||
拿不到源插件 ID 就只能按分身 ID 去取,必然落空。入口 URL 同时带上插件版本,
|
||||
让插件更新后得到新的浏览器与 Service Worker 缓存键。
|
||||
"""
|
||||
if not self._remote_entry_factory:
|
||||
raise RuntimeError("插件联邦入口生成器尚未配置")
|
||||
remote_url = self._remote_entry_factory(plugin_id, dist_path)
|
||||
plugin_version = getattr(plugin, "plugin_version", None)
|
||||
if plugin_version:
|
||||
separator = "&" if "?" in remote_url else "?"
|
||||
remote_url = f"{remote_url}{separator}{urlencode({'v': str(plugin_version)})}"
|
||||
remote: Dict[str, Any] = {
|
||||
"id": plugin_id,
|
||||
"url": self._remote_entry_factory(plugin_id, dist_path),
|
||||
"url": remote_url,
|
||||
"name": plugin.plugin_name,
|
||||
}
|
||||
source_plugin_id = getattr(plugin, "plugin_source_id", None)
|
||||
|
||||
@@ -274,6 +274,34 @@ def test_plugin_static_file_requires_resource_token_by_default(monkeypatch):
|
||||
assert calls == ["resource-token"]
|
||||
|
||||
|
||||
def test_plugin_auth_remote_accepts_versioned_entry_url(monkeypatch):
|
||||
"""认证远程入口带缓存版本参数时仍允许匿名读取。"""
|
||||
calls = []
|
||||
|
||||
class FakePluginManager:
|
||||
"""返回带版本参数的认证远程入口桩。"""
|
||||
|
||||
def get_plugin_auth_providers(self):
|
||||
"""返回认证远程入口列表。"""
|
||||
return [{
|
||||
"remote": {
|
||||
"id": "AuthPlugin",
|
||||
"url": "/plugin/file/AuthPlugin/dist/remoteEntry.js?v=1.2.3",
|
||||
}
|
||||
}]
|
||||
|
||||
monkeypatch.setattr(plugin_endpoint, "get_plugin_manager", lambda: FakePluginManager())
|
||||
monkeypatch.setattr(plugin_endpoint, "verify_resource_token", lambda token: calls.append(token))
|
||||
|
||||
plugin_endpoint._verify_plugin_static_file_access(
|
||||
plugin_id="AuthPlugin",
|
||||
filepath="dist/remoteEntry.js",
|
||||
resource_token=None,
|
||||
)
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_plugin_auth_remote_files_allow_anonymous_bootstrap(monkeypatch):
|
||||
"""插件登录认证远程组件需要允许登录前匿名加载。"""
|
||||
calls = []
|
||||
|
||||
@@ -1229,6 +1229,7 @@ def test_virtual_instance_static_file_reads_from_source_directory(tmp_path, monk
|
||||
|
||||
assert asyncio.run(read_body()) == b"export default 'shared'"
|
||||
assert response.media_type == "application/javascript"
|
||||
assert response.headers["cache-control"] == "no-cache, must-revalidate"
|
||||
plugin_manager.get_plugin_source_id.assert_called_once_with("DemoPluginwork")
|
||||
|
||||
|
||||
|
||||
@@ -217,6 +217,20 @@ def test_projection_builds_federation_and_auth_provider_entries():
|
||||
}]
|
||||
|
||||
|
||||
def test_projection_versions_federation_remote_entry_after_plugin_update():
|
||||
"""插件版本变化时,联邦入口 URL 应生成新的缓存键。"""
|
||||
plugin = _Plugin(
|
||||
plugin_version="1.2.3+build 4",
|
||||
get_render_mode=lambda: ("vue", "dist/assets"),
|
||||
)
|
||||
projection = PluginProjection(
|
||||
{"Demo": plugin},
|
||||
remote_entry_factory=lambda plugin_id, path: f"/{plugin_id}/{path}",
|
||||
)
|
||||
|
||||
assert projection.remotes()[0]["url"] == "/Demo/dist/assets?v=1.2.3%2Bbuild+4"
|
||||
|
||||
|
||||
def test_projection_exposes_source_identity_for_virtual_frontend_instance():
|
||||
"""虚拟实例的联邦入口保留实例 URL,并补充共享源码身份。"""
|
||||
plugin = _Plugin(
|
||||
|
||||
Reference in New Issue
Block a user