fix: tolerate delayed OpenList metadata

This commit is contained in:
jxxghp
2026-05-20 15:53:56 +08:00
parent 5fe5523d13
commit 72ead2970c
4 changed files with 116 additions and 8 deletions
+12 -1
View File
@@ -331,7 +331,18 @@ class Alist(StorageBase, metaclass=WeakSingleton):
) )
return None 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]: def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
""" """
+32 -7
View File
@@ -546,6 +546,7 @@ class TransHandler:
result=result, result=result,
) )
if not new_item: if not new_item:
err_msg = err_msg or f"{fileitem.path} 整理后未获取到目标文件信息"
logger.error(f"文件 {fileitem.path} 整理失败:{err_msg}") logger.error(f"文件 {fileitem.path} 整理失败:{err_msg}")
self.__update_result( self.__update_result(
result=result, result=result,
@@ -608,6 +609,34 @@ class TransHandler:
modify_time=_path.stat().st_mtime, 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 ( if (
fileitem.storage != target_storage fileitem.storage != target_storage
and fileitem.storage != "local" and fileitem.storage != "local"
@@ -719,9 +748,7 @@ class TransHandler:
elif source_oper.copy( elif source_oper.copy(
fileitem, Path(target_fileitem.path), target_file.name fileitem, Path(target_fileitem.path), target_file.name
): ):
new_item = target_oper.get_item(target_file) return __get_remote_targetitem(fileitem, target_file), ""
if new_item:
return new_item, ""
return None, f"{target_storage}{fileitem.path} 复制文件失败" return None, f"{target_storage}{fileitem.path} 复制文件失败"
else: else:
return ( return (
@@ -742,9 +769,7 @@ class TransHandler:
elif source_oper.move( elif source_oper.move(
fileitem, Path(target_fileitem.path), target_file.name fileitem, Path(target_fileitem.path), target_file.name
): ):
new_item = target_oper.get_item(target_file) return __get_remote_targetitem(fileitem, target_file), ""
if new_item:
return new_item, ""
return None, f"{target_storage}{fileitem.path} 移动文件失败" return None, f"{target_storage}{fileitem.path} 移动文件失败"
else: else:
return ( return (
@@ -753,7 +778,7 @@ class TransHandler:
) )
elif transfer_type == "link": elif transfer_type == "link":
if source_oper.link(fileitem, target_file): if source_oper.link(fileitem, target_file):
return target_oper.get_item(target_file), "" return __get_remote_targetitem(fileitem, target_file), ""
else: else:
return None, f"{target_storage}{fileitem.path} 创建硬链接失败" return None, f"{target_storage}{fileitem.path} 创建硬链接失败"
else: else:
+23
View File
@@ -200,6 +200,29 @@ class AlistStorageTest(unittest.TestCase):
self.assertEqual(50, len(items)) self.assertEqual(50, len(items))
self.assertEqual(1, request_utils.post_res.call_count) 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): def test_move_item_returns_target_when_openlist_metadata_is_delayed(self):
""" """
OpenList 操作成功但目标元数据延迟可见时,应返回可用的目标文件项。 OpenList 操作成功但目标元数据延迟可见时,应返回可用的目标文件项。
+49
View File
@@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock
from app.core.config import settings from app.core.config import settings
from app.chain.transfer import JobManager, TransferChain 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 import EpisodeFormat, FileItem, TransferInfo, TransferTask
from app.schemas.types import EventType, MediaType from app.schemas.types import EventType, MediaType
@@ -127,6 +128,54 @@ def migrate_to_media_job(jobview: JobManager, task: TransferTask):
class TransferJobManagerTest(unittest.TestCase): 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): def test_manual_episode_offset_applies_once(self):
chain = make_transfer_chain() chain = make_transfer_chain()
source_fileitem = make_fileitem("/downloads/Test.Show.2026.S01E14.mkv") source_fileitem = make_fileitem("/downloads/Test.Show.2026.S01E14.mkv")