diff --git a/app/chain/transfer/records.py b/app/chain/transfer/records.py index a196489b1..a3c1d6f72 100644 --- a/app/chain/transfer/records.py +++ b/app/chain/transfer/records.py @@ -583,14 +583,15 @@ class ManualHistoryMixin(_TransferOwnerBase): return False, "目标文件历史数据无效,请删除该整理记录后从文件管理重新整理" dest_fileitem = FileItem.model_validate(history.dest_fileitem) storage_chain = StorageChain() - if ( - storage_chain.exists(dest_fileitem) - and not storage_chain.delete_media_file(dest_fileitem) - ): - return False, ( - f"旧目标文件 {dest_fileitem.path} 删除失败,请检查媒体库权限," - "或手动删除该文件后重试" - ) + if storage_chain.exists(dest_fileitem): + deleted = storage_chain.delete_media_file(dest_fileitem) + # 远端存储可能在删除请求返回前由并发任务或生命周期策略移除文件。 + # 只要读回确认目标已不存在,就按幂等删除继续,不能留下永远无法重整的历史。 + if not deleted and storage_chain.exists(dest_fileitem): + return False, ( + f"旧目标文件 {dest_fileitem.path} 删除失败,请检查媒体库权限," + "或手动删除该文件后重试" + ) transfer_history_oper.delete(history.id) # 删除记录是用户显式要求重来,失败计数一并清零,否则重整仍会受上一轮次数限制 clear_transfer_failures(history.src, history.src_storage) diff --git a/tests/test_manual_transfer_history.py b/tests/test_manual_transfer_history.py index 5780a4b41..6ab7a6719 100644 --- a/tests/test_manual_transfer_history.py +++ b/tests/test_manual_transfer_history.py @@ -1011,6 +1011,30 @@ def test_manual_reorganize_removes_success_history_and_old_target(monkeypatch): assert planned == [fileitem.path] +def test_manual_reorganize_accepts_old_target_disappearing_during_delete(monkeypatch): + """远端删除返回失败但读回已不存在时,不应把重整永远卡在旧历史上。""" + chain = make_transfer_chain() + old_dest = make_fileitem("/library/Wrong Show/Wrong.Show.S02E01.mkv") + history = SimpleNamespace( + id=19, status=True, mode="copy", dest_fileitem=old_dest.model_dump(), + transfer_task_id=None, download_hash=None, downloader=None, src=None, src_storage=None, + ) + deleted: list[tuple[str, object]] = [] + states = iter((True, False)) + monkeypatch.setattr("app.chain.transfer.records.StorageChain", lambda: SimpleNamespace( + exists=lambda _item: next(states), + delete_media_file=lambda _item: False, + )) + state, message = chain._delete_manual_transfer_history( + history, + SimpleNamespace(delete=lambda history_id: deleted.append(("history", history_id))), + ) + + assert state is True + assert message == "" + assert deleted == [("history", 19)] + + def test_explicit_durable_reorganize_discards_old_task_and_replans(monkeypatch): """ 显式重新整理 durable 失败历史时不得登记原计划重试,应清理后重新规划。