fix: support site resources from legacy docker updater

This commit is contained in:
jxxghp
2026-08-14 20:26:51 +08:00
parent 369d7d6448
commit b3509c60bb
6 changed files with 89 additions and 0 deletions
+2
View File
@@ -54,6 +54,8 @@ For work that changes or reviews repository behavior, identify the domains actua
The historical `app/core`, `app/helper`, and `app/utils` directories are compatibility-only virtual import roots. Never add physical Python source there and never use those imports from host code. Choose an owner by responsibility, not by whether a function is "shared" or has historically been called a helper. The historical `app/core`, `app/helper`, and `app/utils` directories are compatibility-only virtual import roots. Never add physical Python source there and never use those imports from host code. Choose an owner by responsibility, not by whether a function is "shared" or has historically been called a helper.
`app/helper/` may contain only the non-Python `.resource-compat` marker so source archives retain a write target for old Docker images whose updater is fixed to that path. If such an updater writes the compiled site resources there, `app/application/site/__init__.py` exposes that directory only as a fallback when the canonical extension is absent. New images and current update flows must continue writing exclusively to `app/application/site/`.
| Package | Owns | Must Not Own | Representative Files | | Package | Owns | Must Not Own | Representative Files |
|---|---|---|---| |---|---|---|---|
| `app/foundation/` | 无状态、无配置和无 I/O 的底层机制:反射/动态导入、加密、DOM、身份、集合、单例、文本、URL 和版本比较 | `settings`、DB/SystemConfig、网络请求、运行日志、MoviePilot 业务规则、旧导入路径 | `reflection.py`, `crypto.py`, `collections.py`, `text.py`, `url.py` | | `app/foundation/` | 无状态、无配置和无 I/O 的底层机制:反射/动态导入、加密、DOM、身份、集合、单例、文本、URL 和版本比较 | `settings`、DB/SystemConfig、网络请求、运行日志、MoviePilot 业务规则、旧导入路径 | `reflection.py`, `crypto.py`, `collections.py`, `text.py`, `url.py` |
+24
View File
@@ -1 +1,25 @@
"""站点目录、认证与索引资源的应用能力包。""" """站点目录、认证与索引资源的应用能力包。"""
from importlib.machinery import EXTENSION_SUFFIXES
from pathlib import Path
def _include_legacy_resource_directory(
package_paths: list[str], package_dir: Path
) -> None:
"""canonical 扩展缺失时允许读取旧 Docker 更新器写入的资源目录。"""
extension_names = tuple(f"sites{suffix}" for suffix in EXTENSION_SUFFIXES)
if any((package_dir / name).is_file() for name in extension_names):
return
legacy_dir = package_dir.parent.parent / "helper"
if (
legacy_dir.is_dir()
and any((legacy_dir / name).is_file() for name in extension_names)
and str(legacy_dir) not in package_paths
):
# 旧镜像内固化的 mp_update.sh 无法随源码热更新,只在过渡场景扩展包搜索路径。
package_paths.append(str(legacy_dir))
_include_legacy_resource_directory(__path__, Path(__file__).resolve().parent)
+2
View File
@@ -0,0 +1,2 @@
该目录仅用于兼容旧 Docker 镜像中固化的资源更新脚本,不承载 Python 源码。
新更新器和新镜像仍只把站点资源安装到 app/application/site。
+6
View File
@@ -465,6 +465,12 @@ render_nginx_config
# 自动更新 # 自动更新
cd / cd /
if [ -f /app/docker/update.sh ] && ! cmp -s /app/docker/update.sh /usr/local/bin/mp_update.sh; then
# 后端源码可独立于镜像更新,启动前同步更新器,避免它长期保留过期目录约定。
cp -f /app/docker/update.sh /usr/local/bin/mp_update.sh
chmod +x /usr/local/bin/mp_update.sh
INFO "→ 已同步后端内置更新脚本"
fi
source /usr/local/bin/mp_update.sh source /usr/local/bin/mp_update.sh
if [ "${ONE_SHOT_UPDATE_APPLIED}" = "true" ]; then if [ "${ONE_SHOT_UPDATE_APPLIED}" = "true" ]; then
MOVIEPILOT_AUTO_UPDATE="${MOVIEPILOT_AUTO_UPDATE_ORIGINAL}" MOVIEPILOT_AUTO_UPDATE="${MOVIEPILOT_AUTO_UPDATE_ORIGINAL}"
+8
View File
@@ -8,6 +8,14 @@ MoviePilot keeps the established product packages such as `app/chain`,
`app/helper` and `app/utils` roots are virtual compatibility packages only; `app/helper` and `app/utils` roots are virtual compatibility packages only;
physical Python sources must not be recreated there. physical Python sources must not be recreated there.
The sole filesystem exception is the non-Python `app/helper/.resource-compat`
marker retained in source archives for old Docker images whose updater still
writes compiled site resources to `/app/app/helper`. When the canonical site
extension is absent, `app/application/site/__init__.py` may add that directory
as a package search fallback. Current images and update flows must still write
only to `app/application/site/`; no Python implementation may return to the
legacy root.
Capabilities migrated out of those legacy roots are organized by technical Capabilities migrated out of those legacy roots are organized by technical
responsibility: responsibility:
+47
View File
@@ -1,5 +1,7 @@
from importlib.machinery import EXTENSION_SUFFIXES, PathFinder
from pathlib import Path from pathlib import Path
from app.application.site import _include_legacy_resource_directory
from app.runtime.config import settings from app.runtime.config import settings
from app.adapters.system.resource import ( from app.adapters.system.resource import (
ResourceHelper, ResourceHelper,
@@ -11,6 +13,41 @@ from app.startup import modules_initializer
ROOT_DIR = Path(__file__).resolve().parents[1] ROOT_DIR = Path(__file__).resolve().parents[1]
def test_legacy_docker_updater_resource_directory_remains_importable(tmp_path):
"""旧镜像把资源写入 helper 时,canonical 站点包仍应找到对应扩展。"""
package_dir = tmp_path / "app" / "application" / "site"
legacy_dir = tmp_path / "app" / "helper"
package_dir.mkdir(parents=True)
legacy_dir.mkdir(parents=True)
extension_path = legacy_dir / f"sites{EXTENSION_SUFFIXES[0]}"
extension_path.touch()
package_paths = [str(package_dir)]
_include_legacy_resource_directory(package_paths, package_dir)
assert (ROOT_DIR / "app" / "helper" / ".resource-compat").is_file()
assert package_paths == [str(package_dir), str(legacy_dir)]
spec = PathFinder.find_spec("app.application.site.sites", package_paths)
assert spec is not None
assert spec.origin == str(extension_path)
def test_canonical_site_extension_takes_priority_over_legacy_directory(tmp_path):
"""canonical 扩展存在时不得把旧资源目录加入站点包搜索路径。"""
package_dir = tmp_path / "app" / "application" / "site"
legacy_dir = tmp_path / "app" / "helper"
package_dir.mkdir(parents=True)
legacy_dir.mkdir(parents=True)
extension_name = f"sites{EXTENSION_SUFFIXES[0]}"
(package_dir / extension_name).touch()
(legacy_dir / extension_name).touch()
package_paths = [str(package_dir)]
_include_legacy_resource_directory(package_paths, package_dir)
assert package_paths == [str(package_dir)]
def test_resource_helper_uses_v3_only(): def test_resource_helper_uses_v3_only():
"""在线资源更新器必须只请求 V3 清单、目录和站点索引文件。""" """在线资源更新器必须只请求 V3 清单、目录和站点索引文件。"""
assert settings.VERSION_FLAG == "v3" assert settings.VERSION_FLAG == "v3"
@@ -101,6 +138,16 @@ def test_install_and_docker_paths_do_not_reference_v2_resources():
assert "app/application/site" in content assert "app/application/site" in content
def test_docker_entrypoint_refreshes_stale_update_script_before_use():
"""新镜像应在自动更新前同步源码内置脚本,避免目录约定再次陈旧。"""
content = (ROOT_DIR / "docker" / "entrypoint.sh").read_text(encoding="utf-8")
refresh = "cp -f /app/docker/update.sh /usr/local/bin/mp_update.sh"
source = "source /usr/local/bin/mp_update.sh"
assert refresh in content
assert content.index(refresh) < content.index(source)
def test_v3_release_workflows_use_main_wiki_and_isolated_images(): def test_v3_release_workflows_use_main_wiki_and_isolated_images():
"""V3 正式版和 Beta 构建应读取主 Wiki 分支并保持镜像仓库隔离。""" """V3 正式版和 Beta 构建应读取主 Wiki 分支并保持镜像仓库隔离。"""
build_workflow = (ROOT_DIR / ".github" / "workflows" / "build-v3.yml").read_text(encoding="utf-8") build_workflow = (ROOT_DIR / ".github" / "workflows" / "build-v3.yml").read_text(encoding="utf-8")