diff --git a/modules/module-ci-process-api/src/main/java/cd/casic/ci/api/SftpFileController.java b/modules/module-ci-process-api/src/main/java/cd/casic/ci/api/SftpFileController.java new file mode 100644 index 00000000..c6b86937 --- /dev/null +++ b/modules/module-ci-process-api/src/main/java/cd/casic/ci/api/SftpFileController.java @@ -0,0 +1,88 @@ +package cd.casic.ci.api; + +import cd.casic.ci.process.process.service.sftpFile.SftpFileService; +import cd.casic.ci.process.properties.TargetFileUploadProperties; +import cd.casic.framework.commons.pojo.CommonResult; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * @author HopeLi + * @version v1.0 + * @ClassName SftpFileController + * @Date: 2025/7/1 9:03 + * @Description: + */ +@RestController +@RequestMapping("/sftpFile") +public class SftpFileController { + + @Resource + private SftpFileService sftpFileService; + + @Resource + private TargetFileUploadProperties fileUploadProperties; + + @PostMapping("/upload") + public CommonResult uploadFile( + @RequestParam String remoteDir, + @RequestParam String remoteFileName, + @RequestParam MultipartFile file) { + + String localFilePath = saveTempFile(file); + sftpFileService.uploadFile( + fileUploadProperties.getRemoteHost(), + fileUploadProperties.getRemotePort(), + fileUploadProperties.getUsername(), + fileUploadProperties.getPassword(), + fileUploadProperties.getSshKeyPath(), + localFilePath, remoteDir, remoteFileName); + + return CommonResult.success("文件上传成功"); + } + + @GetMapping("/download") + public void downloadFile( + @RequestParam String remoteFilePath, + HttpServletResponse response) { + + sftpFileService.downloadFile( + fileUploadProperties.getRemoteHost(), + fileUploadProperties.getRemotePort(), + fileUploadProperties.getUsername(), + fileUploadProperties.getPassword(), + fileUploadProperties.getSshKeyPath(), + remoteFilePath, response); + } + + @PostMapping("/download/zip") + public void downloadFilesAsZip( + @RequestBody List remoteFilePaths, + @RequestParam String zipFileName, + HttpServletResponse response) { + + sftpFileService.downloadFilesAsZip( + fileUploadProperties.getRemoteHost(), + fileUploadProperties.getRemotePort(), + fileUploadProperties.getUsername(), + fileUploadProperties.getPassword(), + fileUploadProperties.getSshKeyPath(), + remoteFilePaths, zipFileName, response); + } + + private String saveTempFile(MultipartFile file) { + try { + File tempFile = File.createTempFile("upload-", ".tmp"); + file.transferTo(tempFile); + return tempFile.getAbsolutePath(); + } catch (IOException e) { + throw new RuntimeException("保存临时文件失败", e); + } + } +} diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/SftpClientUtils.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/SftpClientUtils.java new file mode 100644 index 00000000..d8e0ca3c --- /dev/null +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/SftpClientUtils.java @@ -0,0 +1,264 @@ +package cd.casic.ci.process.process.service.sftpFile; + +import cd.casic.ci.process.util.SftpUploadUtil; +import cd.casic.framework.commons.exception.ServiceException; +import com.amazonaws.util.IOUtils; +import com.jcraft.jsch.*; + +import java.io.*; +import java.util.List; +import java.util.Vector; + +/** + * @author HopeLi + * @version v1.0 + * @ClassName SftpClientUtils + * @Date: 2025/7/22 15:09 + * @Description: + */ +public class SftpClientUtils implements AutoCloseable { + private static final int DEFAULT_SFTP_PORT = 22; + + private Session session; + private ChannelSftp channelSftp; + + public SftpClientUtils(String remoteHost, Integer remotePort, String username, + String password, String sshKeyPath) throws JSchException, SftpException, SftpUploadUtil.SftpUploadException { + 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 SftpUploadUtil.SftpUploadException("SSH Key 文件不存在或不是一个有效文件: " + sshKeyPath); + } + jsch.addIdentity(sshKeyPath); + System.out.println("使用 SSH Key 认证: " + sshKeyPath); + } else if (password == null || password.trim().isEmpty()) { + // 如果没有提供密码或密钥路径,则认证信息不全 + throw new SftpUploadUtil.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 channel = session.openChannel("sftp"); + channel.connect(); + System.out.println("SFTP Channel 打开成功."); + + channelSftp = (ChannelSftp) channel; + } + + public void uploadFile(String localFilePath, String remoteDir, String remoteFileName) throws SftpException, SftpUploadUtil.SftpUploadException, FileNotFoundException { + FileInputStream fis = null; + + // 检查并切换到远程目录 + 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 SftpUploadUtil.SftpUploadException("远程目录创建失败: " + remoteDir, e2); + } + } + + // 6. 获取本地文件流 + File localFile = new File(localFilePath); + if (!localFile.exists() || !localFile.isFile()) { + throw new SftpUploadUtil.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("文件上传成功!"); + } + + public void downloadFile(String remoteFilePath, OutputStream outputStream) throws SftpException, SftpUploadUtil.SftpUploadException, IOException { + InputStream inputStream = null; + + String remoteDir = remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/')); + String fileName = remoteFilePath.substring(remoteFilePath.lastIndexOf('/') + 1); + + // 切换目录并列出内容用于调试 + try { + channelSftp.cd(remoteDir); + } catch (SftpException e) { + throw new SftpUploadUtil.SftpUploadException("切换远程目录失败: " + remoteDir, e); + } + + // 列出目录内容用于调试 + Vector entries = channelSftp.ls("."); + List fileNames = entries.stream() + .map(entry -> entry.getFilename()) + .filter(name -> !name.equals(".") && !name.equals("..")) + .toList(); + + System.out.println("远程目录中的文件列表: " + fileNames); + + // 尝试获取文件流 + try { + inputStream = channelSftp.get(fileName); + + if (inputStream == null) { + throw new SftpUploadUtil.SftpUploadException("无法获取远程文件输入流,请检查文件是否存在或被其他进程占用: " + fileName); + } + + IOUtils.copy(inputStream,outputStream); + outputStream.flush(); + } catch (SftpException e) { + throw new SftpUploadUtil.SftpUploadException("文件下载失败: " + remoteFilePath, e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + System.err.println("关闭 InputStream 时发生异常: " + e.getMessage()); + } + } + } + + + + + } + + public InputStream downloadFileToStream(String remoteFilePath) throws SftpException, SftpUploadUtil.SftpUploadException { + InputStream inputStream = null; + + String remoteDir = remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/')); + String fileName = remoteFilePath.substring(remoteFilePath.lastIndexOf('/') + 1); + + // 切换目录并列出内容用于调试 + try { + channelSftp.cd(remoteDir); + } catch (SftpException e) { + throw new SftpUploadUtil.SftpUploadException("切换远程目录失败: " + remoteDir, e); + } + + // 列出目录内容用于调试 + Vector entries = channelSftp.ls("."); + List fileNames = entries.stream() + .map(entry -> entry.getFilename()) + .filter(name -> !name.equals(".") && !name.equals("..")) + .toList(); + + System.out.println("远程目录中的文件列表: " + fileNames); + + // 尝试获取文件流 + try { + inputStream = channelSftp.get(fileName); + + if (inputStream == null) { + throw new SftpUploadUtil.SftpUploadException("无法获取远程文件输入流,请检查文件是否存在或被其他进程占用: " + fileName); + } + + return inputStream; + } catch (SftpException e) { + throw new SftpUploadUtil.SftpUploadException("获取远程文件流失败: " + fileName, e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + System.err.println("关闭 InputStream 时发生异常: " + e.getMessage()); + } + } + } + + + } + + + /** + * 辅助方法:递归创建远程目录 + * @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()); + } 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); + } + } + } + } + + @Override + public void close() { + if (channelSftp != null) { + channelSftp.disconnect(); + } + if (session != null) { + session.disconnect(); + } + } +} diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/SftpFileService.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/SftpFileService.java new file mode 100644 index 00000000..876f5e94 --- /dev/null +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/SftpFileService.java @@ -0,0 +1,22 @@ +package cd.casic.ci.process.process.service.sftpFile; + + +import jakarta.servlet.http.HttpServletResponse; + +import java.util.List; + +/** + * @author HopeLi + * @version v1.0 + * @ClassName TargetManagerService + * @Date: 2025/5/17 10:20 + * @Description: + */ +public interface SftpFileService{ + + void uploadFile(String remoteHost, Integer remotePort, String username, String password, String sshKeyPath, String localFilePath, String remoteDir, String remoteFileName); + + void downloadFile(String remoteHost, Integer remotePort, String username, String password, String sshKeyPath, String remoteFilePath, HttpServletResponse response); + + void downloadFilesAsZip(String remoteHost, Integer remotePort, String username, String password, String sshKeyPath, List remoteFilePaths, String zipFileName, HttpServletResponse response); +} diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/impl/SftpFileServiceImpl.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/impl/SftpFileServiceImpl.java new file mode 100644 index 00000000..60206644 --- /dev/null +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/sftpFile/impl/SftpFileServiceImpl.java @@ -0,0 +1,76 @@ +package cd.casic.ci.process.process.service.sftpFile.impl; + + +import cd.casic.ci.process.process.service.sftpFile.SftpClientUtils; +import cd.casic.ci.process.process.service.sftpFile.SftpFileService; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @author HopeLi + * @version v1.0 + * @ClassName SftpFileServiceImpl + * @Date: 2025/5/17 15:19 + * @Description: + */ +@Service +@Slf4j +public class SftpFileServiceImpl implements SftpFileService { + + + @Override + public void uploadFile(String remoteHost, Integer remotePort, String username, String password, String sshKeyPath, String localFilePath, String remoteDir, String remoteFileName) { + try (SftpClientUtils client = new SftpClientUtils(remoteHost, remotePort, username, password, sshKeyPath)) { + client.uploadFile(localFilePath, remoteDir, remoteFileName); + } catch (Exception e) { + throw new RuntimeException("文件上传失败: " + e.getMessage(), e); + } + } + + @Override + public void downloadFile(String remoteHost, Integer remotePort, String username, String password, String sshKeyPath, String remoteFilePath, HttpServletResponse response) { + try (SftpClientUtils client = new SftpClientUtils(remoteHost, remotePort, username, password, sshKeyPath)) { + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + new File(remoteFilePath).getName() + "\""); + + try (OutputStream out = response.getOutputStream()) { + client.downloadFile(remoteFilePath, out); + } + } catch (Exception e) { + throw new RuntimeException("文件下载失败: " + e.getMessage(), e); + } + } + + @Override + public void downloadFilesAsZip(String remoteHost, Integer remotePort, String username, String password, String sshKeyPath, List remoteFilePaths, String zipFileName, HttpServletResponse response) { + try (SftpClientUtils client = new SftpClientUtils(remoteHost, remotePort, username, password, sshKeyPath)) { + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + "\""); + + try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) { + for (String remoteFilePath : remoteFilePaths) { + String fileName = remoteFilePath.substring(remoteFilePath.lastIndexOf("/") + 1); + try (InputStream in = client.downloadFileToStream(remoteFilePath)) { + zipOut.putNextEntry(new ZipEntry(fileName)); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) > 0) { + zipOut.write(buffer, 0, bytesRead); + } + zipOut.closeEntry(); + } + } + } + } catch (Exception e) { + throw new RuntimeException("批量下载并打包 ZIP 失败: " + e.getMessage(), e); + } + } +} diff --git a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/target/impl/TargetManagerServiceImpl.java b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/target/impl/TargetManagerServiceImpl.java index f4541c2e..f9390093 100644 --- a/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/target/impl/TargetManagerServiceImpl.java +++ b/modules/module-ci-process-biz/src/main/java/cd/casic/ci/process/process/service/target/impl/TargetManagerServiceImpl.java @@ -6,11 +6,9 @@ import cd.casic.ci.process.dto.resp.target.TargetManagerResp; import cd.casic.ci.process.dto.resp.target.TargetVersionResp; import cd.casic.ci.process.process.converter.TargetConverter; import cd.casic.ci.process.process.converter.TargetVersionConverter; -import cd.casic.ci.process.process.dao.pipeline.PipelineDao; import cd.casic.ci.process.process.dao.pipeline.TargetManagerDao; import cd.casic.ci.process.process.dao.pipeline.TargetVersionDao; import cd.casic.ci.process.process.dataObject.base.BaseIdReq; -import cd.casic.ci.process.process.dataObject.pipeline.PipPipeline; import cd.casic.ci.process.process.dataObject.target.TargetManager; import cd.casic.ci.process.process.dataObject.target.TargetVersion; import cd.casic.ci.process.process.service.pipeline.PipelineService; @@ -67,8 +65,6 @@ public class TargetManagerServiceImpl extends ServiceImpl