SftpUtil(传输工具)
描述:在java项目中传输服务器数据到指定主机。
引入依赖
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
代码实现
/*
* Copyright (c) 2005, 2024, EVECOM Technology Co.,Ltd. All rights reserved.
* EVECOM PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package net.evecom.iaplatform.backup.util;
import cn.hutool.core.util.ObjectUtil;
import com.jcraft.jsch.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
* <B>Description: 备份工具</B>
* </P>
* Revision Trail: (Date/Author/Description)
* 2024/6/25 Ryan Huang CREATE
*
* @author Ryan Huang
* @version 1.0
*/
public class SftpUtil {
private static final Logger LOG = LoggerFactory.getLogger(SftpUtil.class);
private String username;
private String password;
private String host;
private Integer port;
private Session session;
private ChannelSftp sftpChannel;
public SftpUtil(String host, Integer port, String username, String password) {
this.host = host;
this.port = port;
this.username = username;
this.password = password;
init();
}
private void init() {
JSch jsch = new JSch();
try {
session = jsch.getSession(username, host, port);
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
session.connect();
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
LOG.info("SFTP连接成功");
} catch (Exception e) {
LOG.error("连接失败:{}", e.getMessage());
throw new RuntimeException("无法建立SFTP连接:" + e.getMessage(), e);
}
}
/**
* 文件传输
*
* @param sourcePath 源目录
* @param targetPath 目标目录
* @param closeSource 传输完成后是否关闭资源
*/
public void transfer(String sourcePath, String targetPath, boolean closeSource) {
transfer(sourcePath, targetPath, null, closeSource);
}
/**
* 文件传输
*
* @param sourcePath 源目录
* @param targetPath 目标目录
* @param ignoredPathList 忽略路径
* @param closeSource 传输完成后是否关闭资源
*/
public void transfer(String sourcePath, String targetPath, List<String> ignoredPathList, boolean closeSource) {
if(sourcePath == null || targetPath == null) {
throw new IllegalArgumentException("源路径和目标路径不能为空");
}
try {
uploadDirectory(sourcePath, targetPath, ignoredPathList);
LOG.info("文件传输成功");
} catch (Exception e) {
LOG.error("文件传输失败:{}", e.getMessage());
throw new RuntimeException("文件传输失败:" + e.getMessage(), e);
} finally {
if(closeSource) {
// 资源泄露防护
destroy();
}
}
}
/**
* 文件传输
*
* @param sourcePath 源目录
* @param targetPath 目标目录
* @param startTime 源目录文件开始时间
* @param endTime 源目录文件结束时间
* @param closeSource 传输完成后是否关闭资源
*/
public void transfer(String sourcePath, String targetPath, LocalDateTime startTime, LocalDateTime endTime, boolean closeSource) {
transfer(sourcePath, targetPath, startTime, endTime, null, closeSource);
}
public void transfer(String sourcePath, String targetPath, LocalDateTime startTime, LocalDateTime endTime, List<String> ignoredPathList, boolean closeSource) {
if(sourcePath == null || targetPath == null) {
throw new IllegalArgumentException("源路径和目标路径不能为空");
}
try {
uploadDirectory(sourcePath, targetPath, startTime, endTime, ignoredPathList);
LOG.info("文件传输成功, 传输文件时间段{}-{}", startTime, endTime);
} catch (Exception e) {
LOG.error("文件传输失败:{}", e.getMessage());
throw new RuntimeException("文件传输失败:" + e.getMessage(), e);
} finally {
if(closeSource) {
// 资源泄露防护
destroy();
}
}
}
/**
* 保存inputStream文件到远程
*
* @param inputStream 流
* @param remotePath 远程路径
* @param remoteFileName 文件名
* @param closeSource 是否关闭资源
*/
public void uploadInputStreamToRemote(InputStream inputStream, String remotePath, String remoteFileName, boolean closeSource) {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){
byte[] buffer = new byte[1024];
int read;
while ((read = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, read);
}
byte[] fileContent = byteArrayOutputStream.toByteArray();
String dirPath = handlePath(remotePath, "");
sftpChannel.cd(dirPath);
OutputStream outputStream = sftpChannel.put(remoteFileName);
outputStream.write(fileContent);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
LOG.error("文件传输失败:{}", e.getMessage());
throw new RuntimeException("文件传输失败:" + e.getMessage(), e);
}finally {
if(closeSource) {
// 资源泄露防护
destroy();
}
}
}
/**
* 销毁连接
*/
public void destroy() {
if (sftpChannel != null) {
try {
sftpChannel.disconnect();
} catch (Exception e) {
LOG.error("关闭SFTP通道失败:", e);
}
}
if (session != null) {
session.disconnect();
}
}
/**
* 上传目录
*
* @param localDirPath 本地目录
* @param remoteBasePath 远程目录
*/
private void uploadDirectory(String localDirPath, String remoteBasePath) {
uploadDirectory(localDirPath, remoteBasePath, null);
}
/**
* 上传目录
*
* @param localDirPath 本地目录
* @param remoteBasePath 远程目录
* @param ignoredFileList 忽略文件
*/
private void uploadDirectory(String localDirPath, String remoteBasePath, List<String> ignoredFileList) {
File localDir = new File(localDirPath);
if (!localDir.exists() || !localDir.isDirectory()) {
throw new IllegalArgumentException("Local path: " + localDirPath + " is not a valid directory.");
}
try {
String remotePath = handlePath(remoteBasePath, localDir.getName());
//目录不存在创建目录
createRemoteDirectoriesRecursively(remotePath);
sftpChannel.cd(remotePath);
File[] files = localDir.listFiles();
if (files != null) {
if(ObjectUtil.isNotEmpty(ignoredFileList)){
files = Arrays.stream(files).filter(file -> !ignoredFileList.contains(file.getAbsolutePath())).toArray(File[]::new);
}
for (File file : files) {
if (file.isDirectory()) {
uploadDirectory(file.getAbsolutePath(), remotePath, ignoredFileList);
} else {
String remoteFilePath = handlePath(remotePath, file.getName());
sftpChannel.put(file.getAbsolutePath(), remoteFilePath);
}
}
}
} catch (SftpException e) {
throw new RuntimeException("Error during SFTP operation: " + e.getMessage(), e);
}
}
/**
* 上传目录
*
* @param localDirPath 本地目录
* @param remoteBasePath 远程目录
* @param startTime 文件开始时间
* @param endTime 文件结束时间
*/
private void uploadDirectory(String localDirPath, String remoteBasePath, LocalDateTime startTime, LocalDateTime endTime) {
uploadDirectory(localDirPath, remoteBasePath, startTime, endTime, null);
}
/**
* 上传目录
*
* @param localDirPath 本地目录
* @param remoteBasePath 远程目录
* @param startTime 文件开始时间
* @param endTime 文件结束时间
* @param ignoredPathList 忽略文件
*/
private void uploadDirectory(String localDirPath, String remoteBasePath, LocalDateTime startTime, LocalDateTime endTime, List<String> ignoredPathList) {
File localDir = new File(localDirPath);
if (!localDir.exists() || !localDir.isDirectory()) {
throw new IllegalArgumentException("Local path: " + localDirPath + " is not a valid directory.");
}
try {
String remotePath = handlePath(remoteBasePath, localDir.getName());
//目录不存在创建目录
createRemoteDirectoriesRecursively(remotePath);
sftpChannel.cd(remotePath);
File[] files = localDir.listFiles((dir1, name) -> {
File file = new File(dir1, name);
ZoneId zoneId = ZoneId.systemDefault();
if (startTime != null && endTime != null) {
return file.lastModified() >= startTime.atZone(zoneId).toInstant().toEpochMilli()
&& file.lastModified() <= endTime.atZone(zoneId).toInstant().toEpochMilli();
}else if (startTime != null) {
return file.lastModified() >= startTime.atZone(zoneId).toInstant().toEpochMilli();
}
return file.lastModified() <= endTime.atZone(zoneId).toInstant().toEpochMilli();
});
if (files != null) {
if(ObjectUtil.isNotEmpty(ignoredPathList)){
files = Arrays.stream(files).filter(file -> !ignoredPathList.contains(file.getAbsolutePath())).toArray(File[]::new);
}
for (File file : files) {
if (file.isDirectory()) {
uploadDirectory(file.getAbsolutePath(), remotePath, startTime, endTime, ignoredPathList);
} else {
String remoteFilePath = handlePath(remotePath, file.getName());
sftpChannel.put(file.getAbsolutePath(), remoteFilePath);
}
}
}
} catch (SftpException e) {
throw new RuntimeException("Error during SFTP operation: " + e.getMessage(), e);
}
}
/**
* 上传文件
*
* @param localDirPath 本地路径
* @param remoteBasePath 远程路径
*/
public void uploadFile(String localDirPath, String remoteBasePath) {
File file = new File(localDirPath);
if (!file.exists() || !file.isFile()) {
throw new IllegalArgumentException("Local path: " + localDirPath + " is not a valid file.");
}
try{
String remotePath = handlePath(remoteBasePath, "");
//目录不存在创建目录
createRemoteDirectoriesRecursively(remotePath);
sftpChannel.cd(remotePath);
String remoteFilePath = handlePath(remoteBasePath, file.getName());
sftpChannel.put(localDirPath, remoteFilePath);
} catch (SftpException e) {
throw new RuntimeException("Error during SFTP operation: " + e.getMessage(), e);
}
}
/**
* 执行远程windows主机命令
*
* @param cmd 命令
* @throws JSchException 异常
* @throws IOException 异常
*/
public int executeCmdForWindow(String cmd) throws JSchException, IOException {
Channel channel = session.openChannel("exec");
LOG.info("执行命令:" + cmd);
((ChannelExec) channel).setCommand("cmd.exe /c " + cmd);
// 获取输出流和错误流以便读取命令执行结果
InputStream in = channel.getInputStream();
InputStream err = ((ChannelExec) channel).getErrStream();
channel.connect();
StringBuilder outputSb = new StringBuilder();
byte[] buffer = new byte[1024];
int status = 0;
while (true) {
while (in.available() > 0) {
int i = in.read(buffer, 0, 1024);
if (i < 0)
break;
outputSb.append(new String(buffer, 0, i));
}
while (err.available() > 0) {
int i = err.read(buffer, 0, 1024);
if (i < 0)
break;
outputSb.append(new String(buffer, 0, i));
}
if (channel.isClosed()) {
if (in.available() > 0) continue;
status = channel.getExitStatus();
break;
}
}
LOG.info("执行结果:" + outputSb);
channel.disconnect();
return status;
}
/**
* 执行远程linux主机命令
*
* @param cmd 命令
* @throws JSchException 异常
* @throws IOException 异常
*/
public int executeCmdForLinux(String cmd) throws JSchException, IOException {
Channel channel = session.openChannel("shell");
// 获取输入流和输出流以交互
OutputStream input = channel.getOutputStream();
PrintStream ps = new PrintStream(input, true);
channel.connect();
LOG.info("执行命令:" + cmd);
ps.println(cmd + "; echo $?"); //添加 "; echo $?" 来获取命令的退出状态码
BufferedReader reader = new BufferedReader(new InputStreamReader(channel.getInputStream()));
String line;
StringBuilder outputBuilder = new StringBuilder();
while ((line = reader.readLine()) != null) {
outputBuilder.append(line).append("\n");
// 当读取到退出状态码时,跳出循环
if (line.trim().matches("\\d+")) {
break;
}
}
String[] outArray = outputBuilder.toString().trim().split("\n");
int exitStatus = Integer.parseInt(outArray[outArray.length - 1]);
// 判断命令是否成功执行
if (exitStatus == 0) {
LOG.info("命令执行成功");
} else {
LOG.error("命令执行失败,退出状态码: " + outputBuilder.toString());
}
reader.close();
ps.close();
channel.disconnect();
return exitStatus;
}
/**
* 判断远程目录是否存在
*
* @param remotePath 远程目录
* @return true:存在 false:不存在
*/
public boolean remoteDirectoryExists(String remotePath) {
try {
String handleFilePath = handlePath(remotePath, "");
SftpATTRS attrs = sftpChannel.lstat(handleFilePath);
return attrs != null && attrs.isDir();
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
return false;
} else {
throw new RuntimeException("Error checking remote directory existence: " + e.getMessage(), e);
}
}
}
/**
* 判断远程文件是否存在
*
* @param filePath 远程文件
* @return true:存在 false:不存在
*/
public boolean remoteFileExists(String filePath) {
try {
String handleFilePath = handlePath(filePath, "");
if (handleFilePath.endsWith("/")) {
handleFilePath = handleFilePath.substring(0, handleFilePath.length() - 1);
}
SftpATTRS attrs = sftpChannel.stat(handleFilePath);
return attrs != null && !attrs.isDir();
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
return false;
} else {
throw new RuntimeException("Error checking remote File existence: " + e.getMessage(), e);
}
}
}
/**
* 判断远程路径是否为目录
*
* @param path 路径
*/
public boolean remotePathIsDir(String path){
try {
String handleFilePath = handlePath(path, "");
if (handleFilePath.endsWith("/")) {
handleFilePath = handleFilePath.substring(0, handleFilePath.length() - 1);
}
SftpATTRS attrs = sftpChannel.stat(handleFilePath);
if (attrs != null) {
if (attrs.isDir()) {
return true;
}
}
} catch (SftpException e) {
throw new RuntimeException("Error checking remote path existence: " + e.getMessage(), e);
}
return false;
}
/**
* 路径处理
*
* @param path 路径
* @param fileName 文件名
*/
public static String handlePath(String path, String fileName) {
String str;
str = path.replace("\\", "/");
if (str.endsWith("/")) {
str = str + fileName;
} else {
str = str + "/" + fileName;
}
if (path.charAt(0) != '/') {
str = "/" + str;
}
return str;
}
/**
* 递归创建目录
*
* @param remotePath 目录
* @throws SftpException 异常
*/
private void createRemoteDirectoriesRecursively(String remotePath) throws SftpException {
// 分割路径以获取各级目录名
String[] directories = remotePath.split("/");
// 过滤掉空字符串,因为split处理绝对路径时会包含一个空元素
List<String> validDirectories = Arrays.stream(directories)
.filter(dir -> !dir.isEmpty())
.collect(Collectors.toList());
// 用于构建当前路径的StringBuilder
StringBuilder currentPath = new StringBuilder();
//在开头添加/代表从跟节点开始创建目录
currentPath.append("/");
// 遍历所有层级的目录名
for (String dir : validDirectories) {
// 添加目录名到当前路径,并以"/"分隔
currentPath.append(dir).append("/");
//:则代表磁盘路径,跳过
if(dir.contains(":")) continue;
// 构建完整的路径并检查是否存在
String fullPath = currentPath.toString();
if (!remoteDirectoryExists(fullPath)) {
// 如果不存在,则创建该层级的目录
sftpChannel.mkdir(fullPath);
}
}
}
}