1、引入jsch和common-pool2包
以下为gradle的引入方式,使用maven的可切换为maven的xml引入方式。
implementation group: 'com.jcraft', name: 'jsch', version: "0.1.55"
implementation group: 'org.apache.commons', name: 'commons-pool2', version: "2.11.1"
2、创建SFTP操作对象,内部包含了各种操作方法
package cn.ac.iscas.dmo.db.sftp;
import cn.ac.iscas.dmo.db.api.util.StringUtils;
import cn.ac.iscas.dmo.db.filesystem.base.util.FsCommonUtils;
import cn.ac.iscas.dmo.db.sftp.pool.PoolProp;
import com.jcraft.jsch.*;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Vector;
/**
* @author zhuquanwen
* @version 1.0
* @date 2022/9/8 9:43
* @since jdk11
*/
public class Sftp {
private ChannelSftp channelSftp;
private Session session;
private String oriPath;
public Sftp(PoolProp poolProp) {
try {
JSch jsch = new JSch();
session = jsch.getSession(poolProp.getUsername(), poolProp.getHost(), poolProp.getPort());
session.setPassword(poolProp.getPassword());
session.setConfig("StrictHostKeyChecking", "no");
session.connect(poolProp.getSessionConnectTimeout());
Channel channel = session.openChannel("sftp");
channel.connect(poolProp.getChannelConnectedTimeout());
channelSftp = (ChannelSftp) channel;
oriPath = channelSftp.pwd();
} catch (Exception e) {
e.printStackTrace();
close();
}
}
/**断开连接*/
public void close() {
if (channelSftp != null) {
try {
channelSftp.disconnect();
} catch (Exception ignored) {
}
}
if (session != null) {
try {
session.disconnect();
} catch (Exception ignored) {
}
}
}
/**验证连接*/
public boolean validate() {
try {
return session.isConnected() && channelSftp.isConnected() /*&&
!Objects.isNull(oriPath) && oriPath.equals(channelSftp.pwd())*/;
} catch (Exception e) {
return false;
}
}
/**
* 获取当前工作目录
* */
public String pwd() throws SftpException {
return channelSftp.pwd();
}
/**获取当前文件的状态*/
public SftpATTRS stat(String path) throws SftpException {
return channelSftp.stat(path);
}
/**获取当前文件是否是文件夹*/
public boolean isDir(String path) throws SftpException {
return channelSftp.stat(path).isDir();
}
/**ls*/
public Vector ls(String path) throws SftpException {
return channelSftp.ls(path);
}
/**mkdir*/
public void mkdir(String path) throws SftpException {
channelSftp.mkdir(path);
}
/**mkdirs*/
public void mkdirs(String path) throws SftpException {
String[] paths = path.split("/");
for (int i = 0; i < paths.length; i++) {
StringBuilder pPath = new StringBuilder();
for (int j = 0; j <= i; j++) {
pPath.append("/").append(paths[j]);
}
String p = pPath.toString();
p = p.replaceAll("//+", "/");
if (exist(p)) {
if (!isDir(p)) {
throw new RuntimeException(String.format("路径:[%s]不是路径", p));
}
} else {
if (!"/".equals(p)) {
channelSftp.mkdir(p);
}
}
}
}
/**touch*/
public void touch(String path) throws SftpException {
// todo 未找到类似touch的方式,暂时以上传一个空文件的方式实现
String parentPath = path.substring(0, path.lastIndexOf("/"));
if (StringUtils.isEmpty(parentPath)) {
parentPath = "/";
}
String fileName = path.substring(path.lastIndexOf("/") + 1);
//如果不存在,创建父目录
mkdirs(parentPath);
File tmpFile = null;
try {
Path tempFile = Files.createTempFile("tmp", "tmp");
tmpFile = tempFile.toFile();
try (FileInputStream fis = new FileInputStream(tmpFile)) {
channelSftp.cd(parentPath);
channelSftp.put(fis, fileName);
}
} catch (SftpException e1) {
throw e1;
} catch (Exception e) {
throw new RuntimeException("创建文件出错", e);
} finally {
if (tmpFile != null) {
tmpFile.delete();
}
}
}
/**
* cp
* 此实现需要该登录用户已被授权ssh功能
* */
public void cp(String src, String dest) {
ChannelExec channelExec = null;
StringBuilder runLog = new StringBuilder();
StringBuilder errLog = new StringBuilder();
try {
String command = "cp -r " + src + " " + dest;
Channel channel = session.openChannel("exec");
channelExec = (ChannelExec) channel;
channelExec.setCommand(command);
channelExec.connect();
// 获取标准输入流
BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getInputStream()));
// 获取标准错误输入流
BufferedReader errInputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getErrStream()));
// 记录命令执行 log
String line;
while ((line = inputStreamReader.readLine()) != null) {
runLog.append(line).append("\n");
}
//记录命令执行错误 log
String errLine;
while ((errLine = errInputStreamReader.readLine()) != null) {
errLog.append(errLine).append("\n");
}
// 输出 shell 命令执行日志
System.out.println("exitStatus=" + channelExec.getExitStatus() + ", openChannel.isClosed="
+ channelExec.isClosed());
System.out.println("命令执行完成,执行日志如下:");
System.out.println(runLog);
System.out.println("命令执行完成,执行错误日志如下:");
System.out.println(errLog);
if (errLog.length() > 0) {
throw new RuntimeException(errLog.toString());
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("复制文件出错", e);
} finally {
if (channelExec != null) {
channelExec.disconnect();
}
}
}
/**
* mv
* 此实现需要该登录用户已被授权ssh功能
* */
public void mv(String src, String dest) {
ChannelExec channelExec = null;
StringBuilder runLog = new StringBuilder();
StringBuilder errLog = new StringBuilder();
try {
String command = "mv " + src + " " + dest;
Channel channel = session.openChannel("exec");
channelExec = (ChannelExec) channel;
channelExec.setCommand(command);
channelExec.connect();
// 获取标准输入流
BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getInputStream()));
// 获取标准错误输入流
BufferedReader errInputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getErrStream()));
// 记录命令执行 log
String line;
while ((line = inputStreamReader.readLine()) != null) {
runLog.append(line).append("\n");
}
//记录命令执行错误 log
String errLine;
while ((errLine = errInputStreamReader.readLine()) != null) {
errLog.append(errLine).append("\n");
}
// 输出 shell 命令执行日志
System.out.println("exitStatus=" + channelExec.getExitStatus() + ", openChannel.isClosed="
+ channelExec.isClosed());
System.out.println("命令执行完成,执行日志如下:");
System.out.println(runLog);
System.out.println("命令执行完成,执行错误日志如下:");
System.out.println(errLog);
if (errLog.length() > 0) {
throw new RuntimeException(errLog.toString());
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("移动文件出错", e);
} finally {
if (channelExec != null) {
channelExec.disconnect();
}
}
}
/**
* 删除文件
* */
@SuppressWarnings("unchecked")
public void rm(String realPath, boolean r) throws SftpException {
if (isDir(realPath)) {
// 文件夹,递归
Vector vector = ls(realPath);
if (vector.size() <= 0) {
// 没有子文件或文件夹,直接删除
channelSftp.rmdir(realPath);
} else {
// 如果有子文件或文件夹,通过r参数判定是否能删除
if (!r) {
throw new RuntimeException(String.format("路径:[%s]中存在子文件或文件夹,无法删除", realPath));
}
// 递归
Iterator<ChannelSftp.LsEntry> iterator = vector.iterator();
while (iterator.hasNext()) {
ChannelSftp.LsEntry lsEntry = iterator.next();
String filename = lsEntry.getFilename();
if (!".".equals(filename) && !"..".equals(filename)) {
rm(FsCommonUtils.optimizationPath(realPath + "/" + filename), r);
}
}
channelSftp.rmdir(realPath);
}
} else {
// 非文件夹直接删除
channelSftp.rm(realPath);
}
}
/**
* 查看当前路径或文件否存在
* */
public boolean exist(String path) throws SftpException {
boolean flag = true;
if ("/".equals(path)) {
return true;
}
try {
channelSftp.stat(path);
} catch (SftpException e) {
if (e.getMessage() != null && e.getMessage().contains("No such file")) {
flag = false;
} else {
throw e;
}
}
return flag;
}
/**
* 上传文件
* */
public void uploadFile(String realPath, InputStream is, boolean append) throws SftpException {
channelSftp.put(is, realPath, append ? ChannelSftp.APPEND : ChannelSftp.OVERWRITE);
}
/**
* 下载文件
* */
public void downloadFile(String path, OutputStream os) throws SftpException {
channelSftp.get(path, os);
}
public ChannelSftp getChannelSftp() {
return channelSftp;
}
}
3、连接池属性对象
package cn.ac.iscas.dmo.db.sftp.pool;
import lombok.Data;
/**
* @author zhuquanwen
* @version 1.0
* @date 2022/9/8 9:57
* @since jdk11
*/
@Data
public class PoolProp {
/**
* 池中最小的连接数,只有当 timeBetweenEvictionRuns 为正时才有效
*/
private int minIdle = 0;
/**
* 池中最大的空闲连接数,为负值时表示无限
*/
private int maxIdle = 8;
/**
* 池可以产生的最大对象数,为负值时表示无限
*/
private int maxActive = 16;
/**
* 当池耗尽时,阻塞的最长时间,为负值时无限等待
*/
private long maxWait = -1;
/**
* 从池中取出对象是是否检测可用
*/
private boolean testOnBorrow = true;
/**
* 将对象返还给池时检测是否可用
*/
private boolean testOnReturn = false;
/**
* 检查连接池对象是否可用
*/
private boolean testWhileIdle = true;
/**
* 距离上次空闲线程检测完成多久后再次执行
*/
private long timeBetweenEvictionRuns = 300000L;
/**
* 用户名
*/
private String username;
/**
* 主机
*/
private String host;
/**
* 端口
*/
private int port;
/**
* 密码
*/
private String password;
/**
* session连接超时时间
*/
private int sessionConnectTimeout = 2000;
/**
* sftp-channel连接超时时间
*/
private int channelConnectedTimeout = 2000;
}
4、连接池
package cn.ac.iscas.dmo.db.sftp.pool;
import cn.ac.iscas.dmo.db.sftp.Sftp;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.time.Duration;
/**
* sftp连接池
*
* @author zhuquanwen
* @version 1.0
* @date 2022/9/8 9:59
* @since jdk11
*/
public class SftpPool implements ObjectPool<Sftp> {
private final GenericObjectPool<Sftp> pool;
public SftpPool(PoolProp poolProp) {
pool = new GenericObjectPool<>(new SftpFactory(poolProp), getConfig(poolProp));
}
@Override
public void addObject() throws Exception {
pool.addObject();
}
@Override
public Sftp borrowObject() throws Exception {
return pool.borrowObject();
}
@Override
public void clear() {
pool.clear();
}
@Override
public void close() {
pool.close();
}
@Override
public int getNumActive() {
return pool.getNumActive();
}
@Override
public int getNumIdle() {
return pool.getNumIdle();
}
@Override
public void invalidateObject(Sftp obj) throws Exception {
pool.invalidateObject(obj);
}
@Override
public void returnObject(Sftp obj) {
pool.returnObject(obj);
}
private static class SftpFactory extends BasePooledObjectFactory<Sftp> {
private final PoolProp poolProp;
public SftpFactory(PoolProp poolProp) {
this.poolProp = poolProp;
}
@Override
public Sftp create() {
return new Sftp(poolProp);
}
@Override
public PooledObject<Sftp> wrap(Sftp sftp) {
return new DefaultPooledObject<>(sftp);
}
@Override
public boolean validateObject(PooledObject<Sftp> p) {
return p.getObject().validate();
}
@Override
public void destroyObject(PooledObject<Sftp> p) {
p.getObject().close();
}
}
private GenericObjectPoolConfig<Sftp> getConfig(PoolProp poolProp) {
if (poolProp == null) {
poolProp = new PoolProp();
}
GenericObjectPoolConfig<Sftp> config = new GenericObjectPoolConfig<>();
config.setMinIdle(poolProp.getMinIdle());
config.setMaxIdle(poolProp.getMaxIdle());
config.setMaxTotal(poolProp.getMaxActive());
config.setMaxWait(Duration.ofMillis(poolProp.getMaxWait()));
config.setTestOnBorrow(poolProp.isTestOnBorrow());
config.setTestOnReturn(poolProp.isTestOnReturn());
config.setTestWhileIdle(poolProp.isTestWhileIdle());
config.setTimeBetweenEvictionRuns(Duration.ofMillis(poolProp.getTimeBetweenEvictionRuns()));
return config;
}
}
5、使用连接池
PoolProp poolProp = new PoolProp();
//todo 连接池信息先用默认的,这里只设置用户名、密码等信息
poolProp.setUsername("root");
poolProp.setHost("125.2.2.2");
poolProp.setPort(22);
poolProp.setPassword("***");
SftpPool sftpPool = new SftpPool(poolProp);
Sftp sftp = null;
try {
sftp = sftpPool.borrowObject();
boolean res = sftp.exist("/root");
} finally {
sftpPool.returnObject(sftp);
}
6、附几个工具类
package cn.ac.iscas.dmo.db.api.util;
import java.util.Objects;
/**
* @author zhuquanwen
* @version 1.0
* @date 2022/3/8 8:52
* @since jdk11
*/
public class StringUtils {
private StringUtils() {
}
public static boolean isEmpty(Object str) {
return (str == null || "".equals(str));
}
public static boolean isNotEmpty(Object str) {
return !isEmpty(str);
}
public static String substringAfter(final String str, final String separator) {
if (isEmpty(str)) {
return str;
}
if (separator == null) {
return "";
}
final int pos = str.indexOf(separator);
if (pos == -1) {
return "";
}
return str.substring(pos + separator.length());
}
public static String trySubstringBetween(final String str, final String open, final String close) {
final int start = str.indexOf(open);
if (start != -1) {
final int end = str.indexOf(close, start + open.length());
if (end != -1) {
return str.substring(start + open.length(), end);
}
}
return str;
}
public static boolean equalsAny(final CharSequence string, final CharSequence... searchStrings) {
if (!Objects.isNull(searchStrings)) {
for (final CharSequence next : searchStrings) {
if (Objects.equals(string, next)) {
return true;
}
}
}
return false;
}
}
package cn.ac.iscas.dmo.db.filesystem.base.util;
import cn.ac.iscas.dmo.db.api.model.permission.AuthContext;
import cn.ac.iscas.dmo.db.api.model.permission.Datasource;
import cn.ac.iscas.dmo.db.api.model.permission.Permission;
import cn.ac.iscas.dmo.db.api.util.AuthContextHolder;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
/**
* @author zhuquanwen
* @version 1.0
* @date 2022/6/21 10:44
* @since jdk11
*/
public class FsCommonUtils {
private FsCommonUtils() {
}
/**
* 一个KB的字节数
*/
public static final long KB = 1024L;
/**
* 一个MB的字节数
*/
public static final long MB = 1024 * 1024L;
/**
* 一个GB的字节数
*/
public static final long GB = 1024 * 1024 * 1024L;
/**
* 一个TB的字节数
*/
public static final long TB = 1024 * 1024 * 1024 * 1024L;
/**
* 用户目录
*/
public static final String USER_SPACE_FORMATTER = "/dmo/users/namespace/%s";
/**
* URL匹配器
*/
public static final UrlMatcher URL_MATCHER = new UrlMatcher();
/**
* 获取有权限的文件路径
*
* @param datasourceName 数据源名称
* @return java.util.List
* @date 2022/6/21
* @since jdk11
*/
public static List<String> getPathPermission(String datasourceName) {
return Optional.ofNullable(AuthContextHolder.getContext())
.map(AuthContext::getPermission)
.map(Permission::getDatasources).flatMap(ds -> ds.stream().filter(d -> d.getName().equalsIgnoreCase(datasourceName)).findFirst())
.map(Datasource::getFilePaths)
.orElse(Collections.emptyList());
}
/**
* 修改路径,将// 替换为/, \替换为/, \\替换为/,使得不同操作系统下的文件分割符统一成 / ,防止拼接时\ / 混用
*
* @param path 路径
* @return java.lang.String 替换后的路径
* @date 2022/6/21
* @since jdk11
*/
public static String optimizationPath(String path) {
path = path.replaceAll("//+", "/")
.replace("\\", "/")
.replace("\\\\", "/");
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return path;
}
/**
* 格式化文件大小
*
* @param len 长度
* @return java.lang.String
* @date 2022/6/21
* @since jdk11
*/
public static String formatSize(long len) {
if (len < KB) {
return len + " B";
} else if (len < MB) {
return scale(len * 1.0 / KB, 2) + " KB";
} else if (len < GB) {
return scale(len * 1.0 / MB, 2) + " MB";
} else if (len < TB) {
return scale(len * 1.0 / GB, 2) + " GB";
} else {
return scale(len * 1.0 / TB, 2) + " TB";
}
}
/**
* 对效数取有效数字
*
* @param data 数据
* @param scale 有效位数
* @return double
* @date 2022/6/21
* @since jdk11
*/
@SuppressWarnings("SameParameterValue")
public static double scale(double data, int scale) {
BigDecimal bg = new BigDecimal(data);
return bg.setScale(scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* 格式化时间
*
* @param timeMillis 毫秒数
* @return java.lang.String
* @date 2022/6/21
* @since jdk11
*/
public static String formatTime(long timeMillis) {
return DateSafeUtils.format(new Date(timeMillis), DateSafeUtils.PATTERN);
}
/**
* 获取文件深度
*
* @param parentPath 路径
* @return int
* @date 2022/6/21
* @since jdk11
*/
public static int getLevel(String parentPath) {
if (parentPath.endsWith("/")) {
parentPath = parentPath.substring(0, parentPath.lastIndexOf("/"));
}
parentPath = optimizationPath(parentPath);
if (Objects.equals("/", parentPath)) {
return 1;
}
return parentPath.split("/").length;
}
}