mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
refactor(task): 重构定时任务与存储路由
- 移除各路由文件顶部冗余注释 - 将 JobRouter/NodeRouter 重命名为 CornJobRouter/CornJobNodeRouter - 新增存储浏览、节点、传输、工作流路由注册 - 调整路由导入来源与任务表名 - 优化 main.py 启动方式及环境配置加载
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.modules.task.storage.workflow.schema import WorkflowCreateSchema, WorkflowExecuteSchema, WorkflowOutSchema, WorkflowQueryParam, WorkflowUpdateSchema
|
||||
from app.modules.task.storage.workflow.service import WorkflowService
|
||||
|
||||
StorageWorkflowRouter = APIRouter(route_class=OperationLogRoute, prefix="/storage/workflow", tags=["传输流程"])
|
||||
|
||||
|
||||
@StorageWorkflowRouter.get("/page", summary="分页查询传输流程", response_model=ResponseSchema[PageResultSchema[WorkflowOutSchema]])
|
||||
async def get_flow_page_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[WorkflowQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result: PageResultSchema[WorkflowOutSchema] = await WorkflowService(auth, db).page(
|
||||
search=search,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
order_by=page.order_by,
|
||||
)
|
||||
return SuccessResponse(data=result, msg="查询传输流程分页成功")
|
||||
|
||||
|
||||
@StorageWorkflowRouter.get("/list", summary="查询传输流程列表", response_model=ResponseSchema[list[WorkflowOutSchema]])
|
||||
async def get_flow_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
search: Annotated[WorkflowQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
result: list[WorkflowOutSchema] = await WorkflowService(auth, db).get_list(search=search)
|
||||
return SuccessResponse(data=result, msg="查询传输流程列表成功")
|
||||
|
||||
|
||||
@StorageWorkflowRouter.get("/detail/{id}", summary="查询传输流程详情", response_model=ResponseSchema[WorkflowOutSchema])
|
||||
async def get_flow_detail_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
id: Annotated[int, Path(description="流程ID", ge=1)],
|
||||
) -> JSONResponse:
|
||||
result: WorkflowOutSchema = await WorkflowService(auth, db).detail(id=id)
|
||||
return SuccessResponse(data=result, msg="查询传输流程详情成功")
|
||||
|
||||
|
||||
@StorageWorkflowRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建传输流程", response_model=ResponseSchema[WorkflowOutSchema])
|
||||
async def create_flow_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:create"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
data: Annotated[WorkflowCreateSchema, Body(description="流程创建参数")],
|
||||
) -> JSONResponse:
|
||||
result: WorkflowOutSchema = await WorkflowService(auth, db).create(data=data)
|
||||
return SuccessResponse(data=result, msg="创建传输流程成功")
|
||||
|
||||
|
||||
@StorageWorkflowRouter.put("/update/{id}", summary="修改传输流程", response_model=ResponseSchema[WorkflowOutSchema])
|
||||
async def update_flow_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:update"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
id: Annotated[int, Path(description="流程ID", ge=1)],
|
||||
data: Annotated[WorkflowUpdateSchema, Body(description="流程修改参数")],
|
||||
) -> JSONResponse:
|
||||
result: WorkflowOutSchema = await WorkflowService(auth, db).update(id=id, data=data)
|
||||
return SuccessResponse(data=result, msg="修改传输流程成功")
|
||||
|
||||
|
||||
@StorageWorkflowRouter.delete("/delete", summary="删除传输流程", response_model=ResponseSchema[None])
|
||||
async def delete_flow_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:flow:delete"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
ids: Annotated[list[int], Body(description="流程ID列表")],
|
||||
) -> JSONResponse:
|
||||
await WorkflowService(auth, db).delete(ids=ids)
|
||||
return SuccessResponse(msg="删除传输流程成功")
|
||||
|
||||
|
||||
@StorageWorkflowRouter.post("/execute/{id}", summary="执行传输流程", response_model=ResponseSchema[list[int]])
|
||||
async def execute_flow_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_storage:workflow:transfer:create"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
id: Annotated[int, Path(description="流程ID", ge=1)],
|
||||
data: Annotated[WorkflowExecuteSchema | None, Body(description="执行参数(源文件/目录路径映射,可选)")] = None,
|
||||
) -> JSONResponse:
|
||||
task_ids: list[int] = await WorkflowService(auth, db).execute(
|
||||
id=id, source_paths=data.source_paths if data else None
|
||||
)
|
||||
return SuccessResponse(data=task_ids, msg=f"执行传输流程成功,已生成 {len(task_ids)} 个传输任务")
|
||||
@@ -0,0 +1,48 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.core.base_schema import AuthSchema
|
||||
|
||||
from .model import WorkflowEdgeModel, WorkflowModel, WorkflowNodeModel
|
||||
from .schema import WorkflowCreateSchema, WorkflowUpdateSchema
|
||||
|
||||
|
||||
class WorkflowCRUD(CRUDBase[WorkflowModel, WorkflowCreateSchema, WorkflowUpdateSchema]):
|
||||
"""传输流程数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
super().__init__(model=WorkflowModel, auth=auth, db=db)
|
||||
|
||||
|
||||
class WorkflowNodeCRUD(CRUDBase[WorkflowNodeModel, object, object]):
|
||||
"""流程节点明细数据层
|
||||
|
||||
明细是 flow 的派生从属数据:随父流程全量覆写/删除,无独立数据权限主体,
|
||||
也没有回收站/软删消费场景。因此不沿用基类软删 delete(否则每次保存画布都会
|
||||
残留一套 is_deleted=1 的旧明细,无限累积),删除统一走下方物理删除方法。
|
||||
"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
super().__init__(model=WorkflowNodeModel, auth=auth, db=db)
|
||||
|
||||
async def hard_delete_by_flow_ids(self, flow_ids: Sequence[int]) -> None:
|
||||
"""按 flow 物理删除明细(父流程行已过数据权限校验)。"""
|
||||
if not flow_ids:
|
||||
return
|
||||
_ = await self.db.execute(delete(WorkflowNodeModel).where(WorkflowNodeModel.flow_id.in_(flow_ids)))
|
||||
|
||||
|
||||
class WorkflowEdgeCRUD(CRUDBase[WorkflowEdgeModel, object, object]):
|
||||
"""流程连线明细数据层,删除语义同 WorkflowNodeCRUD"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
super().__init__(model=WorkflowEdgeModel, auth=auth, db=db)
|
||||
|
||||
async def hard_delete_by_flow_ids(self, flow_ids: Sequence[int]) -> None:
|
||||
"""按 flow 物理删除明细(父流程行已过数据权限校验)。"""
|
||||
if not flow_ids:
|
||||
return
|
||||
_ = await self.db.execute(delete(WorkflowEdgeModel).where(WorkflowEdgeModel.flow_id.in_(flow_ids)))
|
||||
@@ -0,0 +1,56 @@
|
||||
from sqlalchemy import JSON, Boolean, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class WorkflowModel(ModelMixin, UserMixin):
|
||||
"""传输流程:定义源节点 → 目标节点列表(parallel 多目标 / chain 链式)
|
||||
|
||||
graph 仅存画布布局与展示字段(节点位置、连线样式),业务配置落于
|
||||
flow_node / flow_edge 表,回显时由 service 组装,避免双份数据不一致。
|
||||
"""
|
||||
|
||||
__tablename__: str = "task_storage_workflow"
|
||||
__table_args__: dict[str, str] = {"comment": "传输流程定义表"}
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="流程名称")
|
||||
task_type: Mapped[str] = mapped_column(String(16), nullable=False, default="parallel", comment="类型(parallel:多目标 chain:链式)")
|
||||
graph: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="VueFlow画布布局数据 {nodes:[{id,type,position,label}],edges:[{id,source,target,type,animated,style,label}]}")
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启用 1:停用)")
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
||||
|
||||
|
||||
class WorkflowNodeModel(ModelMixin, UserMixin):
|
||||
"""流程画布节点(业务配置):节点关联的存储源与默认源目录。
|
||||
|
||||
节点在画布上的位置/名称等布局字段存 flow.graph 的 nodes 项(key=node_key)。
|
||||
"""
|
||||
|
||||
__tablename__: str = "task_storage_workflow_node"
|
||||
__table_args__: dict[str, str] = {"comment": "流程画布节点表"}
|
||||
|
||||
flow_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True, comment="流程ID")
|
||||
node_key: Mapped[str] = mapped_column(String(64), nullable=False, comment="画布节点ID")
|
||||
source_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True, comment="存储源ID")
|
||||
source_path: Mapped[str | None] = mapped_column(String(1024), default=None, nullable=True, comment="默认源目录")
|
||||
|
||||
|
||||
class WorkflowEdgeModel(ModelMixin, UserMixin):
|
||||
"""流程画布连线(业务配置):传输方式与分片参数。
|
||||
|
||||
连线的目标目录由目标节点的默认源目录决定(节点 source_path),连线不再配置路径。
|
||||
连线的样式/动画等展示字段存 flow.graph 的 edges 项(key=edge_key)。
|
||||
"""
|
||||
|
||||
__tablename__: str = "task_storage_workflow_edge"
|
||||
__table_args__: dict[str, str] = {"comment": "流程画布连线表"}
|
||||
|
||||
flow_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True, comment="流程ID")
|
||||
edge_key: Mapped[str] = mapped_column(String(64), nullable=False, comment="画布连线ID")
|
||||
source_node_key: Mapped[str] = mapped_column(String(64), nullable=False, comment="源画布节点ID")
|
||||
target_node_key: Mapped[str] = mapped_column(String(64), nullable=False, comment="目标画布节点ID")
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="是否启用(禁用则不执行)")
|
||||
transfer_mode: Mapped[str | None] = mapped_column(String(16), default=None, nullable=True, comment="传输方式(stream/multipart,空用存储源默认)")
|
||||
multipart_part_size: Mapped[int | None] = mapped_column(Integer, default=None, nullable=True, comment="分片大小(MB)")
|
||||
multipart_concurrency: Mapped[int | None] = mapped_column(Integer, default=None, nullable=True, comment="分片并发数")
|
||||
@@ -0,0 +1,178 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
|
||||
|
||||
WorkflowTaskType = Literal["parallel", "chain"]
|
||||
|
||||
|
||||
class WorkflowTargetSchema(BaseModel):
|
||||
"""流程目标节点配置(目标目录由目标节点默认源目录决定)"""
|
||||
|
||||
target_id: int = Field(..., ge=1, description="目标节点ID(存储源)")
|
||||
target_path: str = Field(..., max_length=1024, description="目标路径")
|
||||
|
||||
|
||||
class WorkflowSourceSchema(BaseModel):
|
||||
"""流程源节点概览(由画布连线实时派生)"""
|
||||
|
||||
source_id: int = Field(..., ge=1, description="源节点ID(存储源)")
|
||||
source_name: str | None = Field(default=None, description="源存储源名称")
|
||||
|
||||
|
||||
class WorkflowNodeSchema(BaseModel):
|
||||
"""画布节点业务配置(由画布拆分,落 flow_node 表)"""
|
||||
|
||||
node_key: str = Field(..., description="画布节点ID")
|
||||
source_id: int = Field(..., ge=1, description="存储源ID")
|
||||
source_path: str | None = Field(default=None, max_length=1024, description="默认源目录")
|
||||
|
||||
|
||||
class WorkflowEdgeSchema(BaseModel):
|
||||
"""画布连线业务配置(由画布拆分,落 flow_edge 表)
|
||||
|
||||
连线只定义传输方式;目标目录由目标节点的默认源目录(source_path)决定。
|
||||
"""
|
||||
|
||||
edge_key: str = Field(..., description="画布连线ID")
|
||||
source_node_key: str = Field(..., description="源画布节点ID")
|
||||
target_node_key: str = Field(..., description="目标画布节点ID")
|
||||
enabled: bool = Field(default=True, description="是否启用(禁用则不执行)")
|
||||
transfer_mode: str | None = Field(default=None, max_length=16, description="传输方式(stream/multipart,空用存储源默认)")
|
||||
multipart_part_size: int | None = Field(default=None, ge=1, description="分片大小(MB)")
|
||||
multipart_concurrency: int | None = Field(default=None, ge=1, description="分片并发数")
|
||||
|
||||
|
||||
class WorkflowLayoutNodeSchema(BaseModel):
|
||||
"""画布节点布局展示字段(存 flow.graph,业务配置在 flow_node 表)"""
|
||||
|
||||
id: str = Field(..., description="画布节点ID")
|
||||
type: str = Field("storage", description="节点类型")
|
||||
position: dict = Field(default_factory=lambda: {"x": 0, "y": 0}, description="节点位置")
|
||||
label: str | None = Field(default=None, description="节点显示名")
|
||||
style: dict | None = Field(default=None, description="节点样式")
|
||||
|
||||
|
||||
class WorkflowLayoutEdgeSchema(BaseModel):
|
||||
"""画布连线布局展示字段(存 flow.graph,业务配置在 flow_edge 表)"""
|
||||
|
||||
id: str = Field(..., description="画布连线ID")
|
||||
source: str = Field(..., description="源画布节点ID")
|
||||
target: str = Field(..., description="目标画布节点ID")
|
||||
type: str = Field("smoothstep", description="连线类型")
|
||||
animated: bool | None = Field(default=None, description="连线动画")
|
||||
style: dict | None = Field(default=None, description="连线样式")
|
||||
label: str | None = Field(default=None, description="连线显示名")
|
||||
|
||||
|
||||
class WorkflowGraphNodeDataSchema(BaseModel):
|
||||
"""画布节点回显数据(组装完整画布时写入 node.data)"""
|
||||
|
||||
source_id: int = Field(..., ge=1, description="存储源ID")
|
||||
source_path: str | None = Field(default=None, description="默认源目录")
|
||||
label: str | None = Field(default=None, description="节点显示名")
|
||||
protocol: str | None = Field(default=None, description="存储源协议")
|
||||
host: str | None = Field(default=None, description="主机地址")
|
||||
bucket: str | None = Field(default=None, description="桶名(对象存储)")
|
||||
endpoint: str | None = Field(default=None, description="对象存储地址")
|
||||
region: str | None = Field(default=None, description="区域")
|
||||
path_prefix: str | None = Field(default=None, description="路径前缀")
|
||||
|
||||
|
||||
class WorkflowGraphEdgeDataSchema(BaseModel):
|
||||
"""画布连线回显数据(组装完整画布时写入 edge.data)"""
|
||||
|
||||
enabled: bool = Field(default=True, description="是否启用(禁用则不执行)")
|
||||
transfer_mode: str | None = Field(default=None, description="传输方式")
|
||||
multipart_part_size: int | None = Field(default=None, description="分片大小(MB)")
|
||||
multipart_concurrency: int | None = Field(default=None, description="分片并发数")
|
||||
source_label: str | None = Field(default=None, description="源存储源名称")
|
||||
target_label: str | None = Field(default=None, description="目标存储源名称")
|
||||
source_protocol: str | None = Field(default=None, description="源存储源协议")
|
||||
target_protocol: str | None = Field(default=None, description="目标存储源协议")
|
||||
source_storage_id: int | None = Field(default=None, description="源存储源ID")
|
||||
target_storage_id: int | None = Field(default=None, description="目标存储源ID")
|
||||
|
||||
|
||||
class WorkflowSplitResultSchema(BaseModel):
|
||||
"""画布拆分结果:业务明细(node/edge)+ 精简布局 + 派生概览"""
|
||||
|
||||
layout: dict = Field(..., description="精简后的画布布局 {nodes:[{id,type,position,label}],edges:[{id,source,target,type,animated,style,label}]}")
|
||||
nodes: list[WorkflowNodeSchema] = Field(default_factory=list, description="节点业务明细")
|
||||
edges: list[WorkflowEdgeSchema] = Field(default_factory=list, description="连线业务明细")
|
||||
sources: list[WorkflowSourceSchema] = Field(default_factory=list, description="源节点列表(去重)")
|
||||
targets: list[WorkflowTargetSchema] = Field(default_factory=list, description="目标列表(去重)")
|
||||
|
||||
|
||||
class WorkflowTransferPlanSchema(BaseModel):
|
||||
"""执行计划:单条连线生成的传输任务参数"""
|
||||
|
||||
src_id: int = Field(..., ge=1, description="源存储源ID")
|
||||
tgt_id: int = Field(..., ge=1, description="目标存储源ID")
|
||||
src_path: str = Field(..., description="源文件/目录路径")
|
||||
tgt_path: str = Field(..., description="目标路径")
|
||||
transfer_mode: str = Field("stream", description="传输方式(stream/multipart)")
|
||||
multipart_part_size: int | None = Field(default=None, description="分片大小(MB)")
|
||||
multipart_concurrency: int | None = Field(default=None, description="分片并发数")
|
||||
|
||||
|
||||
class WorkflowCreateSchema(BaseModel):
|
||||
"""创建传输流程(画布驱动:业务配置解析后落 flow_node/flow_edge 表)"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=64, description="流程名称")
|
||||
task_type: WorkflowTaskType = Field("parallel", description="类型(parallel:多目标 chain:链式)")
|
||||
graph: dict = Field(..., description="VueFlow画布数据 {nodes:[{id,type,position,data}],edges:[{id,source,target,data}]}")
|
||||
status: int = Field(default=0, ge=0, le=1, description="状态(0:启用 1:停用)")
|
||||
description: str | None = Field(default=None, max_length=255, description="备注")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise ValueError("流程名称不能为空")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_graph(self):
|
||||
"""画布必须包含传输连线(源节点 → 目标节点)。"""
|
||||
if not self.graph or not self.graph.get("edges"):
|
||||
raise ValueError("请至少添加一条传输连线(源节点 → 目标节点)")
|
||||
return self
|
||||
|
||||
|
||||
class WorkflowUpdateSchema(WorkflowCreateSchema):
|
||||
"""更新传输流程"""
|
||||
|
||||
|
||||
class WorkflowExecuteSchema(BaseModel):
|
||||
"""执行传输流程参数"""
|
||||
|
||||
source_paths: dict[str, str] | None = Field(
|
||||
default=None,
|
||||
description="源文件/目录路径映射 {源存储源ID: 路径},执行时必须为每条连线的源存储源指定",
|
||||
)
|
||||
|
||||
|
||||
class WorkflowOutSchema(BaseSchema, UserBySchema):
|
||||
"""传输流程详情响应模型(sources/targets 由 service 从明细表派生)"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
name: str | None = None
|
||||
task_type: WorkflowTaskType = "parallel"
|
||||
sources: list[WorkflowSourceSchema] = Field(default_factory=list, description="源节点列表(由画布派生)")
|
||||
targets: list[WorkflowTargetSchema] = Field(default_factory=list)
|
||||
graph: dict | None = None
|
||||
graph_stats: dict | None = None
|
||||
status: int = 0
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class WorkflowQueryParam(BaseQueryParam, UserByQueryParam):
|
||||
"""传输流程查询参数"""
|
||||
|
||||
name: str | None = Field(None, description="流程名称", json_schema_extra={"q": "like"})
|
||||
task_type: WorkflowTaskType | None = Field(None, description="类型(parallel/chain)", json_schema_extra={"q": "eq"})
|
||||
status: int | None = Field(None, ge=0, le=1, description="状态(0:启用 1:停用)", json_schema_extra={"q": "eq"})
|
||||
@@ -0,0 +1,489 @@
|
||||
from collections import defaultdict
|
||||
from collections.abc import Sequence
|
||||
from typing import cast
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_schema import AuthSchema, PageResultSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.modules.task.storage.node.crud import StorageNodeCRUD
|
||||
from app.modules.task.storage.node.model import StorageNodeModel
|
||||
from app.modules.task.storage.node.service import StorageNodeService
|
||||
from app.modules.task.storage.transfer.schema import TransferMode, TransferTargetSchema, TransferTaskCreateSchema
|
||||
from app.modules.task.storage.transfer.service import StorageTransferService
|
||||
from app.utils.common_util import search_to_dict
|
||||
|
||||
from .crud import WorkflowCRUD, WorkflowEdgeCRUD, WorkflowNodeCRUD
|
||||
from .model import WorkflowEdgeModel, WorkflowModel, WorkflowNodeModel
|
||||
from .schema import (
|
||||
WorkflowCreateSchema,
|
||||
WorkflowEdgeSchema,
|
||||
WorkflowGraphEdgeDataSchema,
|
||||
WorkflowGraphNodeDataSchema,
|
||||
WorkflowLayoutEdgeSchema,
|
||||
WorkflowLayoutNodeSchema,
|
||||
WorkflowNodeSchema,
|
||||
WorkflowOutSchema,
|
||||
WorkflowQueryParam,
|
||||
WorkflowSourceSchema,
|
||||
WorkflowSplitResultSchema,
|
||||
WorkflowTargetSchema,
|
||||
WorkflowTransferPlanSchema,
|
||||
WorkflowUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class WorkflowService:
|
||||
"""传输流程服务(源节点 → 目标节点,支持 1对多 / 多对1)
|
||||
|
||||
数据职责划分:
|
||||
- flow.graph:仅存画布布局与展示字段(节点位置、连线样式/动画),不做业务解析
|
||||
- flow_node / flow_edge 表:节点存储源与默认源目录、连线启用与传输方式(业务配置)
|
||||
- sources / targets:由连线明细实时派生,仅用于列表展示/校验
|
||||
|
||||
保存时拆分画布、回显时组装画布、执行直接读明细表,避免对 graph 的重复解析。
|
||||
"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
self.auth = auth
|
||||
self.db = db
|
||||
|
||||
# ── 内部工具 ────────────────────────────────────────────────────
|
||||
|
||||
def _crud(self) -> WorkflowCRUD:
|
||||
return WorkflowCRUD(self.auth, self.db)
|
||||
|
||||
async def _validate_nodes(
|
||||
self, sources: list[WorkflowSourceSchema], targets: list[WorkflowTargetSchema]
|
||||
) -> None:
|
||||
"""校验源节点与目标节点均存在且启用。"""
|
||||
node_service = StorageNodeService(self.auth, self.db)
|
||||
ids = [s.source_id for s in sources] + [t.target_id for t in targets]
|
||||
if ids:
|
||||
await node_service.get_active_sources(ids)
|
||||
|
||||
@staticmethod
|
||||
def _sources_from(
|
||||
edge_rows: Sequence[WorkflowEdgeModel], node_map: dict[str, WorkflowNodeModel]
|
||||
) -> list[WorkflowSourceSchema]:
|
||||
"""由连线明细派生源节点列表(去重)。"""
|
||||
out: list[WorkflowSourceSchema] = []
|
||||
seen: set[int] = set()
|
||||
for e in edge_rows:
|
||||
src_node = node_map.get(e.source_node_key)
|
||||
if not src_node:
|
||||
continue
|
||||
if src_node.source_id in seen:
|
||||
continue
|
||||
seen.add(src_node.source_id)
|
||||
out.append(WorkflowSourceSchema(source_id=src_node.source_id))
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _targets_from(
|
||||
edge_rows: Sequence[WorkflowEdgeModel], node_map: dict[str, WorkflowNodeModel]
|
||||
) -> list[WorkflowTargetSchema]:
|
||||
"""由连线明细派生目标列表(去重),目标目录取目标节点的默认源目录。"""
|
||||
out: list[WorkflowTargetSchema] = []
|
||||
seen: set[int] = set()
|
||||
for e in edge_rows:
|
||||
tgt_node = node_map.get(e.target_node_key)
|
||||
if not tgt_node:
|
||||
continue
|
||||
if tgt_node.source_id in seen:
|
||||
continue
|
||||
seen.add(tgt_node.source_id)
|
||||
out.append(
|
||||
WorkflowTargetSchema(target_id=tgt_node.source_id, target_path=tgt_node.source_path or "")
|
||||
)
|
||||
return out
|
||||
|
||||
# ── 画布拆分/组装 ───────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _split_graph(graph: dict) -> WorkflowSplitResultSchema:
|
||||
"""校验并拆分提交的画布。
|
||||
|
||||
- 业务配置(节点 source_id/source_path、连线启用与传输方式)→ FlowNode/FlowEdge 明细
|
||||
- 布局展示(位置、连线样式/动画)→ 精简后的 layout
|
||||
同时完成画布完整性校验(存在连线、源≠目标),支持 1对多 / 多对1 拓扑。
|
||||
"""
|
||||
nodes = {n["id"]: n for n in (graph.get("nodes") or [])}
|
||||
edges = graph.get("edges") or []
|
||||
if not edges:
|
||||
raise CustomException(msg="画布中未找到有效的传输连线(源节点 → 目标节点)")
|
||||
|
||||
layout_nodes: list[dict] = []
|
||||
node_items: list[WorkflowNodeSchema] = []
|
||||
for nid, n in nodes.items():
|
||||
d = n.get("data") or {}
|
||||
layout_nodes.append(
|
||||
WorkflowLayoutNodeSchema(
|
||||
id=nid,
|
||||
type=n.get("type") or "storage",
|
||||
position=n.get("position") or {"x": 0, "y": 0},
|
||||
label=n.get("label") or d.get("label"),
|
||||
style=n.get("style"),
|
||||
).model_dump(exclude_none=True)
|
||||
)
|
||||
if d.get("source_id") is not None:
|
||||
node_items.append(
|
||||
WorkflowNodeSchema(node_key=nid, source_id=d["source_id"], source_path=d.get("source_path"))
|
||||
)
|
||||
node_map = {it.node_key: it for it in node_items}
|
||||
|
||||
sources: list[WorkflowSourceSchema] = []
|
||||
targets: list[WorkflowTargetSchema] = []
|
||||
edge_items: list[WorkflowEdgeSchema] = []
|
||||
layout_edges: list[dict] = []
|
||||
seen_edges: set[tuple[int, int]] = set()
|
||||
seen_sources: set[int] = set()
|
||||
seen_targets: set[int] = set()
|
||||
for e in edges:
|
||||
src_n = node_map.get(e.get("source"))
|
||||
tgt_n = node_map.get(e.get("target"))
|
||||
if not src_n or not tgt_n:
|
||||
continue
|
||||
sid, tid = src_n.source_id, tgt_n.source_id
|
||||
src_label = (nodes.get(e.get("source")) or {}).get("data", {}).get("label") or sid
|
||||
tgt_label = (nodes.get(e.get("target")) or {}).get("data", {}).get("label") or tid
|
||||
if sid == tid:
|
||||
raise CustomException(msg=f"流程保存失败,连线「{src_label} → {tgt_label}」源与目标不能相同")
|
||||
pair = (sid, tid)
|
||||
if pair in seen_edges:
|
||||
continue
|
||||
seen_edges.add(pair)
|
||||
if sid not in seen_sources:
|
||||
seen_sources.add(sid)
|
||||
sources.append(WorkflowSourceSchema(source_id=sid))
|
||||
if tid not in seen_targets:
|
||||
seen_targets.add(tid)
|
||||
# 目标目录由目标节点默认源目录决定
|
||||
targets.append(WorkflowTargetSchema(target_id=tid, target_path=tgt_n.source_path or ""))
|
||||
ed = e.get("data") or {}
|
||||
edge_items.append(
|
||||
WorkflowEdgeSchema(
|
||||
edge_key=e["id"],
|
||||
source_node_key=e.get("source"),
|
||||
target_node_key=e.get("target"),
|
||||
enabled=ed.get("enabled", True),
|
||||
transfer_mode=ed.get("transfer_mode"),
|
||||
multipart_part_size=ed.get("multipart_part_size"),
|
||||
multipart_concurrency=ed.get("multipart_concurrency"),
|
||||
)
|
||||
)
|
||||
layout_edges.append(
|
||||
WorkflowLayoutEdgeSchema(
|
||||
id=e["id"],
|
||||
source=e.get("source"),
|
||||
target=e.get("target"),
|
||||
type=e.get("type") or "smoothstep",
|
||||
animated=e.get("animated"),
|
||||
style=e.get("style"),
|
||||
label=e.get("label"),
|
||||
).model_dump(exclude_none=True)
|
||||
)
|
||||
|
||||
if not edge_items:
|
||||
raise CustomException(msg="画布中未找到有效的传输连线(源节点 → 目标节点)")
|
||||
return WorkflowSplitResultSchema(
|
||||
layout={"nodes": layout_nodes, "edges": layout_edges},
|
||||
nodes=node_items,
|
||||
edges=edge_items,
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
)
|
||||
|
||||
async def _save_graph(self, flow_id: int, nodes: list[WorkflowNodeSchema], edges: list[WorkflowEdgeSchema]) -> None:
|
||||
"""覆写流程的业务明细(附属表物理删除后重建,不做逻辑删除)。
|
||||
|
||||
父流程行已在调用方经 WorkflowCRUD 做过数据权限校验,明细随父全量覆写,
|
||||
删除统一走子表 CRUD 的物理清理方法(基类软删 delete 不适用于逐次覆写场景)。
|
||||
"""
|
||||
await WorkflowNodeCRUD(self.auth, self.db).hard_delete_by_flow_ids([flow_id])
|
||||
await WorkflowEdgeCRUD(self.auth, self.db).hard_delete_by_flow_ids([flow_id])
|
||||
user_id = self.auth.user.id
|
||||
for it in nodes:
|
||||
self.db.add(WorkflowNodeModel(flow_id=flow_id, created_id=user_id, updated_id=user_id, **it.model_dump()))
|
||||
for it in edges:
|
||||
self.db.add(WorkflowEdgeModel(flow_id=flow_id, created_id=user_id, updated_id=user_id, **it.model_dump()))
|
||||
await self.db.flush()
|
||||
|
||||
async def _load_flow_graph(self, flow_id: int) -> tuple[list[WorkflowNodeModel], list[WorkflowEdgeModel]]:
|
||||
result = await self.db.execute(
|
||||
select(WorkflowNodeModel)
|
||||
.where(WorkflowNodeModel.flow_id == flow_id)
|
||||
.order_by(WorkflowNodeModel.id)
|
||||
)
|
||||
node_rows = list(result.scalars().all())
|
||||
result = await self.db.execute(
|
||||
select(WorkflowEdgeModel)
|
||||
.where(WorkflowEdgeModel.flow_id == flow_id)
|
||||
.order_by(WorkflowEdgeModel.id)
|
||||
)
|
||||
edge_rows = list(result.scalars().all())
|
||||
return node_rows, edge_rows
|
||||
|
||||
async def _build_graph(
|
||||
self,
|
||||
flow: WorkflowModel,
|
||||
node_rows: Sequence[WorkflowNodeModel],
|
||||
edge_rows: Sequence[WorkflowEdgeModel],
|
||||
) -> dict:
|
||||
"""由布局(flow.graph)+ 业务明细(node/edge 表)+ 存储源组装完整画布,供前端直接回显。"""
|
||||
layout = flow.graph or {}
|
||||
layout_nodes = {n["id"]: n for n in (layout.get("nodes") or [])}
|
||||
layout_edges = {e["id"]: e for e in (layout.get("edges") or [])}
|
||||
nodes_by_key = {n.node_key: n for n in node_rows}
|
||||
edges_by_key = {e.edge_key: e for e in edge_rows}
|
||||
|
||||
ids = {n.source_id for n in node_rows}
|
||||
src_map: dict[int, StorageNodeModel] = {}
|
||||
if ids:
|
||||
sources = await StorageNodeCRUD(self.auth, self.db).get_list(search={"id": ("in", sorted(ids))})
|
||||
src_map = {s.id: s for s in sources}
|
||||
|
||||
out_nodes: list[dict] = []
|
||||
for key, ln in layout_nodes.items():
|
||||
node_row = nodes_by_key.get(key)
|
||||
if not node_row:
|
||||
continue
|
||||
src = src_map.get(node_row.source_id)
|
||||
node = dict(ln)
|
||||
node["data"] = WorkflowGraphNodeDataSchema(
|
||||
source_id=node_row.source_id,
|
||||
source_path=node_row.source_path,
|
||||
label=ln.get("label") or (src.name if src else None),
|
||||
protocol=src.protocol if src else None,
|
||||
host=src.host if src else None,
|
||||
bucket=src.bucket if src else None,
|
||||
endpoint=src.endpoint if src else None,
|
||||
region=src.region if src else None,
|
||||
path_prefix=src.path_prefix if src else None,
|
||||
).model_dump(exclude_none=True)
|
||||
out_nodes.append(node)
|
||||
|
||||
out_edges: list[dict] = []
|
||||
for key, le in layout_edges.items():
|
||||
edge_row = edges_by_key.get(key)
|
||||
if not edge_row:
|
||||
continue
|
||||
src_node = nodes_by_key.get(edge_row.source_node_key)
|
||||
tgt_node = nodes_by_key.get(edge_row.target_node_key)
|
||||
src = src_map.get(src_node.source_id) if src_node else None
|
||||
tgt = src_map.get(tgt_node.source_id) if tgt_node else None
|
||||
edge = dict(le)
|
||||
edge["data"] = WorkflowGraphEdgeDataSchema(
|
||||
enabled=edge_row.enabled,
|
||||
transfer_mode=edge_row.transfer_mode,
|
||||
multipart_part_size=edge_row.multipart_part_size,
|
||||
multipart_concurrency=edge_row.multipart_concurrency,
|
||||
source_label=src.name if src else (str(src_node.source_id) if src_node else None),
|
||||
target_label=tgt.name if tgt else (str(tgt_node.source_id) if tgt_node else None),
|
||||
source_protocol=src.protocol if src else None,
|
||||
target_protocol=tgt.protocol if tgt else None,
|
||||
source_storage_id=src_node.source_id if src_node else None,
|
||||
target_storage_id=tgt_node.source_id if tgt_node else None,
|
||||
).model_dump(exclude_none=True)
|
||||
out_edges.append(edge)
|
||||
|
||||
return {"nodes": out_nodes, "edges": out_edges}
|
||||
|
||||
# ── 查询 ────────────────────────────────────────────────────────
|
||||
|
||||
async def _to_out(self, obj: WorkflowModel) -> WorkflowOutSchema:
|
||||
out = WorkflowOutSchema.model_validate(obj)
|
||||
node_rows, edge_rows = await self._load_flow_graph(obj.id)
|
||||
out.graph_stats = {"node_count": len(node_rows), "edge_count": len(edge_rows)}
|
||||
if edge_rows:
|
||||
node_map = {n.node_key: n for n in node_rows}
|
||||
out.sources = self._sources_from(edge_rows, node_map)
|
||||
out.targets = self._targets_from(edge_rows, node_map)
|
||||
if obj.graph:
|
||||
out.graph = await self._build_graph(obj, node_rows, edge_rows)
|
||||
return out
|
||||
|
||||
async def _to_out_list(self, objs: Sequence[WorkflowModel]) -> list[WorkflowOutSchema]:
|
||||
"""批量组装列表概览:源/目标列表、画布统计,一次查询避免 N+1。"""
|
||||
flow_ids = [o.id for o in objs]
|
||||
nodes_by_flow: dict[int, list[WorkflowNodeModel]] = defaultdict(list)
|
||||
edges_by_flow: dict[int, list[WorkflowEdgeModel]] = defaultdict(list)
|
||||
if flow_ids:
|
||||
result = await self.db.execute(
|
||||
select(WorkflowNodeModel).where(WorkflowNodeModel.flow_id.in_(flow_ids))
|
||||
)
|
||||
for n in result.scalars().all():
|
||||
nodes_by_flow[n.flow_id].append(n)
|
||||
result = await self.db.execute(
|
||||
select(WorkflowEdgeModel).where(WorkflowEdgeModel.flow_id.in_(flow_ids))
|
||||
)
|
||||
for e in result.scalars().all():
|
||||
edges_by_flow[e.flow_id].append(e)
|
||||
|
||||
outs = [WorkflowOutSchema.model_validate(o) for o in objs]
|
||||
for obj, out in zip(objs, outs, strict=False):
|
||||
out.graph = None
|
||||
ns = nodes_by_flow.get(obj.id, [])
|
||||
es = edges_by_flow.get(obj.id, [])
|
||||
out.graph_stats = {"node_count": len(ns), "edge_count": len(es)}
|
||||
if es:
|
||||
node_map = {n.node_key: n for n in ns}
|
||||
out.sources = self._sources_from(es, node_map)
|
||||
out.targets = self._targets_from(es, node_map)
|
||||
return outs
|
||||
|
||||
async def detail(self, id: int) -> WorkflowOutSchema:
|
||||
obj = await self._crud().get_or_404(id=id)
|
||||
return await self._to_out(obj)
|
||||
|
||||
async def page(
|
||||
self,
|
||||
search: WorkflowQueryParam | None,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> PageResultSchema[WorkflowOutSchema]:
|
||||
result = await self._crud().page(
|
||||
offset=(page_no - 1) * page_size,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search_to_dict(search),
|
||||
)
|
||||
items = await self._to_out_list(result.items)
|
||||
return PageResultSchema[WorkflowOutSchema](
|
||||
page_no=result.page_no,
|
||||
page_size=result.page_size,
|
||||
total=result.total,
|
||||
has_next=result.has_next,
|
||||
items=items,
|
||||
)
|
||||
|
||||
async def get_list(self, search: WorkflowQueryParam | None = None) -> list[WorkflowOutSchema]:
|
||||
objs = await self._crud().get_list(search=search_to_dict(search), order_by=[{"id": "asc"}])
|
||||
return await self._to_out_list(objs)
|
||||
|
||||
# ── 写入 ────────────────────────────────────────────────────────
|
||||
|
||||
async def _prepare_and_save(
|
||||
self, flow_id: int | None, data: WorkflowCreateSchema | WorkflowUpdateSchema
|
||||
) -> tuple[dict, list[WorkflowNodeSchema], list[WorkflowEdgeSchema]]:
|
||||
"""拆分画布:业务配置写入明细表,布局存入 flow.graph。"""
|
||||
data_dict = data.model_dump(exclude_unset=True, exclude_none=True)
|
||||
graph = data_dict.pop("graph", None)
|
||||
if not graph:
|
||||
raise CustomException(msg="创建失败,请至少添加一条传输连线")
|
||||
split = self._split_graph(graph)
|
||||
await self._validate_nodes(split.sources, split.targets)
|
||||
data_dict["graph"] = split.layout
|
||||
if flow_id is not None:
|
||||
await self._save_graph(flow_id, split.nodes, split.edges)
|
||||
return data_dict, split.nodes, split.edges
|
||||
|
||||
async def create(self, data: WorkflowCreateSchema) -> WorkflowOutSchema:
|
||||
exist = await self._crud().get(name=data.name)
|
||||
if exist:
|
||||
raise CustomException(msg="创建失败,流程名称已存在")
|
||||
data_dict, node_items, edge_items = await self._prepare_and_save(None, data)
|
||||
obj = await self._crud().create(data=data_dict)
|
||||
await self._save_graph(obj.id, node_items, edge_items)
|
||||
return await self._to_out(obj)
|
||||
|
||||
async def update(self, id: int, data: WorkflowUpdateSchema) -> WorkflowOutSchema:
|
||||
await self._crud().get_or_404(id=id, msg="更新失败,该流程不存在")
|
||||
exist = await self._crud().get(name=data.name)
|
||||
if exist and exist.id != id:
|
||||
raise CustomException(msg="更新失败,流程名称已存在")
|
||||
# _prepare_and_save 在 flow_id 非空时已覆写明细,无需再次 _save_graph
|
||||
data_dict, _, _ = await self._prepare_and_save(id, data)
|
||||
await self._crud().update(id=id, data=data_dict)
|
||||
obj = await self._crud().get_or_404(id=id)
|
||||
return await self._to_out(obj)
|
||||
|
||||
async def delete(self, ids: list[int]) -> None:
|
||||
if not ids:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
await self._crud().delete(ids=ids)
|
||||
# 子表随父流程一并物理清理(无软删消费场景)
|
||||
await WorkflowNodeCRUD(self.auth, self.db).hard_delete_by_flow_ids(ids)
|
||||
await WorkflowEdgeCRUD(self.auth, self.db).hard_delete_by_flow_ids(ids)
|
||||
|
||||
# ── 执行 ────────────────────────────────────────────────────────
|
||||
|
||||
async def execute(self, id: int, source_paths: dict[str, str] | None = None) -> list[int]:
|
||||
"""执行传输流程:直接读取业务明细表,每条启用的连线生成一个传输任务。
|
||||
|
||||
支持 1对多 / 多对1 拓扑;源文件/目录优先取执行时传入的 source_paths
|
||||
(按源存储源ID映射),未传入时回退使用节点配置的默认源目录;
|
||||
传输方式未配置时默认流式传输;禁用的连线不参与执行。
|
||||
"""
|
||||
obj = await self._crud().get_or_404(id=id, msg="执行失败,该流程不存在")
|
||||
node_rows, edge_rows = await self._load_flow_graph(id)
|
||||
# 只执行启用的连线(禁用的连线不生成传输任务)
|
||||
enabled_edges = [e for e in edge_rows if e.enabled]
|
||||
if not enabled_edges:
|
||||
raise CustomException(msg="执行失败,该流程画布没有启用的传输连线")
|
||||
nodes_by_key = {n.node_key: n for n in node_rows}
|
||||
source_paths = source_paths or {}
|
||||
|
||||
# 阶段一:解析并校验每条连线
|
||||
plans: list[WorkflowTransferPlanSchema] = []
|
||||
for e in enabled_edges:
|
||||
src_node = nodes_by_key.get(e.source_node_key)
|
||||
tgt_node = nodes_by_key.get(e.target_node_key)
|
||||
if not src_node or not tgt_node:
|
||||
raise CustomException(msg=f"执行失败,连线 {e.edge_key} 对应的节点不存在")
|
||||
src_id, tgt_id = src_node.source_id, tgt_node.source_id
|
||||
src_path = (source_paths.get(str(src_id)) or (src_node.source_path or "")).strip()
|
||||
# 目标目录由目标节点默认源目录决定
|
||||
tgt_path = (tgt_node.source_path or "").strip()
|
||||
edge_label = f"「存储源{src_id} → 存储源{tgt_id}」"
|
||||
if not src_id or not tgt_id:
|
||||
raise CustomException(msg=f"执行失败,连线 {edge_label} 存在无效的存储源节点")
|
||||
if src_id == tgt_id:
|
||||
raise CustomException(msg=f"执行失败,连线 {edge_label} 源与目标不能相同")
|
||||
if not src_path or not tgt_path:
|
||||
missing = "未选择源文件/目录" if not src_path else "目标节点未配置默认目录"
|
||||
raise CustomException(msg=f"执行失败,连线 {edge_label} {missing}")
|
||||
plans.append(
|
||||
WorkflowTransferPlanSchema(
|
||||
src_id=src_id,
|
||||
tgt_id=tgt_id,
|
||||
src_path=src_path,
|
||||
tgt_path=tgt_path,
|
||||
transfer_mode=e.transfer_mode or "stream",
|
||||
multipart_part_size=e.multipart_part_size,
|
||||
multipart_concurrency=e.multipart_concurrency,
|
||||
)
|
||||
)
|
||||
|
||||
# 阶段二:统一校验所有涉及的存储源可用,再逐个生成传输任务
|
||||
node_service = StorageNodeService(self.auth, self.db)
|
||||
sources = await node_service.get_active_sources(
|
||||
[p.src_id for p in plans] + [p.tgt_id for p in plans]
|
||||
)
|
||||
name_map = {s.id: s.name for s in sources}
|
||||
|
||||
transfer_service = StorageTransferService(self.auth, self.db)
|
||||
task_ids: list[int] = []
|
||||
for p in plans:
|
||||
src_label = name_map.get(p.src_id) or f"存储源{p.src_id}"
|
||||
tgt_label = name_map.get(p.tgt_id) or f"存储源{p.tgt_id}"
|
||||
# 任务名最大 128 字符,超长时截断避免 422
|
||||
task_name = f"{obj.name}-{src_label}→{tgt_label}"[:128]
|
||||
task_id = await transfer_service.create(
|
||||
TransferTaskCreateSchema(
|
||||
name=task_name,
|
||||
task_type="parallel",
|
||||
source_type="remote",
|
||||
source_id=p.src_id,
|
||||
source_path=p.src_path,
|
||||
targets=[TransferTargetSchema(target_id=p.tgt_id, target_path=p.tgt_path)],
|
||||
# flow 侧以 str 保存传输方式,接口侧字面量校验由 pydantic 兜底,此处仅收窄静态类型
|
||||
transfer_mode=cast(TransferMode | None, p.transfer_mode),
|
||||
multipart_part_size=p.multipart_part_size,
|
||||
multipart_concurrency=p.multipart_concurrency,
|
||||
)
|
||||
)
|
||||
task_ids.append(task_id)
|
||||
return task_ids
|
||||
Reference in New Issue
Block a user