org.dromara.hutool.extra.ssh.Sftp Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2023 looly([email protected])
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.extra.ssh;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.extra.ftp.AbstractFtp;
import org.dromara.hutool.extra.ftp.FtpConfig;
import org.dromara.hutool.extra.ftp.FtpException;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.ChannelSftp.LsEntrySelector;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.function.Predicate;
/**
* SFTP是Secure File Transfer Protocol的缩写,安全文件传送协议。可以为传输文件提供一种安全的加密方法。
* SFTP 为 SSH的一部份,是一种传输文件到服务器的安全方式。SFTP是使用加密传输认证信息和传输的数据,所以,使用SFTP是非常安全的。
* 但是,由于这种传输方式使用了加密/解密技术,所以传输效率比普通的FTP要低得多,如果您对网络安全性要求更高时,可以使用SFTP代替FTP。
*
*
* 此类为基于jsch的SFTP实现
* 参考:https://www.cnblogs.com/longyg/archive/2012/06/25/2556576.html
*
*
* @author looly
* @since 4.0.2
*/
public class Sftp extends AbstractFtp {
private Session session;
private ChannelSftp channel;
// ---------------------------------------------------------------------------------------- Constructor start
/**
* 构造
*
* @param sshHost 远程主机
* @param sshPort 远程主机端口
* @param sshUser 远程主机用户名
* @param sshPass 远程主机密码
*/
public Sftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass) {
this(sshHost, sshPort, sshUser, sshPass, DEFAULT_CHARSET);
}
/**
* 构造
*
* @param sshHost 远程主机
* @param sshPort 远程主机端口
* @param sshUser 远程主机用户名
* @param sshPass 远程主机密码
* @param charset 编码
* @since 4.1.14
*/
public Sftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass, final Charset charset) {
this(new FtpConfig(sshHost, sshPort, sshUser, sshPass, charset));
}
/**
* 构造
*
* @param config FTP配置
* @since 5.3.3
*/
public Sftp(final FtpConfig config) {
this(config, true);
}
/**
* 构造
*
* @param config FTP配置
* @param init 是否立即初始化
* @since 5.8.4
*/
public Sftp(final FtpConfig config, final boolean init) {
super(config);
if (init) {
init(config);
}
}
/**
* 构造
*
* @param session {@link Session}
*/
public Sftp(final Session session) {
this(session, DEFAULT_CHARSET);
}
/**
* 构造
*
* @param session {@link Session}
* @param charset 编码
* @since 4.1.14
*/
public Sftp(final Session session, final Charset charset) {
super(FtpConfig.of().setCharset(charset));
init(session, charset);
}
/**
* 构造
*
* @param channel {@link ChannelSftp}
* @param charset 编码
*/
public Sftp(final ChannelSftp channel, final Charset charset) {
super(FtpConfig.of().setCharset(charset));
init(channel, charset);
}
/**
* 构造
*
* @param session {@link Session}
* @param charset 编码
* @param timeOut 超时时间,单位毫秒
* @since 5.8.4
*/
public Sftp(final Session session, final Charset charset, final long timeOut) {
super(FtpConfig.of().setCharset(charset).setConnectionTimeout(timeOut));
init(session, charset);
}
/**
* 构造
*
* @param channel {@link ChannelSftp}
* @param charset 编码
* @param timeOut 超时时间,单位毫秒
* @since 5.8.4
*/
public Sftp(final ChannelSftp channel, final Charset charset, final long timeOut) {
super(FtpConfig.of().setCharset(charset).setConnectionTimeout(timeOut));
init(channel, charset);
}
// ---------------------------------------------------------------------------------------- Constructor end
/**
* 构造
*
* @param sshHost 远程主机
* @param sshPort 远程主机端口
* @param sshUser 远程主机用户名
* @param sshPass 远程主机密码
* @param charset 编码
*/
public void init(final String sshHost, final int sshPort, final String sshUser, final String sshPass, final Charset charset) {
init(JschUtil.getSession(sshHost, sshPort, sshUser, sshPass), charset);
}
/**
* 初始化
*
* @since 5.3.3
*/
public void init() {
init(this.ftpConfig);
}
/**
* 初始化
*
* @param config FTP配置
* @since 5.3.3
*/
public void init(final FtpConfig config) {
init(config.getHost(), config.getPort(), config.getUser(), config.getPassword(), config.getCharset());
}
/**
* 初始化
*
* @param session {@link Session}
* @param charset 编码
*/
public void init(final Session session, final Charset charset) {
this.session = session;
init(JschUtil.openSftp(session, (int) this.ftpConfig.getConnectionTimeout()), charset);
}
/**
* 初始化
*
* @param channel {@link ChannelSftp}
* @param charset 编码
*/
public void init(final ChannelSftp channel, final Charset charset) {
this.ftpConfig.setCharset(charset);
try {
channel.setFilenameEncoding(charset.toString());
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
this.channel = channel;
}
@Override
public Sftp reconnectIfTimeout() {
if (StrUtil.isBlank(this.ftpConfig.getHost())) {
throw new FtpException("Host is blank!");
}
try {
this.cd(StrUtil.SLASH);
} catch (final FtpException e) {
close();
init();
}
return this;
}
/**
* 获取SFTP通道客户端
*
* @return 通道客户端
* @since 4.1.14
*/
public ChannelSftp getClient() {
return this.channel;
}
/**
* 远程当前目录
*
* @return 远程当前目录
*/
@Override
public String pwd() {
try {
return channel.pwd();
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
}
/**
* 获取HOME路径
*
* @return HOME路径
* @since 4.0.5
*/
public String home() {
try {
return channel.getHome();
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
}
/**
* 遍历某个目录下所有文件或目录,不会递归遍历
*
* @param path 遍历某个目录下所有文件或目录
* @return 目录或文件名列表
* @since 4.0.5
*/
@Override
public List ls(final String path) {
return ls(path, null);
}
/**
* 遍历某个目录下所有目录,不会递归遍历
*
* @param path 遍历某个目录下所有目录
* @return 目录名列表
* @since 4.0.5
*/
public List lsDirs(final String path) {
return ls(path, t -> t.getAttrs().isDir());
}
/**
* 遍历某个目录下所有文件,不会递归遍历
*
* @param path 遍历某个目录下所有文件
* @return 文件名列表
* @since 4.0.5
*/
public List lsFiles(final String path) {
return ls(path, t -> !t.getAttrs().isDir());
}
/**
* 遍历某个目录下所有文件或目录,不会递归遍历
* 此方法自动过滤"."和".."两种目录
*
* @param path 遍历某个目录下所有文件或目录
* @param predicate 文件或目录过滤器,可以实现过滤器返回自己需要的文件或目录名列表,{@link Predicate#test(Object)}为{@code true}保留
* @return 目录或文件名列表
* @since 4.0.5
*/
public List ls(final String path, final Predicate predicate) {
final List entries = lsEntries(path, predicate);
if (CollUtil.isEmpty(entries)) {
return ListUtil.empty();
}
return CollUtil.map(entries, LsEntry::getFilename);
}
/**
* 遍历某个目录下所有文件或目录,生成LsEntry列表,不会递归遍历
* 此方法自动过滤"."和".."两种目录
*
* @param path 遍历某个目录下所有文件或目录
* @return 目录或文件名列表
* @since 5.3.5
*/
public List lsEntries(final String path) {
return lsEntries(path, null);
}
/**
* 遍历某个目录下所有文件或目录,生成LsEntry列表,不会递归遍历
* 此方法自动过滤"."和".."两种目录
*
* @param path 遍历某个目录下所有文件或目录
* @param predicate 文件或目录过滤器,可以实现过滤器返回自己需要的文件或目录名列表,{@link Predicate#test(Object)}为{@code true}保留
* @return 目录或文件名列表
* @since 5.3.5
*/
public List lsEntries(final String path, final Predicate predicate) {
final List entryList = new ArrayList<>();
try {
channel.ls(path, entry -> {
final String fileName = entry.getFilename();
if (!StrUtil.equals(".", fileName) && !StrUtil.equals("..", fileName)) {
if (null == predicate || predicate.test(entry)) {
entryList.add(entry);
}
}
return LsEntrySelector.CONTINUE;
});
} catch (final SftpException e) {
if (!StrUtil.startWithIgnoreCase(e.getMessage(), "No such file")) {
throw new JschRuntimeException(e);
}
// 文件不存在忽略
}
return entryList;
}
@Override
public boolean mkdir(final String dir) {
if (isDir(dir)) {
// 目录已经存在,创建直接返回
return true;
}
try {
this.channel.mkdir(dir);
return true;
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
}
@Override
public boolean isDir(final String dir) {
final SftpATTRS sftpATTRS;
try {
sftpATTRS = this.channel.stat(dir);
} catch (final SftpException e) {
final String msg = e.getMessage();
// issue#I4P9ED@Gitee
if (StrUtil.containsAnyIgnoreCase(msg, "No such file", "does not exist")) {
// 文件不存在直接返回false
// pr#378@Gitee
return false;
}
throw new FtpException(e);
}
return sftpATTRS.isDir();
}
/**
* 打开指定目录,如果指定路径非目录或不存在抛出异常
*
* @param directory directory
* @return 是否打开目录
* @throws FtpException 进入目录失败异常
*/
@Override
synchronized public boolean cd(final String directory) throws FtpException {
if (StrUtil.isBlank(directory)) {
// 当前目录
return true;
}
try {
channel.cd(directory.replace('\\', '/'));
return true;
} catch (final SftpException e) {
throw new FtpException(e);
}
}
/**
* 删除文件
*
* @param filePath 要删除的文件绝对路径
*/
@Override
public boolean delFile(final String filePath) {
try {
channel.rm(filePath);
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
return true;
}
/**
* 删除文件夹及其文件夹下的所有文件
*
* @param dirPath 文件夹路径
* @return boolean 是否删除成功
*/
@Override
@SuppressWarnings("unchecked")
public boolean delDir(final String dirPath) {
if (!cd(dirPath)) {
return false;
}
final Vector list;
try {
list = channel.ls(channel.pwd());
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
String fileName;
for (final LsEntry entry : list) {
fileName = entry.getFilename();
if (!".".equals(fileName) && !"..".equals(fileName)) {
if (entry.getAttrs().isDir()) {
delDir(fileName);
} else {
delFile(fileName);
}
}
}
if (!cd("..")) {
return false;
}
// 删除空目录
try {
channel.rmdir(dirPath);
return true;
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
}
/**
* 将本地文件或者文件夹同步(覆盖)上传到远程路径
*
* @param remotePath 远程路径
* @param file 文件或者文件夹
* @since 5.7.6
*/
public void upload(final String remotePath, final File file) {
if (!FileUtil.exists(file)) {
return;
}
if (file.isDirectory()) {
final File[] files = file.listFiles();
if (files == null) {
return;
}
for (final File fileItem : files) {
if (fileItem.isDirectory()) {
final String mkdir = FileUtil.normalize(remotePath + "/" + fileItem.getName());
this.upload(mkdir, fileItem);
} else {
this.uploadFile(remotePath, fileItem);
}
}
} else {
this.uploadFile(remotePath, file);
}
}
@SuppressWarnings("resource")
@Override
public boolean uploadFile(final String destPath, final File file) {
if(!FileUtil.isFile(file)){
throw new FtpException("[{}] is not a file!", file);
}
this.mkDirs(destPath);
put(FileUtil.getAbsolutePath(file), destPath);
return true;
}
/**
* 上传文件到指定目录,可选:
*
*
* 1. path为null或""上传到当前路径
* 2. path为相对路径则相对于当前路径的子路径
* 3. path为绝对路径则上传到此路径
*
*
* @param destPath 服务端路径,可以为{@code null} 或者相对路径或绝对路径
* @param fileName 文件名
* @param fileStream 文件流
* @since 5.7.16
*/
public void uploadFile(String destPath, final String fileName, final InputStream fileStream) {
destPath = StrUtil.addSuffixIfNot(destPath, StrUtil.SLASH) + StrUtil.removePrefix(fileName, StrUtil.SLASH);
put(fileStream, destPath, null, Mode.OVERWRITE);
}
/**
* 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。覆盖模式
*
* @param srcFilePath 本地文件路径
* @param destPath 目标路径,
* @return this
*/
public Sftp put(final String srcFilePath, final String destPath) {
return put(srcFilePath, destPath, Mode.OVERWRITE);
}
/**
* 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。
*
* @param srcFilePath 本地文件路径
* @param destPath 目标路径,
* @param mode {@link Mode} 模式
* @return this
*/
public Sftp put(final String srcFilePath, final String destPath, final Mode mode) {
return put(srcFilePath, destPath, null, mode);
}
/**
* 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。
*
* @param srcFilePath 本地文件路径
* @param destPath 目标路径,
* @param monitor 上传进度监控,通过实现此接口完成进度显示
* @param mode {@link Mode} 模式
* @return this
* @since 4.6.5
*/
public Sftp put(final String srcFilePath, final String destPath, final SftpProgressMonitor monitor, final Mode mode) {
try {
channel.put(srcFilePath, destPath, monitor, mode.ordinal());
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
return this;
}
/**
* 将本地数据流上传到目标服务器,目标文件名为destPath,目标必须为文件
*
* @param srcStream 本地的数据流
* @param destPath 目标路径,
* @param monitor 上传进度监控,通过实现此接口完成进度显示
* @param mode {@link Mode} 模式
* @return this
* @since 5.7.16
*/
public Sftp put(final InputStream srcStream, final String destPath, final SftpProgressMonitor monitor, final Mode mode) {
try {
channel.put(srcStream, destPath, monitor, mode.ordinal());
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
return this;
}
@Override
public void download(final String src, final File destFile) {
get(src, FileUtil.getAbsolutePath(destFile));
}
/**
* 下载文件到{@link OutputStream}中
*
* @param src 源文件路径,包括文件名
* @param out 目标流
* @see #get(String, OutputStream)
*/
public void download(final String src, final OutputStream out) {
get(src, out);
}
/**
* 递归下载FTP服务器上文件到本地(文件目录和服务器同步)
*
* @param sourcePath ftp服务器目录,必须为目录
* @param destDir 本地目录
*/
@Override
public void recursiveDownloadFolder(final String sourcePath, final File destDir) throws JschRuntimeException {
String fileName;
String srcFile;
File destFile;
for (final LsEntry item : lsEntries(sourcePath)) {
fileName = item.getFilename();
srcFile = StrUtil.format("{}/{}", sourcePath, fileName);
destFile = FileUtil.file(destDir, fileName);
if (!item.getAttrs().isDir()) {
// 本地不存在文件或者ftp上文件有修改则下载
if (!FileUtil.exists(destFile)
|| (item.getAttrs().getMTime() > (destFile.lastModified() / 1000))) {
download(srcFile, destFile);
}
} else {
// 服务端依旧是目录,继续递归
FileUtil.mkdir(destFile);
recursiveDownloadFolder(srcFile, destFile);
}
}
}
/**
* 获取远程文件
*
* @param src 远程文件路径
* @param dest 目标文件路径
* @return this
*/
public Sftp get(final String src, final String dest) {
try {
channel.get(src, dest);
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
return this;
}
/**
* 获取远程文件
*
* @param src 远程文件路径
* @param out 目标流
* @return this
* @since 5.7.0
*/
public Sftp get(final String src, final OutputStream out) {
try {
channel.get(src, out);
} catch (final SftpException e) {
throw new JschRuntimeException(e);
}
return this;
}
@Override
public void close() {
JschUtil.close(this.channel);
JschUtil.close(this.session);
}
@Override
public String toString() {
return "Sftp{" +
"host='" + this.ftpConfig.getHost() + '\'' +
", port=" + this.ftpConfig.getPort() +
", user='" + this.ftpConfig.getUser() + '\'' +
'}';
}
/**
* JSch支持的三种文件传输模式
*
* @author looly
*/
public enum Mode {
/**
* 完全覆盖模式,这是JSch的默认文件传输模式,即如果目标文件已经存在,传输的文件将完全覆盖目标文件,产生新的文件。
*/
OVERWRITE,
/**
* 恢复模式,如果文件已经传输一部分,这时由于网络或其他任何原因导致文件传输中断,如果下一次传输相同的文件,则会从上一次中断的地方续传。
*/
RESUME,
/**
* 追加模式,如果目标文件已存在,传输的文件将在目标文件后追加。
*/
APPEND
}
}