0718 ljc 新增文件服务器
This commit is contained in:
parent
d6c5ab4d98
commit
7328373a05
@ -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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ChannelSftp.LsEntry> entries = channelSftp.ls(".");
|
||||
List<String> 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<ChannelSftp.LsEntry> entries = channelSftp.ls(".");
|
||||
List<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String> remoteFilePaths, String zipFileName, HttpServletResponse response);
|
||||
}
|
@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<TargetManagerDao, Targ
|
||||
private AdminUserServiceImpl adminUserService;
|
||||
@Resource
|
||||
private TargetFileUploadProperties fileUploadProperties;
|
||||
@Resource
|
||||
private PipelineDao pipelineDao;
|
||||
|
||||
|
||||
@Resource
|
||||
|
Loading…
x
Reference in New Issue
Block a user