{@code metadata} Part 使用 JSON;工作流文件 Part 使用 + * {@code files.<开始节点文件参数名>},同名 Part 可重复上传多个文件。
+ * + * @param metadata 工作流 ID 与普通运行变量 + * @param multipartRequest multipart 请求 + * @param request Servlet 请求 + * @return 保留执行 ID 字符串并附带工作流拓扑的响应 + */ + @PostMapping( + value = "/runAsync", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @RequirePublishedAccess( + resourceType = "WORKFLOW", + idExpr = "#metadata.id", + denyMessage = "工作流尚未发布") + public PublicWorkflowRunResult runAsyncMultipart( + @Valid + @RequestPart("metadata") + PublicWorkflowRunMetadata metadata, + MultipartHttpServletRequest multipartRequest, + HttpServletRequest request) { + SysApiKey apiKey = + workflowApiPermissionService.assertWorkflowApi( + request.getHeader("ApiKey"), + request.getRequestURI()); + Workflow workflow = loadExecutableWorkflow(metadata.getId()); + PublicWorkflowTopology topology = + publicWorkflowTopologyService.resolve(workflow); + Map继承原有 {@link Result} 并继续把执行 ID 放在 {@code data}, + * 新增 {@code workflow} 区块以保持旧调用方兼容。
+ */ +public class PublicWorkflowRunResult extends Result运行时由循环体末节点隐式返回父循环节点,父循环完成全部迭代后 + * 才会触发同作用域后继。公开拓扑使用末节点到后继的无环屏障表达这一约束。
+ * + * @param nodeById 节点索引 + */ + private void addLoopCompletionBarriers( + Map若作用域叶子本身是嵌套循环,则继续展开到嵌套循环体叶子, + * 确保外层后继等待完整嵌套作用域结束。
+ * + * @param loopNodeId 循环节点 ID + * @param nodeById 节点索引 + * @param visiting 正在展开的循环节点 + * @return 实际完成屏障节点 + */ + private List同一层级节点按发布定义顺序稳定排列。若发布图意外存在环, + * 标准拓扑序之后按定义顺序追加受环路影响的未解析节点。
+ * + * @param nodeById 节点索引 + * @return 拓扑排序结果 + */ + private TopologyOrder resolveTopology( + LinkedHashMap{@code metadata} 只属于 API 信封,不参与工作流变量映射; + * {@code files.<参数名>} 去除前缀后才作为开始节点参数名。
+ * + * @param rawFileParts Servlet 解析出的原始文件 Part + * @return 以开始节点文件参数名分组的文件 + * @throws BusinessException Part 未使用规定命名空间或参数名为空时抛出 + */ + public Map该方法只接受由可信业务记录恢复出的句柄,不得直接使用外部传入的 locator。
+ * + * @param handle 物理对象写入句柄 + * @return 文件输入流,由调用方关闭 + * @throws IOException 无法打开物理对象时抛出 + * @throws UnsupportedOperationException 当前后端尚未实现可恢复读取时抛出 + */ + default InputStream readRecoverable(FileStorageWriteHandle handle) throws IOException { + throw unsupportedRecoverableOperation("readRecoverable"); + } + /** * 精确且幂等地删除句柄对应的物理对象。 * diff --git a/easyflow-commons/easyflow-common-file-storage/src/main/java/tech/easyflow/common/filestorage/impl/LocalFileStorageServiceImpl.java b/easyflow-commons/easyflow-common-file-storage/src/main/java/tech/easyflow/common/filestorage/impl/LocalFileStorageServiceImpl.java index ef92a88e..fd18a241 100644 --- a/easyflow-commons/easyflow-common-file-storage/src/main/java/tech/easyflow/common/filestorage/impl/LocalFileStorageServiceImpl.java +++ b/easyflow-commons/easyflow-common-file-storage/src/main/java/tech/easyflow/common/filestorage/impl/LocalFileStorageServiceImpl.java @@ -272,6 +272,27 @@ public class LocalFileStorageServiceImpl implements FileStorageService { } } + /** + * 按句柄固化的本地根目录精确打开普通文件。 + * + * @param handle 本地可恢复写句柄 + * @return 文件输入流,由调用方关闭 + * @throws IOException 文件不存在、不可读或路径不安全时抛出 + */ + @Override + public InputStream readRecoverable(FileStorageWriteHandle handle) throws IOException { + requireLocalHandle(handle); + Path target = resolveControlledTarget(handle, false); + if (!Files.exists(target, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("本地可恢复文件不存在: " + target); + } + if (Files.isSymbolicLink(target) + || !Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("本地可恢复读取目标不是普通文件: " + target); + } + return Files.newInputStream(target); + } + /** * 精确且幂等地删除句柄对应的最终文件与确定性暂存文件,并确认两者均不存在。 * diff --git a/easyflow-commons/easyflow-common-file-storage/src/main/java/tech/easyflow/common/filestorage/impl/XFIleStorageServiceImpl.java b/easyflow-commons/easyflow-common-file-storage/src/main/java/tech/easyflow/common/filestorage/impl/XFIleStorageServiceImpl.java index 66ed08a5..06ff94da 100644 --- a/easyflow-commons/easyflow-common-file-storage/src/main/java/tech/easyflow/common/filestorage/impl/XFIleStorageServiceImpl.java +++ b/easyflow-commons/easyflow-common-file-storage/src/main/java/tech/easyflow/common/filestorage/impl/XFIleStorageServiceImpl.java @@ -1,7 +1,9 @@ package tech.easyflow.common.filestorage.impl; +import io.minio.GetObjectArgs; import org.dromara.x.file.storage.core.FileInfo; import org.dromara.x.file.storage.core.platform.FileStorage; +import org.dromara.x.file.storage.core.platform.MinioFileStorage; import org.dromara.x.file.storage.core.recorder.FileRecorder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -198,6 +200,44 @@ public class XFIleStorageServiceImpl implements FileStorageService { } } + /** + * 按句柄固定的平台和对象键读取物理文件。 + * + *MinIO 使用已配置客户端直接读取,兼容私有桶和内网端点;其他平台仅在能够 + * 精确推导公开 URL 时使用现有读取能力。
+ * + * @param handle x-file-storage 可恢复写句柄 + * @return 文件输入流,由调用方关闭 + * @throws IOException 平台不支持精确读取或对象读取失败时抛出 + */ + @Override + public InputStream readRecoverable(FileStorageWriteHandle handle) throws IOException { + FileStorage storage = requireStorage(handle); + requirePersistedBasePathSupport(storage, handle); + FileInfo fileInfo = toFileInfo(handle); + if (storage instanceof MinioFileStorage minioStorage) { + try { + return minioStorage.getClient().getObject( + GetObjectArgs.builder() + .bucket(minioStorage.getBucketName()) + .object(storage.getFileKey(fileInfo)) + .build()); + } catch (Exception exception) { + throw new IOException("读取 MinIO 可恢复文件失败", exception); + } + } + String url = deriveUrlBestEffort(storage, fileInfo); + if (!StringUtils.hasText(url)) { + throw new IOException("当前 x-file-storage 平台不支持可恢复文件读取: " + + storage.getClass().getName()); + } + try { + return readStream(url); + } catch (RuntimeException exception) { + throw new IOException("读取 x-file-storage 可恢复文件失败", exception); + } + } + /** * 直接调用句柄指定平台的物理删除与存在检查,绕过依赖 URL 记录的聚合删除路径。 * diff --git a/easyflow-commons/easyflow-common-file-storage/src/test/java/tech/easyflow/common/filestorage/FileStorageManagerTest.java b/easyflow-commons/easyflow-common-file-storage/src/test/java/tech/easyflow/common/filestorage/FileStorageManagerTest.java index 9ff4f662..3671a9dc 100644 --- a/easyflow-commons/easyflow-common-file-storage/src/test/java/tech/easyflow/common/filestorage/FileStorageManagerTest.java +++ b/easyflow-commons/easyflow-common-file-storage/src/test/java/tech/easyflow/common/filestorage/FileStorageManagerTest.java @@ -22,7 +22,7 @@ public class FileStorageManagerTest { * 验证 prepare 使用当前后端,而后续操作在默认后端切换后仍按句柄后端路由。 */ @Test - public void recoverableOperationsRouteByPreparedBackendAfterSwitch() { + public void recoverableOperationsRouteByPreparedBackendAfterSwitch() throws IOException { RecordingStorage local = new RecordingStorage("local"); RecordingStorage xFile = new RecordingStorage("xFileStorage"); AtomicReference