From 72ead2970cfbcd4b76556f271029853e91b06717 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 20 May 2026 15:53:56 +0800 Subject: [PATCH] fix: tolerate delayed OpenList metadata --- app/modules/filemanager/storages/alist.py | 13 +++++- app/modules/filemanager/transhandler.py | 39 ++++++++++++++---- tests/test_alist_storage.py | 23 +++++++++++ tests/test_transfer_job_manager.py | 49 +++++++++++++++++++++++ 4 files changed, 116 insertions(+), 8 deletions(-) diff --git a/app/modules/filemanager/storages/alist.py b/app/modules/filemanager/storages/alist.py index ee5f538d..afc058f6 100644 --- a/app/modules/filemanager/storages/alist.py +++ b/app/modules/filemanager/storages/alist.py @@ -331,7 +331,18 @@ class Alist(StorageBase, metaclass=WeakSingleton): ) return None - return self._delay_get_item(path, refresh=True) + return self._delay_get_item( + path, refresh=True + ) or self.__build_transfer_item( + schemas.FileItem( + storage=self.schema.value, + type="dir", + path=fileitem.path, + name=name, + basename=Path(name).stem, + ), + path, + ) def get_folder(self, path: Path) -> Optional[schemas.FileItem]: """ diff --git a/app/modules/filemanager/transhandler.py b/app/modules/filemanager/transhandler.py index fa84f6c1..5585690b 100644 --- a/app/modules/filemanager/transhandler.py +++ b/app/modules/filemanager/transhandler.py @@ -546,6 +546,7 @@ class TransHandler: result=result, ) if not new_item: + err_msg = err_msg or f"{fileitem.path} 整理后未获取到目标文件信息" logger.error(f"文件 {fileitem.path} 整理失败:{err_msg}") self.__update_result( result=result, @@ -608,6 +609,34 @@ class TransHandler: modify_time=_path.stat().st_mtime, ) + def __build_remote_targetitem(_source_item: FileItem, _path: Path) -> FileItem: + """ + 根据已确认的目标路径构造网盘文件信息,用于兼容元数据延迟可见的存储。 + """ + return FileItem( + storage=target_storage, + path=_path.as_posix(), + name=_path.name, + basename=_path.stem, + type=_source_item.type or "file", + size=_source_item.size, + extension=_path.suffix.lstrip("."), + modify_time=_source_item.modify_time, + thumbnail=_source_item.thumbnail, + ) + + def __get_remote_targetitem(_source_item: FileItem, _path: Path) -> FileItem: + """ + 获取网盘目标文件信息,目标存储索引未刷新时使用目标路径兜底。 + """ + target_item = target_oper.get_item(_path) + if target_item: + return target_item + logger.warn( + f"目标文件【{target_storage}】{_path} 元数据暂不可见,使用目标路径构造整理结果" + ) + return __build_remote_targetitem(_source_item, _path) + if ( fileitem.storage != target_storage and fileitem.storage != "local" @@ -719,9 +748,7 @@ class TransHandler: elif source_oper.copy( fileitem, Path(target_fileitem.path), target_file.name ): - new_item = target_oper.get_item(target_file) - if new_item: - return new_item, "" + return __get_remote_targetitem(fileitem, target_file), "" return None, f"【{target_storage}】{fileitem.path} 复制文件失败" else: return ( @@ -742,9 +769,7 @@ class TransHandler: elif source_oper.move( fileitem, Path(target_fileitem.path), target_file.name ): - new_item = target_oper.get_item(target_file) - if new_item: - return new_item, "" + return __get_remote_targetitem(fileitem, target_file), "" return None, f"【{target_storage}】{fileitem.path} 移动文件失败" else: return ( @@ -753,7 +778,7 @@ class TransHandler: ) elif transfer_type == "link": if source_oper.link(fileitem, target_file): - return target_oper.get_item(target_file), "" + return __get_remote_targetitem(fileitem, target_file), "" else: return None, f"【{target_storage}】{fileitem.path} 创建硬链接失败" else: diff --git a/tests/test_alist_storage.py b/tests/test_alist_storage.py index f1d6ff10..e0c67007 100644 --- a/tests/test_alist_storage.py +++ b/tests/test_alist_storage.py @@ -200,6 +200,29 @@ class AlistStorageTest(unittest.TestCase): self.assertEqual(50, len(items)) self.assertEqual(1, request_utils.post_res.call_count) + def test_create_folder_returns_target_when_openlist_metadata_is_delayed(self): + """ + OpenList 创建目录成功但元数据延迟可见时,应返回可用的目标目录项。 + """ + request_utils = MagicMock() + request_utils.post_res.return_value = _FakeResponse( + {"code": 200, "message": "success", "data": None} + ) + + with patch.object(Alist, "get_conf", return_value={"url": "http://openlist.test", "token": "token"}): + with patch.object(self.storage, "_Alist__get_header_with_token", return_value={}): + with patch.object(alist_module, "RequestUtils", return_value=request_utils): + with patch.object(self.storage, "_delay_get_item", return_value=None): + folder = self.storage.create_folder( + self._dir_item("/library/Test Show (2026)"), + "Season 1", + ) + + self.assertIsNotNone(folder) + self.assertEqual("/library/Test Show (2026)/Season 1", folder.path) + self.assertEqual("alist", folder.storage) + self.assertEqual("dir", folder.type) + def test_move_item_returns_target_when_openlist_metadata_is_delayed(self): """ OpenList 操作成功但目标元数据延迟可见时,应返回可用的目标文件项。 diff --git a/tests/test_transfer_job_manager.py b/tests/test_transfer_job_manager.py index e028b92f..7b828ade 100644 --- a/tests/test_transfer_job_manager.py +++ b/tests/test_transfer_job_manager.py @@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock from app.core.config import settings from app.chain.transfer import JobManager, TransferChain +from app.modules.filemanager.transhandler import TransHandler from app.schemas import EpisodeFormat, FileItem, TransferInfo, TransferTask from app.schemas.types import EventType, MediaType @@ -127,6 +128,54 @@ def migrate_to_media_job(jobview: JobManager, task: TransferTask): class TransferJobManagerTest(unittest.TestCase): + def test_same_storage_success_uses_target_path_when_metadata_is_delayed(self): + """ + 网盘操作已成功但目标元数据暂不可见时,整理结果应按成功路径落库。 + """ + source_item = FileItem( + storage="alist", + path="/downloads/Test.Show.S01E01.mkv", + type="file", + name="Test.Show.S01E01.mkv", + basename="Test.Show.S01E01", + extension="mkv", + size=1024, + modify_time=1715939275.0, + ) + target_path = Path( + "/library/Test Show (2026)/Season 1/Test.Show.S01E01.mkv" + ) + target_folder = FileItem( + storage="alist", + path=target_path.parent.as_posix(), + type="dir", + name=target_path.parent.name, + ) + source_oper = SimpleNamespace( + is_support_transtype=lambda transfer_type: True, + move=lambda fileitem, path, name: True, + ) + target_oper = SimpleNamespace( + get_folder=lambda path: target_folder, + get_item=lambda path: None, + ) + + new_item, errmsg = TransHandler._TransHandler__transfer_command( + fileitem=source_item, + target_storage="alist", + source_oper=source_oper, + target_oper=target_oper, + target_file=target_path, + transfer_type="move", + ) + + self.assertEqual("", errmsg) + self.assertIsNotNone(new_item) + self.assertEqual(target_path.as_posix(), new_item.path) + self.assertEqual("alist", new_item.storage) + self.assertEqual("file", new_item.type) + self.assertEqual(1024, new_item.size) + def test_manual_episode_offset_applies_once(self): chain = make_transfer_chain() source_fileitem = make_fileitem("/downloads/Test.Show.2026.S01E14.mkv")