fix(4.0): 更新规则时先在所有实例上匹配种子,任一实例不可用则不做任何修改

此前逐实例移动种子,后面的实例不可用时前面的实例已经移动,数据库仍记着旧目录。
This commit is contained in:
Estrella Pan
2026-10-08 16:20:06 +02:00
parent 813f04abff
commit fb77bd3e10
2 changed files with 65 additions and 7 deletions

View File

@@ -221,13 +221,33 @@ class TorrentManager:
# 没换:规则的种子可能经订阅进了别的实例,逐个实例按各自的
# 下载目录移动
if new_instance == old_instance:
for instance_id in await self._torrent_instances(old_data):
root = settings.downloader_instance(instance_id).path
moved_path = gen_save_path(data, root)
old_paths = _save_paths(old_data, root)
if old_paths == {normalize_save_path(moved_path)}:
continue
match_list = await self.__match_torrents_list(old_data, instance_id)
# 先在所有实例上匹配种子:任一实例不可用就什么都不改,
# 避免一部分实例已移动而规则仍记着旧目录
moves = []
try:
for instance_id in await self._torrent_instances(old_data):
root = settings.downloader_instance(instance_id).path
moved_path = gen_save_path(data, root)
old_paths = _save_paths(old_data, root)
if old_paths == {normalize_save_path(moved_path)}:
continue
match_list = await self.__match_torrents_list(
old_data, instance_id
)
moves.append((instance_id, moved_path, old_paths, match_list))
except ConnectionError as e:
logger.warning(
"[Manager] Can't update rule %s: %s", old_data.rule_name, e
)
return ResponseModel(
status_code=500,
status=False,
msg_en=f"Updating {old_data.official_title} failed: a "
"downloader is unreachable; nothing was changed, retry later.",
msg_zh=f"更新 {old_data.official_title} 失败:下载器不可用,"
"规则未修改,请稍后重试。",
)
for instance_id, moved_path, old_paths, match_list in moves:
async with DownloadClient(instance_id) as client:
# Move existing torrents to new location if path changed
if match_list:

View File

@@ -448,6 +448,44 @@ class TestRuleOnAnotherInstance:
moved = _mock("b")._torrents["hb"]["save_path"]
assert moved == "/b/Bangumi/Test Anime (2024)/Season 2"
async def test_update_rule_unavailable_instance_changes_nothing_for_retry(
self, two_instances
):
"""实例 b 不可用:a 上的种子不移动、规则不改,b 恢复后重试两边一起移动。"""
from module.manager import TorrentManager
from module.models import BangumiUpdate
bangumi_id = await self._rule_with_torrent_on_a()
hb = _completed("B", "hb", "/b/Bangumi/Test Anime (2024)/Season 1")
_mock("b")._torrents["hb"] = hb
_mock("b").auth = AsyncMock(return_value=False) # type: ignore[method-assign]
async with Database() as db:
await db.torrent.add(
Torrent(name="B", url="u-b", bangumi_id=1, downloader_id="b")
)
old = await db.bangumi.search_id(bangumi_id)
assert old is not None
update = BangumiUpdate(**old.model_dump(exclude={"id"}))
update.season = 2
resp = await TorrentManager(db).update_rule(bangumi_id, update)
assert resp.status is False and resp.status_code == 500
assert _mock("a")._torrents["old"]["save_path"] == old.save_path
kept = await db.bangumi.search_id(bangumi_id)
assert kept is not None and kept.save_path == old.save_path
# 登录失败会丢弃缓存的客户端,恢复后是新的客户端实例
_mock("b")._torrents["hb"] = hb
update = BangumiUpdate(**old.model_dump(exclude={"id"}))
update.season = 2
retry = await TorrentManager(db).update_rule(bangumi_id, update)
assert retry.status is True
new = await db.bangumi.search_id(bangumi_id)
season2 = "Bangumi/Test Anime (2024)/Season 2"
assert new is not None and new.save_path == f"/a/{season2}"
assert _mock("a")._torrents["old"]["save_path"] == f"/a/{season2}"
assert _mock("b")._torrents["hb"]["save_path"] == f"/b/{season2}"
async def test_delete_rule_unavailable_instance_keeps_rule_for_retry(
self, two_instances
):