| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- package com.agent.management.engine;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.stereotype.Component;
- import java.io.IOException;
- import java.nio.file.Files;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- import java.util.regex.Pattern;
- /**
- * 工作流运行目录管理器
- * 为每次工作流执行创建独立的工作目录,永久保留
- *
- * <p>目录结构:
- * <ul>
- * <li>新建:{@code {basePath}/{workflowName}/{runId}/}(workflowName 为 kebab-case 唯一标识)</li>
- * <li>兼容:旧目录 {@code {basePath}/{workflowId}/{runId}/} 仍可读取(用户改名前的历史 run)</li>
- * </ul>
- */
- @Slf4j
- @Component
- public class WorkflowRunDirManager {
- /** runId 白名单:UUID 截取的 8~16 位十六进制 */
- private static final Pattern RUN_ID_PATTERN = Pattern.compile("^[a-fA-F0-9]{1,16}$");
- private final Path basePath;
- public WorkflowRunDirManager(
- @Value("${workflow.run-dir:./data/workflow-runs}") String basePath) {
- this.basePath = Paths.get(basePath).toAbsolutePath().normalize();
- try {
- Files.createDirectories(this.basePath);
- log.info("[RunDirManager] 工作目录基路径: {}", this.basePath);
- } catch (IOException e) {
- throw new IllegalStateException("无法创建工作流运行目录: " + this.basePath, e);
- }
- }
- /**
- * 为指定工作流执行创建工作目录(新建使用 workflowName 作为目录段)。
- *
- * @param workflowId 工作流内部 ID(兼容旧目录兜底用)
- * @param workflowName 工作流唯一标识(kebab-case),新建目录使用此字段
- * @param runId 运行 ID(必须为 1~16 位十六进制)
- */
- public Path createRunDir(Long workflowId, String workflowName, String runId) {
- validateRunId(runId);
- Path runDir = resolveByName(workflowName, runId);
- try {
- Files.createDirectories(runDir);
- log.info("[RunDirManager] 创建运行目录: {}", runDir);
- return runDir;
- } catch (IOException e) {
- throw new RuntimeException("创建运行目录失败: " + runDir, e);
- }
- }
- /**
- * 获取指定运行的工作目录(已存在时返回,不存在返回 null)。
- * 查询顺序:先按 name,再按 id 兜底。
- */
- public Path getRunDir(Long workflowId, String workflowName, String runId) {
- try {
- validateRunId(runId);
- } catch (IllegalArgumentException e) {
- return null;
- }
- // 1. 优先按 name 查找
- if (workflowName != null && !workflowName.isEmpty()) {
- Path byName = resolveByName(workflowName, runId);
- if (Files.isDirectory(byName)) return byName;
- }
- // 2. 回退按 id 查找(兼容旧目录)
- if (workflowId != null) {
- Path byId = resolveById(workflowId, runId);
- if (Files.isDirectory(byId)) return byId;
- }
- return null;
- }
- /**
- * 兼容旧调用:仅按 id 创建/查询。新代码应使用带 name 的重载。
- */
- @Deprecated
- public Path createRunDir(Long workflowId, String runId) {
- return createRunDir(workflowId, String.valueOf(workflowId), runId);
- }
- @Deprecated
- public Path getRunDir(Long workflowId, String runId) {
- return getRunDir(workflowId, null, runId);
- }
- /**
- * 获取基路径
- */
- public Path getBasePath() {
- return basePath;
- }
- /**
- * 校验 runId 格式,防止路径穿越
- */
- private void validateRunId(String runId) {
- if (runId == null || !RUN_ID_PATTERN.matcher(runId).matches()) {
- throw new IllegalArgumentException("非法 runId: " + runId);
- }
- }
- private Path resolveByName(String workflowName, String runId) {
- Path runDir = basePath.resolve(workflowName).resolve(runId).normalize();
- if (!runDir.startsWith(basePath)) {
- throw new IllegalArgumentException("解析后的 runId 路径越界: " + runDir);
- }
- return runDir;
- }
- private Path resolveById(Long workflowId, String runId) {
- Path runDir = basePath.resolve(String.valueOf(workflowId)).resolve(runId).normalize();
- if (!runDir.startsWith(basePath)) {
- throw new IllegalArgumentException("解析后的 runId 路径越界: " + runDir);
- }
- return runDir;
- }
- }
|