From eb7b06607c0d30d61cd69d0ea3c1d54a136feccb Mon Sep 17 00:00:00 2001 From: even <827656971@qq.com> Date: Tue, 27 May 2025 21:26:20 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=AE=E6=A0=87=E4=B8=8A=E4=BC=A0worker?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E3=80=81=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E7=94=9F=E6=88=90worker=20=E3=80=81=20AFLworker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resp/machine/MachineInfoResp.java | 2 +- .../process/engine/constant/AFLConstant.java | 8 + .../constant/TestCaseGenerationConstant.java | 8 + .../engine/manager/impl/MemoryLogManager.java | 1 + .../PipelineSchedulingBootstrapper.java | 4 +- .../ci/process/engine/worker/AFLWorker.java | 145 +++------- .../engine/worker/TargetHandleWorker.java | 56 +++- .../worker/TestCaseGenerationWorker.java | 52 ++++ .../process/dataObject/log/PipTaskLog.java | 3 + .../dataObject/machine/MachineInfo.java | 2 +- .../dataObject/pipeline/PipPipeline.java | 5 +- .../casic/ci/process/util/SftpUploadUtil.java | 266 ++++++++++++++++++ .../test/java/cd/casic/server/SftpTest.java | 30 ++ 13 files changed, 469 insertions(+), 113 deletions(-) create mode 100644 modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/constant/AFLConstant.java create mode 100644 modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/constant/TestCaseGenerationConstant.java create mode 100644 modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/util/SftpUploadUtil.java create mode 100644 ops-server/src/test/java/cd/casic/server/SftpTest.java diff --git a/modules/module-ci-common-pipeline/src/main/java/cd/casic/ci/common/pipeline/resp/machine/MachineInfoResp.java b/modules/module-ci-common-pipeline/src/main/java/cd/casic/ci/common/pipeline/resp/machine/MachineInfoResp.java index 983b2d61..5ff0c90f 100644 --- a/modules/module-ci-common-pipeline/src/main/java/cd/casic/ci/common/pipeline/resp/machine/MachineInfoResp.java +++ b/modules/module-ci-common-pipeline/src/main/java/cd/casic/ci/common/pipeline/resp/machine/MachineInfoResp.java @@ -14,7 +14,7 @@ public class MachineInfoResp { * id */ @TableId(type = IdType.ASSIGN_ID) - private Long id; + private String id; /** * 主机ip diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/constant/AFLConstant.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/constant/AFLConstant.java new file mode 100644 index 00000000..1b355927 --- /dev/null +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/constant/AFLConstant.java @@ -0,0 +1,8 @@ +package cd.casic.ci.process.engine.constant; + +public class AFLConstant { + /** + * 脚本内容 + */ + public static final String COMMAND_SCRIPT ="buildScript"; +} diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/constant/TestCaseGenerationConstant.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/constant/TestCaseGenerationConstant.java new file mode 100644 index 00000000..18d761b5 --- /dev/null +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/constant/TestCaseGenerationConstant.java @@ -0,0 +1,8 @@ +package cd.casic.ci.process.engine.constant; + +public class TestCaseGenerationConstant { + /** + * 脚本内容key + */ + public static final String COMMAND_SCRIPT ="buildScript"; +} diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/manager/impl/MemoryLogManager.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/manager/impl/MemoryLogManager.java index 264e0c57..27cb3741 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/manager/impl/MemoryLogManager.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/manager/impl/MemoryLogManager.java @@ -32,6 +32,7 @@ public class MemoryLogManager implements LoggerManager { private final Map taskIdMemoryLogMap = new ConcurrentHashMap<>(); public final Integer FLUSH_DB_SIZE=2*1024*1024; +// public final Integer FLUSH_DB_SIZE=1000; private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); /** * 缓存最近一次执行的日志,key是taskId,val是数据库id(已入库的情况下)用于buffer满了增加日志内容 diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/scheduler/PipelineSchedulingBootstrapper.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/scheduler/PipelineSchedulingBootstrapper.java index f0dddc8c..5505b438 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/scheduler/PipelineSchedulingBootstrapper.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/scheduler/PipelineSchedulingBootstrapper.java @@ -19,8 +19,8 @@ import java.util.List; * @Date: 2025/5/26 17:28 * @Description: */ -@Component -@RequiredArgsConstructor +//@Component +//@RequiredArgsConstructor public class PipelineSchedulingBootstrapper { @Resource private PipelineSchedulingPropertiesServiceImpl taskService; diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/AFLWorker.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/AFLWorker.java index e1e02985..c307fc39 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/AFLWorker.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/AFLWorker.java @@ -1,12 +1,20 @@ package cd.casic.ci.process.engine.worker; import cd.casic.ci.common.pipeline.annotation.Plugin; +import cd.casic.ci.process.engine.constant.AFLConstant; +import cd.casic.ci.process.engine.constant.DIYImageExecuteCommandConstant; +import cd.casic.ci.process.engine.constant.EngineRuntimeConstant; +import cd.casic.ci.process.engine.runContext.BaseRunContext; +import cd.casic.ci.process.engine.runContext.PipelineRunContext; import cd.casic.ci.process.engine.runContext.TaskRunContext; import cd.casic.ci.process.process.dataObject.base.PipBaseElement; +import cd.casic.ci.process.process.dataObject.log.PipTaskLog; +import cd.casic.ci.process.process.dataObject.machine.MachineInfo; import cd.casic.ci.process.process.dataObject.pipeline.PipPipeline; import cd.casic.ci.process.process.dataObject.target.TargetVersion; import cd.casic.ci.process.process.dataObject.task.PipTask; import cd.casic.ci.process.process.service.target.TargetVersionService; +import cd.casic.ci.process.util.CryptogramUtil; import cd.casic.framework.commons.exception.ServiceException; import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants; import com.jcraft.jsch.*; @@ -21,121 +29,50 @@ import java.util.Map; @Plugin(taskType = "AFL") @Slf4j -public class AFLWorker extends SshWorker{ - @Resource - private TargetVersionService targetVersionService; +public class AFLWorker extends SshWorker { + @Override public void execute(TaskRunContext context) { - String filePath = ""; + int statusCode = -1; Map localVariables = context.getLocalVariables(); - PipBaseElement taskContextDef = context.getContextDef(); - if (taskContextDef instanceof PipTask pipTask){ - // 查询并下载目标文件 - String pipelineId = pipTask.getPipelineId(); - //根据流水线id查询流水线信息 - PipPipeline pipeline = (PipPipeline) getContextManager().getContext(pipelineId).getContextDef(); - //根据目标id查询目标信息 - if (StringUtils.isEmpty(pipeline.getTargetVersionId())){ - throw new ServiceException(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),"目标文件不存在"); - } - TargetVersion targetVersion = targetVersionService.getById(pipeline.getTargetVersionId()); - filePath = targetVersion.getFilePath(); - File file = new File(filePath); - if (!file.exists() || !file.canRead()) { - log.error("目标文件不存在或不可读"); - localVariables.put("statusCode", "-1"); - append(context,"目标文件不存在或不可读"); + if (context.getContextDef() instanceof PipTask taskDef) { + log.info(taskDef.getTaskName()); + Map taskProperties = taskDef.getTaskProperties(); + Object commandScriptObj = taskProperties.get(AFLConstant.COMMAND_SCRIPT); +// Object machineIdObj = taskProperties.get(DIYImageExecuteCommandConstant.MACHINE_ID); + String commandScript = commandScriptObj instanceof String ? ((String) commandScriptObj) : null; + + PipPipeline pipeline = (PipPipeline) getContextManager().getContext(taskDef.getPipelineId()).getContextDef(); + String machineId = pipeline.getMachineId(); + if (StringUtils.isEmpty(commandScript)||StringUtils.isEmpty(machineId)) { +// 缺少参数 toBadEnding(); } - // 上传文件 - // 执行shell脚本 - } - } - public static void main(String[] args) { - String remoteHost = "your-remote-server-ip-or-hostname"; // 远程服务器IP或主机名 - int remotePort = 22; // SFTP 默认端口通常是 22 (SSH 端口) - String username = "your-username"; // 远程服务器用户名 - String password = "your-password"; // 远程服务器密码 或 SSH Key 路径 - String localFilePath = "path/to/your/local/file.txt"; // 要上传的本地文件路径 - String remoteDir = "/path/on/remote/server/"; // 远程服务器存放文件的目录 - String remoteFileName = "uploaded_file.txt"; // 上传到远程服务器的文件名 (可以和本地不同) - - Session session = null; - Channel channel = null; - ChannelSftp channelSftp = null; - FileInputStream fis = null; - - try { - JSch jsch = new JSch(); - - // 如果使用 SSH Key 认证,可以添加身份文件: - // jsch.addIdentity("/path/to/your/private/key"); // 替换为你的私钥文件路径 - - session = jsch.getSession(username, remoteHost, remotePort); - - // 如果使用密码认证: - session.setPassword(password); - - // 设置连接不进行主机密钥检查 (生产环境不推荐,应该配置known_hosts) - java.util.Properties config = new java.util.Properties(); - config.put("StrictHostKeyChecking", "no"); - session.setConfig(config); - - session.connect(); - System.out.println("SFTP Session 连接成功."); - - channel = session.openChannel("sftp"); - channel.connect(); - System.out.println("SFTP Channel 打开成功."); - - channelSftp = (ChannelSftp) channel; - - // 切换到远程目录 (如果目录不存在,可能需要先创建) try { - channelSftp.cd(remoteDir); - } catch (SftpException e) { - System.err.println("远程目录不存在,尝试创建: " + remoteDir); - try { - channelSftp.mkdir(remoteDir); - channelSftp.cd(remoteDir); - } catch (SftpException e2) { - System.err.println("创建远程目录失败: " + remoteDir); - throw e2; // 抛出异常终止上传 - } - } + //将节点的配置信息反编译成对象 + log.info("构建脚本" + commandScript); + //如果machineId为0,则说明该节点没有配置机器,则使用开始节点的机器 - // 上传文件 - File localFile = new File(localFilePath); - fis = new FileInputStream(localFile); - - channelSftp.put(fis, remoteFileName); - System.out.println("文件上传成功到: " + remoteDir + remoteFileName); - - } catch (JSchException | SftpException | IOException e) { - System.err.println("SFTP 文件上传过程中发生异常: " + e.getMessage()); - e.printStackTrace(); - } finally { - // 关闭资源 - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - e.printStackTrace(); - } + //获取机器 + MachineInfo machineInfoDO = this.getMachineInfoService().getById(machineId); + statusCode = shell(machineInfoDO, CryptogramUtil.doDecrypt(machineInfoDO.getPassword()), context, + "echo \"自定义镜像执行命令\"", + commandScript + ); + } catch (Exception e) { + String errorMessage = "该节点配置信息为空,请先配置该节点信息" + "\r\n"; + log.error("执行ssh失败:", e); + append(context, errorMessage); + toBadEnding(); } - if (channelSftp != null) { - channelSftp.disconnect(); - System.out.println("SFTP Channel 已断开."); - } - if (channel != null) { - channel.disconnect(); - } - if (session != null) { - session.disconnect(); - System.out.println("SFTP Session 已断开."); + if (statusCode == 0) { + log.info("节点执行完成"); + } else { + log.error("节点执行失败"); } + localVariables.put(DIYImageExecuteCommandConstant.STATUS_CODE, statusCode); } } } diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/TargetHandleWorker.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/TargetHandleWorker.java index 34f46a63..6a13207d 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/TargetHandleWorker.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/TargetHandleWorker.java @@ -3,8 +3,23 @@ package cd.casic.ci.process.engine.worker; import cd.casic.ci.common.pipeline.annotation.Plugin; import cd.casic.ci.process.engine.runContext.TaskRunContext; import cd.casic.ci.process.process.dataObject.base.PipBaseElement; +import cd.casic.ci.process.process.dataObject.machine.MachineInfo; +import cd.casic.ci.process.process.dataObject.pipeline.PipPipeline; +import cd.casic.ci.process.process.dataObject.target.TargetVersion; +import cd.casic.ci.process.process.dataObject.task.PipTask; +import cd.casic.ci.process.process.service.machine.MachineInfoService; +import cd.casic.ci.process.process.service.target.TargetVersionService; +import cd.casic.ci.process.util.CryptogramUtil; +import cd.casic.ci.process.util.SftpUploadUtil; +import cd.casic.framework.commons.exception.ServiceException; +import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants; import com.alibaba.fastjson.JSON; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.util.Map; /** * 目标处理worker @@ -13,11 +28,44 @@ import lombok.extern.slf4j.Slf4j; @Slf4j @Plugin(taskType = "code") public class TargetHandleWorker extends BaseWorker{ + @Resource + private TargetVersionService targetVersionService; + @Resource + private MachineInfoService machineInfoService; @Override public void execute(TaskRunContext context) { - PipBaseElement contextDef = context.getContextDef(); - String id = contextDef.getId(); - log.info("==============触发worker执行========"); - log.info("==========运行context:{}===========", JSON.toJSONString(context)); + String filePath = ""; + Map localVariables = context.getLocalVariables(); + PipBaseElement taskContextDef = context.getContextDef(); + if (taskContextDef instanceof PipTask pipTask){ + // 查询并下载目标文件 + String pipelineId = pipTask.getPipelineId(); + //根据流水线id查询流水线信息 + PipPipeline pipeline = (PipPipeline) getContextManager().getContext(pipelineId).getContextDef(); + //根据目标id查询目标信息 + if (StringUtils.isEmpty(pipeline.getTargetVersionId())){ + throw new ServiceException(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),"目标文件不存在"); + } + TargetVersion targetVersion = targetVersionService.getById(pipeline.getTargetVersionId()); + filePath = targetVersion.getFilePath(); + File file = new File(filePath); + if (!file.exists() || !file.canRead()) { + log.error("目标文件不存在或不可读"); + localVariables.put("statusCode", "-1"); + append(context,"目标文件不存在或不可读"); + toBadEnding(); + } + // 上传文件 + String machineId = pipeline.getMachineId(); + MachineInfo byId = machineInfoService.getById(machineId); + append(context,"开始文件上传"); + try { + SftpUploadUtil.uploadFileViaSftp(byId.getMachineHost(),byId.getSshPort(),byId.getUsername(), CryptogramUtil.doDecrypt(byId.getPassword()),null,file.getAbsolutePath(),"/home/casic/706/ai_test_527",file.getName()); + } catch (SftpUploadUtil.SftpUploadException e) { + log.error("文件上传失败",e); + toBadEnding(); + } + append(context,"文件上传至"+byId.getMachineHost()+" /home/casic/706/ai_test_527"); + } } } diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/TestCaseGenerationWorker.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/TestCaseGenerationWorker.java index 72b65770..be2d1b70 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/TestCaseGenerationWorker.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/engine/worker/TestCaseGenerationWorker.java @@ -1,12 +1,64 @@ package cd.casic.ci.process.engine.worker; import cd.casic.ci.common.pipeline.annotation.Plugin; +import cd.casic.ci.process.engine.constant.AFLConstant; +import cd.casic.ci.process.engine.constant.DIYImageExecuteCommandConstant; +import cd.casic.ci.process.engine.constant.TestCaseGenerationConstant; import cd.casic.ci.process.engine.runContext.TaskRunContext; +import cd.casic.ci.process.process.dataObject.machine.MachineInfo; +import cd.casic.ci.process.process.dataObject.pipeline.PipPipeline; +import cd.casic.ci.process.process.dataObject.task.PipTask; +import cd.casic.ci.process.util.CryptogramUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; @Plugin(taskType = "TEST_CASE_GENERATION") +@Slf4j public class TestCaseGenerationWorker extends SshWorker{ @Override public void execute(TaskRunContext context) { + int statusCode = -1; + Map localVariables = context.getLocalVariables(); + if (context.getContextDef() instanceof PipTask taskDef) { + log.info(taskDef.getTaskName()); + Map taskProperties = taskDef.getTaskProperties(); + Object commandScriptObj = taskProperties.get(TestCaseGenerationConstant.COMMAND_SCRIPT); +// Object machineIdObj = taskProperties.get(DIYImageExecuteCommandConstant.MACHINE_ID); + String commandScript = commandScriptObj instanceof String ? ((String) commandScriptObj) : null; + PipPipeline pipeline = (PipPipeline) getContextManager().getContext(taskDef.getPipelineId()).getContextDef(); + String machineId = pipeline.getMachineId(); + if (StringUtils.isEmpty(commandScript)||StringUtils.isEmpty(machineId)) { +// 缺少参数 + toBadEnding(); + } + + try { + //将节点的配置信息反编译成对象 + log.info("构建脚本" + commandScript); + + //如果machineId为0,则说明该节点没有配置机器,则使用开始节点的机器 + + //获取机器 + MachineInfo machineInfoDO = this.getMachineInfoService().getById(machineId); + statusCode = shell(machineInfoDO, CryptogramUtil.doDecrypt(machineInfoDO.getPassword()), context, + "echo \"自定义镜像执行命令\"", + commandScript + ); + } catch (Exception e) { + String errorMessage = "该节点配置信息为空,请先配置该节点信息" + "\r\n"; + log.error("执行ssh失败:", e); + append(context, errorMessage); + toBadEnding(); + } + if (statusCode == 0) { + log.info("节点执行完成"); + } else { + log.error("节点执行失败"); + } + localVariables.put(DIYImageExecuteCommandConstant.STATUS_CODE, statusCode); + } } } diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/log/PipTaskLog.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/log/PipTaskLog.java index be769290..558e1da2 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/log/PipTaskLog.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/log/PipTaskLog.java @@ -2,6 +2,8 @@ package cd.casic.ci.process.process.dataObject.log; import cd.casic.ci.process.constant.CommandConstant; import cd.casic.framework.commons.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import lombok.EqualsAndHashCode; @@ -10,6 +12,7 @@ import lombok.EqualsAndHashCode; public class PipTaskLog extends BaseDO { private String taskId; private String content; + @TableId(type = IdType.ASSIGN_ID) private String id; public void append(String content){ this.content += CommandConstant.ENTER; diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/machine/MachineInfo.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/machine/MachineInfo.java index 06649eda..e24dc537 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/machine/MachineInfo.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/machine/MachineInfo.java @@ -34,7 +34,7 @@ public class MachineInfo extends BaseDO { * id */ @TableId(type = IdType.ASSIGN_ID) - private Long id; + private String id; /** * 主机ip diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/pipeline/PipPipeline.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/pipeline/PipPipeline.java index 37245d10..363e0788 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/pipeline/PipPipeline.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/dataObject/pipeline/PipPipeline.java @@ -135,5 +135,8 @@ public class PipPipeline extends PipBaseElement { * 运行实例编号 */ private String instanceNum; - + /** + * 机器id + * */ + private String machineId; } diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/util/SftpUploadUtil.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/util/SftpUploadUtil.java new file mode 100644 index 00000000..81f787c9 --- /dev/null +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/util/SftpUploadUtil.java @@ -0,0 +1,266 @@ +package cd.casic.ci.process.util; + +import cd.casic.framework.commons.exception.ServiceException; +import com.jcraft.jsch.*; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +public class SftpUploadUtil { + + private static final int DEFAULT_SFTP_PORT = 22; + + /** + * 通过 SFTP 上传文件到远程服务器 + * + * @param remoteHost 远程服务器IP或主机名 + * @param remotePort 远程服务器端口 (通常是 22),为 null 或 <= 0 时使用默认端口 22 + * @param username 远程服务器用户名 + * @param password 远程服务器密码 (如果使用密码认证) + * @param sshKeyPath SSH Key 文件路径 (如果使用密钥认证,password 参数可以为 null) + * @param localFilePath 本地要上传的文件路径 + * @param remoteDir 远程服务器存放文件的目录 (例如: /home/user/uploads/) + * @param remoteFileName 上传到远程服务器的文件名 (为 null 或空字符串时使用本地文件名) + * @throws SftpUploadException 如果上传过程中发生任何错误 + */ + public static void uploadFileViaSftp( + String remoteHost, + Integer remotePort, + String username, + String password, + String sshKeyPath, + String localFilePath, + String remoteDir, + String remoteFileName) throws SftpUploadException { + + Session session = null; + Channel channel = null; + ChannelSftp channelSftp = null; + FileInputStream fis = null; + + try { + JSch jsch = new JSch(); + + // 1. 添加身份认证信息 (密码或密钥) + if (sshKeyPath != null && !sshKeyPath.trim().isEmpty()) { + // 使用 SSH Key 认证 + File sshKeyFile = new File(sshKeyPath); + if (!sshKeyFile.exists() || !sshKeyFile.isFile()) { + throw new SftpUploadException("SSH Key 文件不存在或不是一个有效文件: " + sshKeyPath); + } + jsch.addIdentity(sshKeyPath); + System.out.println("使用 SSH Key 认证: " + sshKeyPath); + } else if (password == null || password.trim().isEmpty()) { + // 如果没有提供密码或密钥路径,则认证信息不全 + throw new SftpUploadException("必须提供密码或 SSH Key 路径进行 SFTP 认证."); + } + // 如果提供了密码,将在 getSession 后设置,因为 getSession 需要用户名、主机和端口先建立连接意图 + + + // 2. 获取 Session + int port = (remotePort != null && remotePort > 0) ? remotePort : DEFAULT_SFTP_PORT; + session = jsch.getSession(username, remoteHost, port); + System.out.println("尝试连接 SFTP 服务器: " + username + "@" + remoteHost + ":" + port); + + + // 如果使用密码认证且提供了密码 + if (password != null && !password.trim().isEmpty() && (sshKeyPath == null || sshKeyPath.trim().isEmpty())) { + session.setPassword(password); + System.out.println("使用密码认证."); + } + + // 设置连接不进行主机密钥检查 (生产环境不推荐,应该配置 known_hosts) + // 在实际应用中,应该引导用户信任主机密钥或提前将主机密钥加入 known_hosts + java.util.Properties config = new java.util.Properties(); + config.put("StrictHostKeyChecking", "no"); // !!! 生产环境请谨慎使用或配置正确的主机密钥检查 !!! + session.setConfig(config); + + // 3. 连接 Session + session.connect(); + System.out.println("SFTP Session 连接成功."); + + // 4. 打开 SFTP Channel + channel = session.openChannel("sftp"); + channel.connect(); + System.out.println("SFTP Channel 打开成功."); + + channelSftp = (ChannelSftp) channel; + + // 5. 检查并切换到远程目录 + try { + channelSftp.cd(remoteDir); + System.out.println("已切换到远程目录: " + remoteDir); + } catch (SftpException e) { + // 如果远程目录不存在,尝试创建 + System.err.println("远程目录不存在: " + remoteDir + ",尝试创建..."); + try { + // 尝试递归创建目录 (如果需要) + createRemoteDirRecursive(channelSftp, remoteDir); + channelSftp.cd(remoteDir); // 创建后再次切换 + System.out.println("远程目录创建成功并已切换: " + remoteDir); + } catch (SftpException e2) { + // 创建目录失败 + throw new SftpUploadException("远程目录创建失败: " + remoteDir, e2); + } + } + + // 6. 获取本地文件流 + File localFile = new File(localFilePath); + if (!localFile.exists() || !localFile.isFile()) { + throw new SftpUploadException("本地文件不存在或不是一个有效文件: " + localFilePath); + } + fis = new FileInputStream(localFile); + System.out.println("本地文件流获取成功: " + localFilePath); + + // 7. 确定远程文件名 + String finalRemoteFileName = (remoteFileName != null && !remoteFileName.trim().isEmpty()) ? + remoteFileName : localFile.getName(); + System.out.println("最终上传到远程的文件名为: " + finalRemoteFileName); + + // 8. 上传文件 + channelSftp.put(fis, finalRemoteFileName); + System.out.println("文件上传成功!"); + + } catch (JSchException e) { + throw new SftpUploadException("SFTP 连接或认证失败: " + e.getMessage(), e); + } catch (SftpException e) { + throw new SftpUploadException("SFTP 操作失败 (如切换目录或上传文件): " + e.getMessage(), e); + } catch (FileNotFoundException e) { + throw new SftpUploadException("本地文件未找到: " + e.getMessage(), e); + } catch (IOException e) { + throw new SftpUploadException("文件流读写异常: " + e.getMessage(), e); + } catch (SftpUploadException e) { + // 重新抛出自定义异常 + throw e; + } catch (Exception e) { + // 捕获其他未知异常 + throw new SftpUploadException("SFTP 上传过程中发生未知异常: " + e.getMessage(), e); + } finally { + // 9. 关闭资源 (确保在任何情况下都关闭) + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + System.err.println("关闭本地文件流失败: " + e.getMessage()); + e.printStackTrace(); // 打印堆栈以便调试 + } + } + if (channelSftp != null) { + channelSftp.disconnect(); + System.out.println("SFTP Channel 已断开."); + } + if (channel != null) { + channel.disconnect(); + System.out.println("SFTP Channel 资源已释放."); + } + if (session != null) { + session.disconnect(); + System.out.println("SFTP Session 已断开."); + } + } + } + + /** + * 辅助方法:递归创建远程目录 + * @param channelSftp ChannelSftp 实例 + * @param remoteDir 要创建的目录路径 + * @throws SftpException 如果创建失败 + */ + private static void createRemoteDirRecursive(ChannelSftp channelSftp, String remoteDir) throws SftpException { + // 标准化路径,去掉末尾的 / + String cleanRemoteDir = remoteDir.endsWith("/") ? remoteDir.substring(0, remoteDir.length() - 1) : remoteDir; + + String[] pathElements = cleanRemoteDir.split("/"); + StringBuilder currentDir = new StringBuilder(); + try { + channelSftp.cd("/"); // 先回到根目录 + } catch (SftpException e) { + // 理论上不应该失败,除非根目录都不可访问 + throw new ServiceException(); + } + + for (String dir : pathElements) { + if (dir == null || dir.isEmpty()) { + continue; // 跳过空的路径元素,比如路径以/开头 + } + currentDir.append("/").append(dir); + try { + channelSftp.cd(currentDir.toString()); + // System.out.println("目录已存在: " + currentDir.toString()); + } catch (SftpException e) { + if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + // 目录不存在,创建它 + try { + System.out.println("创建目录: " + currentDir.toString()); + channelSftp.mkdir(currentDir.toString()); + channelSftp.cd(currentDir.toString()); // 创建后进入该目录 + System.out.println("目录创建成功并进入: " + currentDir.toString()); + } catch (SftpException e2) { + throw new SftpException(e2.id, "无法创建远程目录: " + currentDir.toString(), e2); + } + } else { + // 其他 SFTP 异常 + throw new SftpException(e.id, "切换或检查远程目录失败: " + currentDir.toString(), e); + } + } + } + } + + + // 自定义异常类,用于封装上传过程中的错误 + public static class SftpUploadException extends Exception { + public SftpUploadException(String message) { + super(message); + } + public SftpUploadException(String message, Throwable cause) { + super(message, cause); + } + } + + // 示例用法 + public static void main(String[] args) { + // 使用密码认证示例 + try { + System.out.println("--- 开始通过密码认证 SFTP 上传 ---"); + SftpUploadUtil.uploadFileViaSftp( + "your-remote-server-ip", // 替换为你的远程服务器IP或主机名 + 22, // SFTP端口 (如果不是22,请修改) + "your-username", // 替换为你的 SFTP 用户名 + "your-password", // 替换为你的 SFTP 密码 + null, // SSH Key 路径 (密码认证时为 null) + "path/to/your/local/file.txt", // 替换为你要上传的本地文件路径 + "/upload/target/directory/", // 替换为远程服务器上的目标目录 (确保用户有写入权限) + "uploaded_file.txt" // 上传到远程服务器的文件名 (null 或空则使用本地文件名) + ); + System.out.println("文件通过密码认证 SFTP 上传成功!"); + } catch (SftpUploadException e) { + System.err.println("文件通过密码认证 SFTP 上传失败: " + e.getMessage()); + e.printStackTrace(); + } + + System.out.println("\n------------------------------------\n"); + + // 使用 SSH Key 认证示例 (假设你有一个私钥文件) + // 请确保你的 SSH Key 不需要密码 (passphrase) + try { + System.out.println("--- 开始通过 SSH Key 认证 SFTP 上传 ---"); + SftpUploadUtil.uploadFileViaSftp( + "your-remote-server-ip", // 替换为你的远程服务器IP或主机名 + null, // SFTP端口 (使用默认端口 22) + "your-username", // 替换为你的 SFTP 用户名 + null, // 密码 (SSH Key 认证时为 null) + "/path/to/your/private/key",// 替换为你的本地私钥文件路径 (例如: /home/user/.ssh/id_rsa) + "path/to/another/local/file.log", // 替换为另一个你要上传的本地文件路径 + "/upload/other/files/", // 替换为另一个远程目录 + null // 远程文件名使用本地文件名 + ); + System.out.println("文件通过 SSH Key 认证 SFTP 上传成功!"); + } catch (SftpUploadException e) { + System.err.println("文件通过 SSH Key 认证 SFTP 上传失败: " + e.getMessage()); + e.printStackTrace(); + } + System.out.println("\n------------------------------------\n"); + } +} diff --git a/ops-server/src/test/java/cd/casic/server/SftpTest.java b/ops-server/src/test/java/cd/casic/server/SftpTest.java new file mode 100644 index 00000000..19d5d216 --- /dev/null +++ b/ops-server/src/test/java/cd/casic/server/SftpTest.java @@ -0,0 +1,30 @@ +package cd.casic.server; + +import cd.casic.ci.process.process.dataObject.machine.MachineInfo; +import cd.casic.ci.process.process.service.machine.MachineInfoService; +import cd.casic.ci.process.util.CryptogramUtil; +import cd.casic.ci.process.util.SftpUploadUtil; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.stereotype.Service; +import org.springframework.test.context.ActiveProfiles; + +import java.io.File; + +@SpringBootTest(classes = {OpsServerApplication.class}) +@ActiveProfiles("local") +public class SftpTest { + @Resource + MachineInfoService machineInfoService; + @Test + public void test01() throws SftpUploadUtil.SftpUploadException { + MachineInfo byId = machineInfoService.getById("1"); + File file = new File("src/test/java/cd/casic/server/text.txt"); + System.out.println(file.getAbsolutePath()); + System.out.println(file.exists()); + System.out.println(file.getName()); + SftpUploadUtil.uploadFileViaSftp(byId.getMachineHost(),byId.getSshPort(),byId.getUsername(), CryptogramUtil.doDecrypt(byId.getPassword()),null,file.getAbsolutePath(),"/home/casic/706/ai_test_527","到此一游.txt"); + } +}