From 1332576c3fb76211f8a979b28d91bed3188fc657 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 20 Jul 2026 13:42:27 +0800 Subject: [PATCH] fix(download): accept legacy remote subscription paths (#6147) --- app/helper/directory.py | 21 +++++-- tests/test_download_save_path_allowlist.py | 66 ++++++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/app/helper/directory.py b/app/helper/directory.py index d76b0f9f..ee95ad40 100644 --- a/app/helper/directory.py +++ b/app/helper/directory.py @@ -303,18 +303,21 @@ def validate_download_save_path(save_path: str) -> str: """ 校验用户传入的下载保存目录,/download/paths 暴露的下载目录配置是允许写入的公共合同。 - :param save_path: 下载保存目录,支持本地 /path 或远端 :/path + :param save_path: 下载保存目录,支持本地 /path、远端 :/path 和旧版订阅中的无前缀远程路径 :return: 可直接传给下载接口的规范化保存目录 """ value = str(save_path or "").strip() + has_storage_prefix = any(value.startswith(f"{item.value}:") for item in StorageSchema) storage, raw_path = _split_file_uri(value) target_style, target_path = _normalize_download_path(raw_path, storage) + download_roots = [] for dir_info in DirectoryHelper().get_download_dirs(): root = _normalize_download_root(dir_info) - if not root: - continue - root_storage, root_style, root_path = root + if root: + download_roots.append(root) + + for root_storage, root_style, root_path in download_roots: if storage != root_storage: continue if target_style != root_style: @@ -322,4 +325,14 @@ def validate_download_save_path(save_path: str) -> str: if target_path == root_path or target_path.is_relative_to(root_path): return _download_path_uri(storage, target_path) + # 旧版订阅界面只持久化 download_path,需要从已配置根目录恢复远程存储类型。 + if (not has_storage_prefix + and storage == StorageSchema.Local.value + and target_style == "posix"): + for root_storage, root_style, root_path in download_roots: + if root_storage == StorageSchema.Local.value or target_style != root_style: + continue + if target_path == root_path or target_path.is_relative_to(root_path): + return _download_path_uri(root_storage, target_path) + raise ValueError("保存路径不在允许的下载目录范围内") diff --git a/tests/test_download_save_path_allowlist.py b/tests/test_download_save_path_allowlist.py index 227be4c7..6335302c 100644 --- a/tests/test_download_save_path_allowlist.py +++ b/tests/test_download_save_path_allowlist.py @@ -135,6 +135,34 @@ def test_validate_download_save_path_accepts_configured_roots_and_children(save_ assert validate_download_save_path(save_path) == expected +def test_validate_download_save_path_accepts_legacy_remote_path_without_storage_prefix(): + """旧版订阅保存的远程原始路径应恢复为带存储前缀的 FileURI。""" + assert validate_download_save_path("/media/anime/sub") == "rclone:/media/anime/sub" + + +def test_validate_download_save_path_prefers_configured_local_root(monkeypatch): + """无前缀路径同时命中本地和远程根目录时应保持本地语义。""" + monkeypatch.setattr( + "app.helper.directory.DirectoryHelper.get_download_dirs", + lambda _self: [ + TransferDirectoryConf( + name="远程下载", + priority=1, + storage="rclone", + download_path="/shared", + ), + TransferDirectoryConf( + name="本地下载", + priority=2, + storage="local", + download_path="/shared", + ), + ], + ) + + assert validate_download_save_path("/shared/movie") == "/shared/movie" + + @pytest.mark.parametrize( ("save_path", "expected"), [ @@ -258,6 +286,23 @@ def test_resolve_media_download_dir_applies_remote_root_classification(monkeypat assert error_msg == "" +def test_resolve_media_download_dir_accepts_legacy_remote_root_without_storage_prefix(monkeypatch): + """订阅中的旧版远程根路径应按对应存储和分类配置解析。""" + monkeypatch.setattr( + "app.helper.directory.DirectoryHelper.get_download_dirs", + lambda _self: _classified_download_dirs(), + ) + + storage, target_dir, error_msg = DownloadChain._resolve_media_download_dir( + media_info=_build_tv_media(), + save_path="/media", + ) + + assert storage == "rclone" + assert target_dir == Path("/media/电视剧/动漫") + assert error_msg == "" + + def test_resolve_media_download_dir_uses_matching_media_specific_root(monkeypatch): monkeypatch.setattr( "app.helper.directory.DirectoryHelper.get_download_dirs", @@ -386,6 +431,27 @@ def test_download_single_applies_configured_root_classification(monkeypatch): assert chain.download.call_args.kwargs["download_dir"] == Path("/downloads/电视剧/动漫") +def test_download_single_accepts_legacy_remote_root_without_storage_prefix(monkeypatch): + """旧订阅的无前缀远程根应以正确 FileURI 提交给下载模块。""" + monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None) + monkeypatch.setattr( + "app.helper.directory.DirectoryHelper.get_download_dirs", + lambda _self: _classified_download_dirs(), + ) + chain = _build_download_chain() + chain.download.return_value = ("qb", None, "Original", "test stop") + context = _build_context() + context.media_info = _build_tv_media() + + chain.download_single( + context=context, + torrent_content=b"torrent-content", + save_path="/media", + ) + + assert chain.download.call_args.kwargs["download_dir"] == Path("rclone:/media/电视剧/动漫") + + @pytest.mark.parametrize("save_path", ["", " "]) def test_download_single_rejects_explicit_empty_save_path_before_default_fallback(monkeypatch, save_path): monkeypatch.setattr(download_module.eventmanager, "send_event", lambda *args, **kwargs: None)