refactor: add durable transfer planning checkpoints

This commit is contained in:
jxxghp
2026-08-27 13:16:33 +08:00
parent 49d30e5fdf
commit 23bff6b2bb
41 changed files with 7401 additions and 1098 deletions
+83 -6
View File
@@ -5,7 +5,7 @@ import traceback
from abc import ABCMeta
from collections.abc import Callable
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, cast
from app.application.chain.context import ChainRuntimeContext, get_chain_runtime_context
from app.application.chain.data import get_chain_data_ports
@@ -15,7 +15,7 @@ from app.application.configuration import (
)
from app.chain._messaging import MessageProcessingMixin, NotificationMixin
from app.chain._recognition import RecognitionMixin
from app.domain.context import Context, MediaInfo, SubtitleInfo, TorrentInfo
from app.domain.context import Context, MediaInfo, MusicInfo, SubtitleInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase
from app.runtime.log import logger
from app.schemas.category import CategoryConfig
@@ -35,6 +35,9 @@ from app.schemas.types import (
)
from app.schemas.workflow import FileItem
if TYPE_CHECKING:
from app.application.transfer import TransferPlanCheckpoint, TransferPlanningInput
class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
metaclass=ABCMeta):
@@ -65,6 +68,7 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
system_error_handler=self.__handle_system_error,
rate_limit_handler=self.__handle_rate_limit_error,
)
self._legacy_transfer_command = context.legacy_transfer_command
self.messagequeue = context.message_queue_factory(self.run_module)
@property
@@ -216,6 +220,15 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
"""
return self._module_dispatcher.dispatch(method, *args, **kwargs)
def run_module_strict(
self,
method: str,
*args: Any,
**kwargs: Any,
) -> Any:
"""运行模块并传播 provider 异常,供必须区分空结果与查询失败的能力使用。"""
return self._module_dispatcher.dispatch_strict(method, *args, **kwargs)
async def async_run_module(
self,
method: str,
@@ -854,7 +867,7 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
self,
fileitem: FileItem,
meta: MetaBase,
mediainfo: MediaInfo,
mediainfo: Union[MediaInfo, MusicInfo],
target_directory: TransferDirectoryConf = None,
target_storage: Optional[str] = None,
target_path: Path = None,
@@ -868,7 +881,7 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
preview: bool = False,
) -> Optional[TransferInfo]:
"""
文件转移
经启动组合根注入的 canonical durable command 执行旧整理 ABI。
:param fileitem: 文件信息
:param meta: 预识别的元数据
:param mediainfo: 识别的媒体信息
@@ -885,8 +898,9 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
:param preview: 是否仅预览,不执行实际转移
:return: {path, target_path, message}
"""
return self.run_module(
"transfer",
if self._legacy_transfer_command is None:
raise RuntimeError("旧整理兼容命令尚未由启动组合根配置")
return self._legacy_transfer_command(
fileitem=fileitem,
meta=meta,
mediainfo=mediainfo,
@@ -903,6 +917,69 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
preview=preview,
)
def plan_transfer(
self,
fileitem: FileItem,
meta: MetaBase,
mediainfo: Union[MediaInfo, MusicInfo],
target_directory: Optional[TransferDirectoryConf] = None,
target_storage: Optional[str] = None,
target_path: Optional[Path] = None,
transfer_type: Optional[str] = None,
scrape: Optional[bool] = None,
library_type_folder: Optional[bool] = None,
library_category_folder: Optional[bool] = None,
episodes_info: Optional[List[TmdbEpisode]] = None,
source_oper: Any = None,
preview: bool = False,
planning_input: Optional[TransferPlanningInput] = None,
) -> Optional[TransferPlanCheckpoint]:
"""调用文件管理模块生成无文件写入的冻结整理计划。"""
return cast(
"Optional[TransferPlanCheckpoint]",
self.run_module(
"plan_transfer",
fileitem=fileitem,
meta=meta,
mediainfo=mediainfo,
target_directory=target_directory,
target_path=target_path,
target_storage=target_storage,
transfer_type=transfer_type,
scrape=scrape,
library_type_folder=library_type_folder,
library_category_folder=library_category_folder,
episodes_info=episodes_info,
source_oper=source_oper,
preview=preview,
planning_input=planning_input,
),
)
def execute_transfer_plan(
self,
checkpoint: TransferPlanCheckpoint,
*,
meta: MetaBase,
mediainfo: Union[MediaInfo, MusicInfo],
source_oper: Any = None,
target_oper: Any = None,
cleanup_media_file: Optional[Callable[[FileItem], bool]] = None,
) -> Optional[TransferInfo]:
"""调用文件管理模块执行已冻结计划,并注入统一安全删除能力。"""
return cast(
Optional[TransferInfo],
self.run_module(
"execute_transfer_plan",
checkpoint=checkpoint,
meta=meta,
mediainfo=mediainfo,
source_oper=source_oper,
target_oper=target_oper,
cleanup_media_file=cleanup_media_file,
),
)
def transfer_completed(self, hashs: str, downloader: Optional[str] = None) -> None:
"""
下载器转移完成后的处理
+16 -1
View File
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, cast
from app.application.directory import DirectoryHelper
from app.chain import ChainBase
@@ -99,6 +99,21 @@ class StorageChain(ChainBase):
"""
return self.run_module("get_file_item", storage=storage, path=path)
def get_file_item_strict(
self,
storage: str,
path: Path,
) -> Optional[_SchemaFileItem]:
"""严格查询文件项:确认不存在返回空,provider 或 I/O 失败直接抛出。"""
return cast(
Optional[_SchemaFileItem],
self.run_module_strict(
"get_file_item",
storage=storage,
path=path,
),
)
def get_parent_item(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
"""
获取上级目录项
+876 -88
View File
File diff suppressed because it is too large Load Diff