fix code and add Ftp

This commit is contained in:
Looly 2023-12-20 14:22:09 +08:00
parent 3eec56008c
commit 004d4f407d
11 changed files with 1071 additions and 1057 deletions

View File

@ -13,15 +13,12 @@
package org.dromara.hutool.extra.ftp;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.io.file.FileNameUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.text.CharUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.core.text.StrUtil;
import java.io.Closeable;
import java.io.File;
import java.nio.charset.Charset;
import java.util.List;
/**
@ -30,15 +27,15 @@ import java.util.List;
* @author looly
* @since 4.1.14
*/
public abstract class AbstractFtp implements Closeable {
/**
* 默认编码
*/
public static final Charset DEFAULT_CHARSET = CharsetUtil.UTF_8;
public abstract class AbstractFtp implements Ftp {
protected FtpConfig ftpConfig;
@Override
public FtpConfig getConfig() {
return this.ftpConfig;
}
/**
* 构造
*
@ -49,74 +46,7 @@ public abstract class AbstractFtp implements Closeable {
this.ftpConfig = config;
}
/**
* 如果连接超时的话重新进行连接
*
* @return this
* @since 4.5.2
*/
public abstract AbstractFtp reconnectIfTimeout();
/**
* 打开指定目录具体逻辑取决于实现例如在FTP中进入失败返回{@code false} SFTP中则抛出异常
*
* @param directory directory
* @return 是否打开目录
*/
public abstract boolean cd(String directory);
/**
* 打开上级目录
*
* @return 是否打开目录
* @since 4.0.5
*/
public boolean toParent() {
return cd("..");
}
/**
* 远程当前目录工作目录
*
* @return 远程当前目录
*/
public abstract String pwd();
/**
* 判断给定路径是否为目录
*
* @param dir 被判断的路径
* @return 是否为目录
* @since 5.7.5
*/
public boolean isDir(final String dir) {
final String workDir = pwd();
try {
return cd(dir);
} finally {
cd(workDir);
}
}
/**
* 在当前远程目录工作目录下创建新的目录
*
* @param dir 目录名
* @return 是否创建成功
*/
public abstract boolean mkdir(String dir);
/**
* 文件或目录是否存在<br>
* <ul>
* <li>提供路径为空则返回{@code false}</li>
* <li>提供路径非目录但是以'/''\'结尾返回{@code false}</li>
* <li>文件名是'.'或者'..'返回{@code false}</li>
* </ul>
*
* @param path 目录
* @return 是否存在
*/
@Override
public boolean exist(final String path) {
if (StrUtil.isBlank(path)) {
return false;
@ -130,12 +60,12 @@ public abstract class AbstractFtp implements Closeable {
}
final String fileName = FileNameUtil.getName(path);
if (".".equals(fileName) || "..".equals(fileName)) {
if (StrUtil.DOT.equals(fileName) || StrUtil.DOUBLE_DOT.equals(fileName)) {
return false;
}
// 文件验证
final String dir = StrUtil.defaultIfEmpty(StrUtil.removeSuffix(path, fileName), ".");
final String dir = StrUtil.defaultIfEmpty(StrUtil.removeSuffix(path, fileName), StrUtil.DOT);
// issue#I7CSQ9 检查父目录为目录且是否存在
if (!isDir(dir)) {
return false;
@ -149,35 +79,7 @@ public abstract class AbstractFtp implements Closeable {
return containsIgnoreCase(names, fileName);
}
/**
* 遍历某个目录下所有文件和目录不会递归遍历
*
* @param path 需要遍历的目录
* @return 文件和目录列表
*/
public abstract List<String> ls(String path);
/**
* 删除指定目录下的指定文件
*
* @param path 目录路径
* @return 是否存在
*/
public abstract boolean delFile(String path);
/**
* 删除文件夹及其文件夹下的所有文件
*
* @param dirPath 文件夹路径
* @return boolean 是否删除成功
*/
public abstract boolean delDir(String dirPath);
/**
* 创建指定文件夹及其父目录从根目录开始创建创建完成后回到默认的工作目录
*
* @param dir 文件夹路径绝对路径
*/
@Override
public void mkDirs(final String dir) {
final String[] dirs = StrUtil.trim(dir).split("[\\\\/]+");
@ -207,24 +109,6 @@ public abstract class AbstractFtp implements Closeable {
cd(now);
}
/**
* 将本地文件上传到目标服务器目标文件名为destPath若destPath为目录则目标文件名将与file文件名相同
* 覆盖模式
*
* @param destPath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param file 需要上传的文件
* @return 是否成功
*/
public abstract boolean uploadFile(String destPath, File file);
/**
* 下载文件
*
* @param path 文件路径
* @param outFile 输出文件或目录
*/
public abstract void download(String path, File outFile);
/**
* 下载文件-避免未完成的文件<br>
* 来自<a href="https://gitee.com/dromara/hutool/pulls/407">https://gitee.com/dromara/hutool/pulls/407</a><br>
@ -260,15 +144,6 @@ public abstract class AbstractFtp implements Closeable {
}
}
/**
* 递归下载FTP服务器上文件到本地(文件目录和服务器同步), 服务器上有新文件会覆盖本地文件
*
* @param sourcePath ftp服务器目录
* @param destDir 本地目录
* @since 5.3.5
*/
public abstract void recursiveDownloadFolder(String sourcePath, File destDir);
// ---------------------------------------------------------------------------------------------------------------------------------------- Private method start
/**
@ -279,18 +154,10 @@ public abstract class AbstractFtp implements Closeable {
* @return 是否包含
*/
private static boolean containsIgnoreCase(final List<String> names, final String nameToFind) {
if (CollUtil.isEmpty(names)) {
return false;
}
if (StrUtil.isEmpty(nameToFind)) {
return false;
}
for (final String name : names) {
if (nameToFind.equalsIgnoreCase(name)) {
return true;
}
}
return false;
return CollUtil.contains(names, nameToFind::equalsIgnoreCase);
}
// ---------------------------------------------------------------------------------------------------------------------------------------- Private method end
}

View File

@ -0,0 +1,750 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* 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:
* https://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.ftp;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.dromara.hutool.core.array.ArrayUtil;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.file.FileNameUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.extra.ssh.Connector;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
/**
* Apache Commons FTP客户端封装<br>
* 此客户端基于Apache-Commons-Net
* <p>
* 常见搭建ftp的工具有
* <ul>
* <li>filezila server ;根目录一般都是空</li>
* <li>linux vsftpd ; 使用的 系统用户的目录这里往往都是不是根目录/home/hutool/ftp</li>
* </ul>
*
* @author Looly
*/
public class CommonsFtp extends AbstractFtp {
/**
* 默认端口
*/
public static final int DEFAULT_PORT = 21;
// region ----- of
/**
* 构造CommonsFtp匿名登录
*
* @param host 域名或IP
* @return CommonsFtp
*/
public static CommonsFtp of(final String host) {
return of(host, DEFAULT_PORT);
}
/**
* 构造匿名登录
*
* @param host 域名或IP
* @param port 端口
* @return CommonsFtp
*/
public static CommonsFtp of(final String host, final int port) {
return of(host, port, "anonymous", StrUtil.EMPTY);
}
/**
* 构造
*
* @param host 域名或IP
* @param port 端口
* @param user 用户名
* @param password 密码
* @return CommonsFtp
*/
public static CommonsFtp of(final String host, final int port, final String user, final String password) {
return of(Connector.of(host, port, user, password), DEFAULT_CHARSET);
}
/**
* 构造
*
* @param connector 连接信息包括hostportuserpassword等信息
* @param charset 编码
* @return CommonsFtp
*/
public static CommonsFtp of(final Connector connector, final Charset charset) {
return of(connector, charset, null, null);
}
/**
* 构造
*
* @param connector 连接信息包括hostportuserpassword等信息
* @param charset 编码
* @param serverLanguageCode 服务器语言 例如zh
* @param systemKey 服务器标识 例如org.apache.commons.net.ftp.FTPClientConfig.SYST_NT
* @return CommonsFtp
*/
public static CommonsFtp of(final Connector connector, final Charset charset, final String serverLanguageCode, final String systemKey) {
return of(connector, charset, serverLanguageCode, systemKey, null);
}
/**
* 构造
*
* @param connector 连接信息包括hostportuserpassword等信息
* @param charset 编码
* @param serverLanguageCode 服务器语言
* @param systemKey 系统关键字
* @param mode 模式
* @return CommonsFtp
*/
public static CommonsFtp of(final Connector connector, final Charset charset, final String serverLanguageCode, final String systemKey, final FtpMode mode) {
return new CommonsFtp(new FtpConfig(connector, charset, serverLanguageCode, systemKey), mode);
}
//endregion
private FTPClient client;
private FtpMode mode;
/**
* 执行完操作是否返回当前目录
*/
private boolean backToPwd;
// region ----- 构造
/**
* 构造
*
* @param config FTP配置
* @param mode 模式
*/
public CommonsFtp(final FtpConfig config, final FtpMode mode) {
super(config);
this.mode = mode;
this.init();
}
/**
* 构造
*
* @param client 自定义实例化好的{@link FTPClient}
* @since 5.7.22
*/
public CommonsFtp(final FTPClient client) {
super(FtpConfig.of());
this.client = client;
}
// endregion
/**
* 初始化连接
*
* @return this
*/
public CommonsFtp init() {
return this.init(this.ftpConfig, this.mode);
}
/**
* 初始化连接
*
* @param config FTP配置
* @param mode 模式
* @return this
*/
public CommonsFtp init(final FtpConfig config, final FtpMode mode) {
final FTPClient client = new FTPClient();
// issue#I3O81Y@Gitee
client.setRemoteVerificationEnabled(false);
final Charset charset = config.getCharset();
if (null != charset) {
client.setControlEncoding(charset.toString());
}
client.setConnectTimeout((int) config.getConnector().getTimeout());
final String systemKey = config.getSystemKey();
if (StrUtil.isNotBlank(systemKey)) {
final FTPClientConfig conf = new FTPClientConfig(systemKey);
final String serverLanguageCode = config.getServerLanguageCode();
if (StrUtil.isNotBlank(serverLanguageCode)) {
conf.setServerLanguageCode(config.getServerLanguageCode());
}
client.configure(conf);
}
// connect
final Connector connector = config.getConnector();
try {
// 连接ftp服务器
client.connect(connector.getHost(), connector.getPort());
client.setSoTimeout((int) config.getSoTimeout());
// 登录ftp服务器
client.login(connector.getUser(), connector.getPassword());
} catch (final IOException e) {
throw new IORuntimeException(e);
}
final int replyCode = client.getReplyCode(); // 是否成功登录服务器
if (!FTPReply.isPositiveCompletion(replyCode)) {
try {
client.disconnect();
} catch (final IOException e) {
// ignore
}
throw new FtpException("Login failed for user [{}], reply code is: [{}]", connector.getUser(), replyCode);
}
this.client = client;
if (mode != null) {
//noinspection resource
setMode(mode);
}
return this;
}
/**
* 设置FTP连接模式可选主动和被动模式
*
* @param mode 模式枚举
* @return this
* @since 4.1.19
*/
public CommonsFtp setMode(final FtpMode mode) {
this.mode = mode;
switch (mode) {
case Active:
this.client.enterLocalActiveMode();
break;
case Passive:
this.client.enterLocalPassiveMode();
break;
}
return this;
}
/**
* 设置执行完操作是否返回当前目录
*
* @param backToPwd 执行完操作是否返回当前目录
* @return this
* @since 4.6.0
*/
public CommonsFtp setBackToPwd(final boolean backToPwd) {
this.backToPwd = backToPwd;
return this;
}
/**
* 是否执行完操作返回当前目录
*
* @return 执行完操作是否返回当前目录
* @since 5.7.17
*/
public boolean isBackToPwd() {
return this.backToPwd;
}
/**
* 如果连接超时的话重新进行连接 经测试当连接超时时client.isConnected()仍然返回ture无法判断是否连接超时 因此通过发送pwd命令的方式检查连接是否超时
*
* @return this
*/
@Override
public CommonsFtp reconnectIfTimeout() {
String pwd = null;
try {
pwd = pwd();
} catch (final IORuntimeException fex) {
// ignore
}
if (pwd == null) {
return this.init();
}
return this;
}
/**
* 改变目录
*
* @param directory 目录
* @return 是否成功
*/
@Override
synchronized public boolean cd(final String directory) {
if (StrUtil.isBlank(directory)) {
// 当前目录
return true;
}
try {
return client.changeWorkingDirectory(directory);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 远程当前目录
*
* @return 远程当前目录
* @since 4.1.14
*/
@Override
public String pwd() {
try {
return client.printWorkingDirectory();
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
@Override
public List<String> ls(final String path) {
return ArrayUtil.map(lsFiles(path), FTPFile::getName);
}
/**
* 遍历某个目录下所有文件和目录不会递归遍历<br>
* 此方法自动过滤"."".."两种目录
*
* @param path 目录
* @param predicate 过滤器null表示不过滤默认去掉"."".."两种目录
* @return 文件名或目录名列表
*/
public List<String> ls(final String path, final Predicate<FTPFile> predicate) {
return CollUtil.map(lsFiles(path, predicate), FTPFile::getName);
}
/**
* 遍历某个目录下所有文件和目录不会递归遍历<br>
* 此方法自动过滤"."".."两种目录
*
* @param path 目录
* @param predicate 过滤器null表示不过滤默认去掉"."".."两种目录
* @return 文件或目录列表
* @since 5.3.5
*/
public List<FTPFile> lsFiles(final String path, final Predicate<FTPFile> predicate) {
final FTPFile[] ftpFiles = lsFiles(path);
if (ArrayUtil.isEmpty(ftpFiles)) {
return ListUtil.empty();
}
final List<FTPFile> result = new ArrayList<>(ftpFiles.length - 2 <= 0 ? ftpFiles.length : ftpFiles.length - 2);
String fileName;
for (final FTPFile ftpFile : ftpFiles) {
fileName = ftpFile.getName();
if (!StrUtil.equals(".", fileName) && !StrUtil.equals("..", fileName)) {
if (null == predicate || predicate.test(ftpFile)) {
result.add(ftpFile);
}
}
}
return result;
}
/**
* 遍历某个目录下所有文件和目录不会递归遍历
*
* @param path 目录如果目录不存在抛出异常
* @return 文件或目录列表
* @throws FtpException 路径不存在
* @throws IORuntimeException IO异常
*/
public FTPFile[] lsFiles(final String path) throws FtpException, IORuntimeException {
String pwd = null;
if (StrUtil.isNotBlank(path)) {
pwd = pwd();
if (!cd(path)) {
throw new FtpException("Change dir to [{}] error, maybe path not exist!", path);
}
}
FTPFile[] ftpFiles;
try {
ftpFiles = this.client.listFiles();
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
// 回到原目录
cd(pwd);
}
return ftpFiles;
}
@Override
public boolean mkdir(final String dir) throws IORuntimeException {
try {
return this.client.makeDirectory(dir);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 获取服务端目录状态
*
* @param path 路径
* @return 状态int服务端不同返回不同
* @throws IORuntimeException IO异常
* @since 5.4.3
*/
public int stat(final String path) throws IORuntimeException {
try {
return this.client.stat(path);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 判断ftp服务器目录内是否还有子元素目录或文件
*
* @param path 文件路径
* @return 是否存在
* @throws IORuntimeException IO异常
*/
public boolean existFile(final String path) throws IORuntimeException {
final FTPFile[] ftpFileArr;
try {
ftpFileArr = client.listFiles(path);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
return ArrayUtil.isNotEmpty(ftpFileArr);
}
@Override
public boolean delFile(final String path) throws IORuntimeException {
final String pwd = pwd();
final String fileName = FileNameUtil.getName(path);
final String dir = StrUtil.removeSuffix(path, fileName);
if (!cd(dir)) {
throw new FtpException("Change dir to [{}] error, maybe dir not exist!", path);
}
boolean isSuccess;
try {
isSuccess = client.deleteFile(fileName);
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
// 回到原目录
cd(pwd);
}
return isSuccess;
}
@Override
public boolean delDir(final String dirPath) throws IORuntimeException {
final FTPFile[] dirs;
try {
dirs = client.listFiles(dirPath);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
String name;
String childPath;
for (final FTPFile ftpFile : dirs) {
name = ftpFile.getName();
childPath = StrUtil.format("{}/{}", dirPath, name);
if (ftpFile.isDirectory()) {
// 上级和本级目录除外
if (!".".equals(name) && !"..".equals(name)) {
delDir(childPath);
}
} else {
delFile(childPath);
}
}
// 删除空目录
try {
return this.client.removeDirectory(dirPath);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 上传文件到指定目录可选
*
* <pre>
* 1. remotePath为null或""上传到当前路径
* 2. remotePath为相对路径则相对于当前路径的子路径
* 3. remotePath为绝对路径则上传到此路径
* </pre>
*
* @param remotePath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param file 文件
* @return 是否上传成功
*/
@Override
public boolean uploadFile(final String remotePath, final File file) {
Assert.notNull(file, "file to upload is null !");
if (!FileUtil.isFile(file)) {
throw new FtpException("[{}] is not a file!", file);
}
return uploadFile(remotePath, file.getName(), file);
}
/**
* 上传文件到指定目录可选
*
* <pre>
* 1. remotePath为null或""上传到当前路径
* 2. remotePath为相对路径则相对于当前路径的子路径
* 3. remotePath为绝对路径则上传到此路径
* </pre>
*
* @param file 文件
* @param remotePath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param fileName 自定义在服务端保存的文件名
* @return 是否上传成功
* @throws IORuntimeException IO异常
*/
public boolean uploadFile(final String remotePath, final String fileName, final File file) throws IORuntimeException {
try (final InputStream in = FileUtil.getInputStream(file)) {
return uploadFile(remotePath, fileName, in);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 上传文件到指定目录可选
*
* <pre>
* 1. remotePath为null或""上传到当前路径
* 2. remotePath为相对路径则相对于当前路径的子路径
* 3. remotePath为绝对路径则上传到此路径
* </pre>
*
* @param remotePath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param fileName 文件名
* @param fileStream 文件流
* @return 是否上传成功
* @throws IORuntimeException IO异常
*/
public boolean uploadFile(final String remotePath, final String fileName, final InputStream fileStream) throws IORuntimeException {
try {
client.setFileType(FTPClient.BINARY_FILE_TYPE);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
String pwd = null;
if (this.backToPwd) {
pwd = pwd();
}
if (StrUtil.isNotBlank(remotePath)) {
mkDirs(remotePath);
if (!cd(remotePath)) {
throw new FtpException("Change dir to [{}] error, maybe dir not exist!", remotePath);
}
}
try {
return client.storeFile(fileName, fileStream);
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
if (this.backToPwd) {
cd(pwd);
}
}
}
/**
* 递归上传文件支持目录<br>
* 上传时如果uploadFile为目录只复制目录下所有目录和文件到目标路径下并不会复制目录本身<br>
* 上传时自动创建父级目录
*
* @param remotePath 目录路径
* @param uploadFile 上传文件或目录
*/
public void upload(final String remotePath, final File uploadFile) {
if (!FileUtil.isDirectory(uploadFile)) {
this.uploadFile(remotePath, uploadFile);
return;
}
final File[] files = uploadFile.listFiles();
if (ArrayUtil.isEmpty(files)) {
return;
}
final List<File> dirs = new ArrayList<>(files.length);
//第一次只处理文件防止目录在前面导致先处理子目录而引发文件所在目录不正确
for (final File f : files) {
if (f.isDirectory()) {
dirs.add(f);
} else {
this.uploadFile(remotePath, f);
}
}
//第二次只处理目录
for (final File f : dirs) {
final String dir = FileUtil.normalize(remotePath + "/" + f.getName());
upload(dir, f);
}
}
/**
* 下载文件
*
* @param path 文件路径包含文件名
* @param outFile 输出文件或目录当为目录时使用服务端的文件名
*/
@Override
public void download(final String path, final File outFile) {
final String fileName = FileNameUtil.getName(path);
final String dir = StrUtil.removeSuffix(path, fileName);
download(dir, fileName, outFile);
}
/**
* 递归下载FTP服务器上文件到本地(文件目录和服务器同步)
*
* @param sourcePath ftp服务器目录
* @param destDir 本地目录
*/
@Override
public void recursiveDownloadFolder(final String sourcePath, final File destDir) {
String fileName;
String srcFile;
File destFile;
for (final FTPFile ftpFile : lsFiles(sourcePath, null)) {
fileName = ftpFile.getName();
srcFile = StrUtil.format("{}/{}", sourcePath, fileName);
destFile = FileUtil.file(destDir, fileName);
if (!ftpFile.isDirectory()) {
// 本地不存在文件或者ftp上文件有修改则下载
if (!FileUtil.exists(destFile)
|| (ftpFile.getTimestamp().getTimeInMillis() > destFile.lastModified())) {
download(srcFile, destFile);
}
} else {
// 服务端依旧是目录继续递归
FileUtil.mkdir(destFile);
recursiveDownloadFolder(srcFile, destFile);
}
}
}
/**
* 下载文件
*
* @param path 文件所在路径远程目录不包含文件名
* @param fileName 文件名
* @param outFile 输出文件或目录当为目录时使用服务端文件名
* @throws IORuntimeException IO异常
*/
public void download(final String path, final String fileName, File outFile) throws IORuntimeException {
if (outFile.isDirectory()) {
outFile = new File(outFile, fileName);
}
if (!outFile.exists()) {
FileUtil.touch(outFile);
}
try (final OutputStream out = FileUtil.getOutputStream(outFile)) {
download(path, fileName, out);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 下载文件到输出流
*
* @param path 文件路径
* @param fileName 文件名
* @param out 输出位置
*/
public void download(final String path, final String fileName, final OutputStream out) {
download(path, fileName, out, null);
}
/**
* 下载文件到输出流
*
* @param path 服务端的文件路径
* @param fileName 服务端的文件名
* @param out 输出流下载的文件写出到这个流中
* @param fileNameCharset 文件名编码通过此编码转换文件名编码为ISO8859-1
* @throws IORuntimeException IO异常
* @since 5.5.7
*/
public void download(final String path, String fileName, final OutputStream out, final Charset fileNameCharset) throws IORuntimeException {
String pwd = null;
if (this.backToPwd) {
pwd = pwd();
}
if (!cd(path)) {
throw new FtpException("Change dir to [{}] error, maybe dir not exist!", path);
}
if (null != fileNameCharset) {
fileName = new String(fileName.getBytes(fileNameCharset), StandardCharsets.ISO_8859_1);
}
try {
client.setFileType(FTPClient.BINARY_FILE_TYPE);
client.retrieveFile(fileName, out);
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
if (backToPwd) {
cd(pwd);
}
}
}
/**
* 获取FTPClient客户端对象
*
* @return {@link FTPClient}
*/
public FTPClient getClient() {
return this.client;
}
@Override
public void close() throws IOException {
if (null != this.client) {
this.client.logout();
if (this.client.isConnected()) {
this.client.disconnect();
}
this.client = null;
}
}
}

View File

@ -1,773 +1,156 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* 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:
* https://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.ftp;
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.io.IORuntimeException;
import org.dromara.hutool.core.io.file.FileNameUtil;
import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.array.ArrayUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.rythmengine.utils.F;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
/**
* FTP客户端封装<br>
* 此客户端基于Apache-Commons-Net
* <p>
* 常见搭建ftp的工具有
* 1filezila server ;根目录一般都是空
* 2linux vsftpd ; 使用的 系统用户的目录这里往往都是不是根目录/home/ftpuser/ftp
* FTP的统一规范接口
*
* @author looly
* @since 4.1.8
* @author Looly
*/
public class Ftp extends AbstractFtp {
public interface Ftp extends Closeable {
/**
* 默认端口
* 默认编码
*/
public static final int DEFAULT_PORT = 21;
private FTPClient client;
private FtpMode mode;
/**
* 执行完操作是否返回当前目录
*/
private boolean backToPwd;
Charset DEFAULT_CHARSET = CharsetUtil.UTF_8;
/**
* 构造匿名登录
* 获取FTP配置
*
* @param host 域名或IP
* @return FTP配置
*/
public Ftp(final String host) {
this(host, DEFAULT_PORT);
}
FtpConfig getConfig();
/**
* 构造匿名登录
*
* @param host 域名或IP
* @param port 端口
*/
public Ftp(final String host, final int port) {
this(host, port, "anonymous", "");
}
/**
* 构造
*
* @param host 域名或IP
* @param port 端口
* @param user 用户名
* @param password 密码
*/
public Ftp(final String host, final int port, final String user, final String password) {
this(host, port, user, password, CharsetUtil.UTF_8);
}
/**
* 构造
*
* @param host 域名或IP
* @param port 端口
* @param user 用户名
* @param password 密码
* @param charset 编码
*/
public Ftp(final String host, final int port, final String user, final String password, final Charset charset) {
this(host, port, user, password, charset, null, null);
}
/**
* 构造
*
* @param host 域名或IP
* @param port 端口
* @param user 用户名
* @param password 密码
* @param charset 编码
* @param serverLanguageCode 服务器语言 例如zh
* @param systemKey 服务器标识 例如org.apache.commons.net.ftp.FTPClientConfig.SYST_NT
*/
public Ftp(final String host, final int port, final String user, final String password, final Charset charset, final String serverLanguageCode, final String systemKey) {
this(host, port, user, password, charset, serverLanguageCode, systemKey, null);
}
/**
* 构造
*
* @param host 域名或IP
* @param port 端口
* @param user 用户名
* @param password 密码
* @param charset 编码
* @param serverLanguageCode 服务器语言
* @param systemKey 系统关键字
* @param mode 模式
*/
public Ftp(final String host, final int port, final String user, final String password, final Charset charset, final String serverLanguageCode, final String systemKey, final FtpMode mode) {
this(new FtpConfig(host, port, user, password, charset, serverLanguageCode, systemKey), mode);
}
/**
* 构造
*
* @param config FTP配置
* @param mode 模式
*/
public Ftp(final FtpConfig config, final FtpMode mode) {
super(config);
this.mode = mode;
this.init();
}
/**
* 构造
*
* @param client 自定义实例化好的{@link FTPClient}
* @since 5.7.22
*/
public Ftp(final FTPClient client) {
super(FtpConfig.of());
this.client = client;
}
/**
* 初始化连接
* 如果连接超时的话重新进行连接
*
* @return this
*/
public Ftp init() {
return this.init(this.ftpConfig, this.mode);
}
Ftp reconnectIfTimeout();
/**
* 初始化连接
*
* @param host 域名或IP
* @param port 端口
* @param user 用户名
* @param password 密码
* @return this
*/
public Ftp init(final String host, final int port, final String user, final String password) {
return this.init(host, port, user, password, null);
}
/**
* 初始化连接
*
* @param host 域名或IP
* @param port 端口
* @param user 用户名
* @param password 密码
* @param mode 模式
* @return this
*/
public Ftp init(final String host, final int port, final String user, final String password, final FtpMode mode) {
return init(new FtpConfig(host, port, user, password, this.ftpConfig.getCharset(), null, null), mode);
}
/**
* 初始化连接
*
* @param config FTP配置
* @param mode 模式
* @return this
*/
public Ftp init(final FtpConfig config, final FtpMode mode) {
final FTPClient client = new FTPClient();
// issue#I3O81Y@Gitee
client.setRemoteVerificationEnabled(false);
final Charset charset = config.getCharset();
if (null != charset) {
client.setControlEncoding(charset.toString());
}
client.setConnectTimeout((int) config.getConnectionTimeout());
final String systemKey = config.getSystemKey();
if (StrUtil.isNotBlank(systemKey)) {
final FTPClientConfig conf = new FTPClientConfig(systemKey);
final String serverLanguageCode = config.getServerLanguageCode();
if (StrUtil.isNotBlank(serverLanguageCode)) {
conf.setServerLanguageCode(config.getServerLanguageCode());
}
client.configure(conf);
}
// connect
try {
// 连接ftp服务器
client.connect(config.getHost(), config.getPort());
client.setSoTimeout((int) config.getSoTimeout());
// 登录ftp服务器
client.login(config.getUser(), config.getPassword());
} catch (final IOException e) {
throw new IORuntimeException(e);
}
final int replyCode = client.getReplyCode(); // 是否成功登录服务器
if (!FTPReply.isPositiveCompletion(replyCode)) {
try {
client.disconnect();
} catch (final IOException e) {
// ignore
}
throw new FtpException("Login failed for user [{}], reply code is: [{}]", config.getUser(), replyCode);
}
this.client = client;
if (mode != null) {
//noinspection resource
setMode(mode);
}
return this;
}
/**
* 设置FTP连接模式可选主动和被动模式
*
* @param mode 模式枚举
* @return this
* @since 4.1.19
*/
public Ftp setMode(final FtpMode mode) {
this.mode = mode;
switch (mode) {
case Active:
this.client.enterLocalActiveMode();
break;
case Passive:
this.client.enterLocalPassiveMode();
break;
}
return this;
}
/**
* 设置执行完操作是否返回当前目录
*
* @param backToPwd 执行完操作是否返回当前目录
* @return this
* @since 4.6.0
*/
public Ftp setBackToPwd(final boolean backToPwd) {
this.backToPwd = backToPwd;
return this;
}
/**
* 是否执行完操作返回当前目录
*
* @return 执行完操作是否返回当前目录
* @since 5.7.17
*/
public boolean isBackToPwd() {
return this.backToPwd;
}
/**
* 如果连接超时的话重新进行连接 经测试当连接超时时client.isConnected()仍然返回ture无法判断是否连接超时 因此通过发送pwd命令的方式检查连接是否超时
*
* @return this
*/
@Override
public Ftp reconnectIfTimeout() {
String pwd = null;
try {
pwd = pwd();
} catch (final IORuntimeException fex) {
// ignore
}
if (pwd == null) {
return this.init();
}
return this;
}
/**
* 改变目录
*
* @param directory 目录
* @return 是否成功
*/
@Override
synchronized public boolean cd(final String directory) {
if (StrUtil.isBlank(directory)) {
// 当前目录
return true;
}
try {
return client.changeWorkingDirectory(directory);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 远程当前目录
* 远程当前目录工作目录
*
* @return 远程当前目录
* @since 4.1.14
*/
@Override
public String pwd() {
String pwd();
/**
* 打开指定目录具体逻辑取决于实现例如在FTP中进入失败返回{@code false} SFTP中则抛出异常
*
* @param directory directory
* @return 是否打开目录
*/
boolean cd(String directory);
/**
* 打开上级目录
*
* @return 是否打开目录
*/
default boolean toParent() {
return cd(StrUtil.DOUBLE_DOT);
}
/**
* 文件或目录是否存在<br>
* <ul>
* <li>提供路径为空则返回{@code false}</li>
* <li>提供路径非目录但是以'/''\'结尾返回{@code false}</li>
* <li>文件名是'.'或者'..'返回{@code false}</li>
* </ul>
*
* @param path 目录
* @return 是否存在
*/
boolean exist(final String path);
/**
* 判断给定路径是否为目录
*
* @param dir 被判断的路径
* @return 是否为目录
* @since 5.7.5
*/
default boolean isDir(final String dir) {
final String workDir = pwd();
try {
return client.printWorkingDirectory();
} catch (final IOException e) {
throw new IORuntimeException(e);
return cd(dir);
} finally {
cd(workDir);
}
}
@Override
public List<String> ls(final String path) {
return ArrayUtil.map(lsFiles(path), FTPFile::getName);
}
/**
* 遍历某个目录下所有文件和目录不会递归遍历<br>
* 此方法自动过滤"."".."两种目录
* 在当前远程目录工作目录下创建新的目录
*
* @param path 目录
* @param predicate 过滤器null表示不过滤默认去掉"."".."两种目录
* @return 文件名或目录名列表
* @param dir 目录名
* @return 是否创建成功
*/
public List<String> ls(final String path, final Predicate<FTPFile> predicate) {
return CollUtil.map(lsFiles(path, predicate), FTPFile::getName);
}
boolean mkdir(String dir);
/**
* 遍历某个目录下所有文件和目录不会递归遍历<br>
* 此方法自动过滤"."".."两种目录
* 创建指定文件夹及其父目录从根目录开始创建创建完成后回到默认的工作目录
*
* @param path 目录
* @param predicate 过滤器null表示不过滤默认去掉"."".."两种目录
* @return 文件或目录列表
* @since 5.3.5
* @param dir 文件夹路径绝对路径
*/
public List<FTPFile> lsFiles(final String path, final Predicate<FTPFile> predicate) {
final FTPFile[] ftpFiles = lsFiles(path);
if (ArrayUtil.isEmpty(ftpFiles)) {
return ListUtil.empty();
}
final List<FTPFile> result = new ArrayList<>(ftpFiles.length - 2 <= 0 ? ftpFiles.length : ftpFiles.length - 2);
String fileName;
for (final FTPFile ftpFile : ftpFiles) {
fileName = ftpFile.getName();
if (!StrUtil.equals(".", fileName) && !StrUtil.equals("..", fileName)) {
if (null == predicate || predicate.test(ftpFile)) {
result.add(ftpFile);
}
}
}
return result;
}
void mkDirs(final String dir);
/**
* 遍历某个目录下所有文件和目录不会递归遍历
*
* @param path 目录如果目录不存在抛出异常
* @return 文件或目录列表
* @throws FtpException 路径不存在
* @throws IORuntimeException IO异常
* @param path 需要遍历的目录
* @return 文件和目录列表
*/
public FTPFile[] lsFiles(final String path) throws FtpException, IORuntimeException {
String pwd = null;
if (StrUtil.isNotBlank(path)) {
pwd = pwd();
if (!cd(path)) {
throw new FtpException("Change dir to [{}] error, maybe path not exist!", path);
}
}
FTPFile[] ftpFiles;
try {
ftpFiles = this.client.listFiles();
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
// 回到原目录
cd(pwd);
}
return ftpFiles;
}
@Override
public boolean mkdir(final String dir) throws IORuntimeException {
try {
return this.client.makeDirectory(dir);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
List<String> ls(String path);
/**
* 获取服务端目录状态
* 删除指定目录下的指定文件
*
* @param path 路径
* @return 状态int服务端不同返回不同
* @throws IORuntimeException IO异常
* @since 5.4.3
*/
public int stat(final String path) throws IORuntimeException {
try {
return this.client.stat(path);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 判断ftp服务器目录内是否还有子元素目录或文件
*
* @param path 文件路径
* @param path 目录路径
* @return 是否存在
* @throws IORuntimeException IO异常
*/
public boolean existFile(final String path) throws IORuntimeException {
final FTPFile[] ftpFileArr;
try {
ftpFileArr = client.listFiles(path);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
return ArrayUtil.isNotEmpty(ftpFileArr);
}
@Override
public boolean delFile(final String path) throws IORuntimeException {
final String pwd = pwd();
final String fileName = FileNameUtil.getName(path);
final String dir = StrUtil.removeSuffix(path, fileName);
if (!cd(dir)) {
throw new FtpException("Change dir to [{}] error, maybe dir not exist!", path);
}
boolean isSuccess;
try {
isSuccess = client.deleteFile(fileName);
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
// 回到原目录
cd(pwd);
}
return isSuccess;
}
@Override
public boolean delDir(final String dirPath) throws IORuntimeException {
final FTPFile[] dirs;
try {
dirs = client.listFiles(dirPath);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
String name;
String childPath;
for (final FTPFile ftpFile : dirs) {
name = ftpFile.getName();
childPath = StrUtil.format("{}/{}", dirPath, name);
if (ftpFile.isDirectory()) {
// 上级和本级目录除外
if (!".".equals(name) && !"..".equals(name)) {
delDir(childPath);
}
} else {
delFile(childPath);
}
}
// 删除空目录
try {
return this.client.removeDirectory(dirPath);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
boolean delFile(String path);
/**
* 上传文件到指定目录可选
* 删除文件夹及其文件夹下的所有文件
*
* <pre>
* 1. remotePath为null或""上传到当前路径
* 2. remotePath为相对路径则相对于当前路径的子路径
* 3. remotePath为绝对路径则上传到此路径
* </pre>
*
* @param remotePath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param file 文件
* @return 是否上传成功
* @param dirPath 文件夹路径
* @return boolean 是否删除成功
*/
@Override
public boolean uploadFile(final String remotePath, final File file) {
Assert.notNull(file, "file to upload is null !");
if (!FileUtil.isFile(file)) {
throw new FtpException("[{}] is not a file!", file);
}
return uploadFile(remotePath, file.getName(), file);
}
boolean delDir(String dirPath);
/**
* 上传文件到指定目录可选
* 将本地文件上传到目标服务器目标文件名为destPath若destPath为目录则目标文件名将与file文件名相同
* 覆盖模式
*
* <pre>
* 1. remotePath为null或""上传到当前路径
* 2. remotePath为相对路径则相对于当前路径的子路径
* 3. remotePath为绝对路径则上传到此路径
* </pre>
*
* @param file 文件
* @param remotePath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param fileName 自定义在服务端保存的文件名
* @return 是否上传成功
* @throws IORuntimeException IO异常
* @param destPath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param file 需要上传的文件
* @return 是否成功
*/
public boolean uploadFile(final String remotePath, final String fileName, final File file) throws IORuntimeException {
try (final InputStream in = FileUtil.getInputStream(file)) {
return uploadFile(remotePath, fileName, in);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 上传文件到指定目录可选
*
* <pre>
* 1. remotePath为null或""上传到当前路径
* 2. remotePath为相对路径则相对于当前路径的子路径
* 3. remotePath为绝对路径则上传到此路径
* </pre>
*
* @param remotePath 服务端路径可以为{@code null} 或者相对路径或绝对路径
* @param fileName 文件名
* @param fileStream 文件流
* @return 是否上传成功
* @throws IORuntimeException IO异常
*/
public boolean uploadFile(final String remotePath, final String fileName, final InputStream fileStream) throws IORuntimeException {
try {
client.setFileType(FTPClient.BINARY_FILE_TYPE);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
String pwd = null;
if (this.backToPwd) {
pwd = pwd();
}
if (StrUtil.isNotBlank(remotePath)) {
mkDirs(remotePath);
if (!cd(remotePath)) {
throw new FtpException("Change dir to [{}] error, maybe dir not exist!", remotePath);
}
}
try {
return client.storeFile(fileName, fileStream);
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
if (this.backToPwd) {
cd(pwd);
}
}
}
/**
* 递归上传文件支持目录<br>
* 上传时如果uploadFile为目录只复制目录下所有目录和文件到目标路径下并不会复制目录本身<br>
* 上传时自动创建父级目录
*
* @param remotePath 目录路径
* @param uploadFile 上传文件或目录
*/
public void upload(final String remotePath, final File uploadFile) {
if (!FileUtil.isDirectory(uploadFile)) {
this.uploadFile(remotePath, uploadFile);
return;
}
final File[] files = uploadFile.listFiles();
if (ArrayUtil.isEmpty(files)) {
return;
}
final List<File> dirs = new ArrayList<>(files.length);
//第一次只处理文件防止目录在前面导致先处理子目录而引发文件所在目录不正确
for (final File f : files) {
if (f.isDirectory()) {
dirs.add(f);
} else {
this.uploadFile(remotePath, f);
}
}
//第二次只处理目录
for (final File f : dirs) {
final String dir = FileUtil.normalize(remotePath + "/" + f.getName());
upload(dir, f);
}
}
boolean uploadFile(String destPath, File file);
/**
* 下载文件
*
* @param path 文件路径包含文件名
* @param outFile 输出文件或目录当为目录时使用服务端的文件名
* @param path 文件路径
* @param outFile 输出文件或目录
*/
@Override
public void download(final String path, final File outFile) {
final String fileName = FileNameUtil.getName(path);
final String dir = StrUtil.removeSuffix(path, fileName);
download(dir, fileName, outFile);
}
void download(String path, File outFile);
/**
* 递归下载FTP服务器上文件到本地(文件目录和服务器同步)
* 递归下载FTP服务器上文件到本地(文件目录和服务器同步), 服务器上有新文件会覆盖本地文件
*
* @param sourcePath ftp服务器目录
* @param destDir 本地目录
* @since 5.3.5
*/
@Override
public void recursiveDownloadFolder(final String sourcePath, final File destDir) {
String fileName;
String srcFile;
File destFile;
for (final FTPFile ftpFile : lsFiles(sourcePath, null)) {
fileName = ftpFile.getName();
srcFile = StrUtil.format("{}/{}", sourcePath, fileName);
destFile = FileUtil.file(destDir, fileName);
if (!ftpFile.isDirectory()) {
// 本地不存在文件或者ftp上文件有修改则下载
if (!FileUtil.exists(destFile)
|| (ftpFile.getTimestamp().getTimeInMillis() > destFile.lastModified())) {
download(srcFile, destFile);
}
} else {
// 服务端依旧是目录继续递归
FileUtil.mkdir(destFile);
recursiveDownloadFolder(srcFile, destFile);
}
}
}
/**
* 下载文件
*
* @param path 文件所在路径远程目录不包含文件名
* @param fileName 文件名
* @param outFile 输出文件或目录当为目录时使用服务端文件名
* @throws IORuntimeException IO异常
*/
public void download(final String path, final String fileName, File outFile) throws IORuntimeException {
if (outFile.isDirectory()) {
outFile = new File(outFile, fileName);
}
if (!outFile.exists()) {
FileUtil.touch(outFile);
}
try (final OutputStream out = FileUtil.getOutputStream(outFile)) {
download(path, fileName, out);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 下载文件到输出流
*
* @param path 文件路径
* @param fileName 文件名
* @param out 输出位置
*/
public void download(final String path, final String fileName, final OutputStream out) {
download(path, fileName, out, null);
}
/**
* 下载文件到输出流
*
* @param path 服务端的文件路径
* @param fileName 服务端的文件名
* @param out 输出流下载的文件写出到这个流中
* @param fileNameCharset 文件名编码通过此编码转换文件名编码为ISO8859-1
* @throws IORuntimeException IO异常
* @since 5.5.7
*/
public void download(final String path, String fileName, final OutputStream out, final Charset fileNameCharset) throws IORuntimeException {
String pwd = null;
if (this.backToPwd) {
pwd = pwd();
}
if (!cd(path)) {
throw new FtpException("Change dir to [{}] error, maybe dir not exist!", path);
}
if (null != fileNameCharset) {
fileName = new String(fileName.getBytes(fileNameCharset), StandardCharsets.ISO_8859_1);
}
try {
client.setFileType(FTPClient.BINARY_FILE_TYPE);
client.retrieveFile(fileName, out);
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
if (backToPwd) {
cd(pwd);
}
}
}
/**
* 获取FTPClient客户端对象
*
* @return {@link FTPClient}
*/
public FTPClient getClient() {
return this.client;
}
@Override
public void close() throws IOException {
if (null != this.client) {
this.client.logout();
if (this.client.isConnected()) {
this.client.disconnect();
}
this.client = null;
}
}
void recursiveDownloadFolder(String sourcePath, File destDir);
}

View File

@ -12,6 +12,8 @@
package org.dromara.hutool.extra.ftp;
import org.dromara.hutool.extra.ssh.Connector;
import java.io.Serializable;
import java.nio.charset.Charset;
@ -32,44 +34,24 @@ public class FtpConfig implements Serializable {
return new FtpConfig();
}
/**
* 主机
*/
private String host;
/**
* 端口
*/
private int port;
/**
* 用户名
*/
private String user;
/**
* 密码
*/
private String password;
private Connector connector;
/**
* 编码
*/
private Charset charset;
/**
* 连接超时时长单位毫秒
*/
private long connectionTimeout;
/**
* Socket连接超时时长单位毫秒
*/
private long soTimeout;
/**
* 设置服务器语言
* 服务器语言
*/
private String serverLanguageCode;
/**
* 设置服务器系统关键词
* 服务器系统关键词
*/
private String systemKey;
@ -82,114 +64,137 @@ public class FtpConfig implements Serializable {
/**
* 构造
*
* @param host 主机
* @param port 端口
* @param user 用户名
* @param password 密码
* @param connector 连接信息包括hostportuserpassword等信息
* @param charset 编码
*/
public FtpConfig(final String host, final int port, final String user, final String password, final Charset charset) {
this(host, port, user, password, charset, null, null);
public FtpConfig(final Connector connector, final Charset charset) {
this(connector, charset, null, null);
}
/**
* 构造
*
* @param host 主机
* @param port 端口
* @param user 用户名
* @param password 密码
* @param connector 连接信息包括hostportuserpassword等信息
* @param charset 编码
* @param serverLanguageCode 服务器语言
* @param systemKey 系统关键字
* @since 5.5.7
*/
public FtpConfig(final String host, final int port, final String user, final String password, final Charset charset, final String serverLanguageCode, final String systemKey) {
this.host = host;
this.port = port;
this.user = user;
this.password = password;
public FtpConfig(final Connector connector, final Charset charset, final String serverLanguageCode, final String systemKey) {
this.connector = connector;
this.charset = charset;
this.serverLanguageCode = serverLanguageCode;
this.systemKey = systemKey;
}
public String getHost() {
return host;
/**
* 获取连接信息
*
* @return 连接信息
*/
public Connector getConnector() {
return connector;
}
public FtpConfig setHost(final String host) {
this.host = host;
/**
* 设置连接信息
*
* @param connector 连接信息
* @return this
*/
public FtpConfig setConnector(final Connector connector) {
this.connector = connector;
return this;
}
public int getPort() {
return port;
/**
* 设置超时注意此方法会调用{@link Connector#setTimeout(long)}<br>
* 此方法需在{@link #setConnector(Connector)}后调用否则会创建空的Connector
* @param timeout 链接超时
* @return this
*/
public FtpConfig setConnectionTimeout(final long timeout){
if(null == connector){
connector = Connector.of();
}
public FtpConfig setPort(final int port) {
this.port = port;
return this;
}
public String getUser() {
return user;
}
public FtpConfig setUser(final String user) {
this.user = user;
return this;
}
public String getPassword() {
return password;
}
public FtpConfig setPassword(final String password) {
this.password = password;
connector.setTimeout(timeout);
return this;
}
/**
* 获取编码
*
* @return 编码
*/
public Charset getCharset() {
return charset;
}
/**
* 设置编码
*
* @param charset 编码
* @return this
*/
public FtpConfig setCharset(final Charset charset) {
this.charset = charset;
return this;
}
public long getConnectionTimeout() {
return connectionTimeout;
}
public FtpConfig setConnectionTimeout(final long connectionTimeout) {
this.connectionTimeout = connectionTimeout;
return this;
}
/**
* 获取读取数据超时时间
*
* @return 读取数据超时时间
*/
public long getSoTimeout() {
return soTimeout;
}
/**
* 设置读取数据超时时间
*
* @param soTimeout 读取数据超时时间
* @return this
*/
public FtpConfig setSoTimeout(final long soTimeout) {
this.soTimeout = soTimeout;
return this;
}
/**
* 获取服务器语言
*
* @return 服务器语言
*/
public String getServerLanguageCode() {
return serverLanguageCode;
}
/**
* 设置服务器语言
*
* @param serverLanguageCode 服务器语言
* @return this
*/
public FtpConfig setServerLanguageCode(final String serverLanguageCode) {
this.serverLanguageCode = serverLanguageCode;
return this;
}
/**
* 获取服务器系统关键词
*
* @return 服务器系统关键词
*/
public String getSystemKey() {
return systemKey;
}
/**
* 设置服务器系统关键词
*
* @param systemKey 服务器系统关键词
* @return this
*/
public FtpConfig setSystemKey(final String systemKey) {
this.systemKey = systemKey;
return this;

View File

@ -22,8 +22,13 @@ package org.dromara.hutool.extra.ftp;
* @since 4.1.19
*/
public enum FtpMode {
/** 主动模式 */
/**
* 主动模式
*/
Active,
/** 被动模式 */
/**
* 被动模式
*/
Passive
}

View File

@ -13,16 +13,59 @@
package org.dromara.hutool.extra.ssh;
/**
* 连接者对象提供一些连接的基本信息
* 连接者对象提供一些连接的基本信息包括
* <ul>
* <li>host主机名</li>
* <li>port端口</li>
* <li>user用户名默认root</li>
* <li>password密码</li>
* <li>timeout连接超时毫秒数</li>
* </ul>
*
* @author looly
*/
public class Connector {
/**
* 创建Connector所有参数为默认用于构建模式
*
* @return Connector
*/
public static Connector of() {
return new Connector();
}
/**
* 创建Connector
*
* @param host 主机名
* @param port 端口
* @param user 用户名
* @param password 密码
* @return Connector
*/
public static Connector of(final String host, final int port, final String user, final String password) {
return of(host, port, user, password, 0);
}
/**
* 创建Connector
*
* @param host 主机名
* @param port 端口
* @param user 用户名
* @param password 密码
* @param timeout 连接超时时长0表示默认
* @return Connector
*/
public static Connector of(final String host, final int port, final String user, final String password, final long timeout) {
return new Connector(host, port, user, password, timeout);
}
private String host;
private int port;
private String user = "root";
private String password;
private String group;
private long timeout;
/**
@ -31,31 +74,6 @@ public class Connector {
public Connector() {
}
/**
* 构造
*
* @param user 用户名
* @param password 密码
* @param group
*/
public Connector(final String user, final String password, final String group) {
this.user = user;
this.password = password;
this.group = group;
}
/**
* 构造
*
* @param host 主机名
* @param port 端口
* @param user 用户名
* @param password 密码
*/
public Connector(final String host, final int port, final String user, final String password) {
this(host, port, user, password, 0);
}
/**
* 构造
*
@ -86,9 +104,11 @@ public class Connector {
* 设定主机名
*
* @param host 主机名
* @return this
*/
public void setHost(final String host) {
public Connector setHost(final String host) {
this.host = host;
return this;
}
/**
@ -104,9 +124,11 @@ public class Connector {
* 设定端口号
*
* @param port 端口号
* @return this
*/
public void setPort(final int port) {
public Connector setPort(final int port) {
this.port = port;
return this;
}
/**
@ -122,9 +144,11 @@ public class Connector {
* 设定用户名
*
* @param name 用户名
* @return this
*/
public void setUser(final String name) {
public Connector setUser(final String name) {
this.user = name;
return this;
}
/**
@ -140,27 +164,11 @@ public class Connector {
* 设定密码
*
* @param password 密码
* @return this
*/
public void setPassword(final String password) {
public Connector setPassword(final String password) {
this.password = password;
}
/**
* 获得用户组名
*
* @return 用户组
*/
public String getGroup() {
return group;
}
/**
* 设定用户组名
*
* @param group 用户组
*/
public void setGroup(final String group) {
this.group = group;
return this;
}
/**
@ -176,9 +184,11 @@ public class Connector {
* 设置连接超时时间
*
* @param timeout 连接超时时间
* @return this
*/
public void setTimeout(final long timeout) {
public Connector setTimeout(final long timeout) {
this.timeout = timeout;
return this;
}
/**
@ -191,7 +201,6 @@ public class Connector {
", port=" + port +
", user='" + user + '\'' +
", password='" + password + '\'' +
", group='" + group + '\'' +
", timeout=" + timeout +
'}';
}

View File

@ -52,8 +52,7 @@ public class JschSftp extends AbstractFtp {
private Session session;
private ChannelSftp channel;
// ---------------------------------------------------------------------------------------- Constructor start
//region ----- of
/**
* 构造
*
@ -61,9 +60,10 @@ public class JschSftp extends AbstractFtp {
* @param sshPort 远程主机端口
* @param sshUser 远程主机用户名
* @param sshPass 远程主机密码
* @return JschSftp
*/
public JschSftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass) {
this(sshHost, sshPort, sshUser, sshPass, DEFAULT_CHARSET);
public static JschSftp of(final String sshHost, final int sshPort, final String sshUser, final String sshPass) {
return of(sshHost, sshPort, sshUser, sshPass, DEFAULT_CHARSET);
}
/**
@ -74,11 +74,12 @@ public class JschSftp extends AbstractFtp {
* @param sshUser 远程主机用户名
* @param sshPass 远程主机密码
* @param charset 编码
* @since 4.1.14
* @return JschSftp
*/
public JschSftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass, final Charset charset) {
this(new FtpConfig(sshHost, sshPort, sshUser, sshPass, charset));
public static JschSftp of(final String sshHost, final int sshPort, final String sshUser, final String sshPass, final Charset charset) {
return new JschSftp(new FtpConfig(Connector.of(sshHost, sshPort, sshUser, sshPass), charset));
}
// endregion
/**
* 构造
@ -132,6 +133,7 @@ public class JschSftp extends AbstractFtp {
init();
}
// ---------------------------------------------------------------------------------------- Constructor end
/**
* 初始化
*/
@ -140,12 +142,13 @@ public class JschSftp extends AbstractFtp {
if (null == this.channel) {
if (null == this.session) {
final FtpConfig config = this.ftpConfig;
this.session = new JschSession(new Connector(
config.getHost(),
config.getPort(),
config.getUser(),
config.getPassword(),
config.getConnectionTimeout()))
final Connector connector = config.getConnector();
this.session = new JschSession(Connector.of(
connector.getHost(),
connector.getPort(),
connector.getUser(),
connector.getPassword(),
connector.getTimeout()))
.getRaw();
}
@ -159,7 +162,7 @@ public class JschSftp extends AbstractFtp {
try {
if (!channel.isConnected()) {
channel.connect((int) Math.max(this.ftpConfig.getConnectionTimeout(), 0));
channel.connect((int) Math.max(this.ftpConfig.getConnector().getTimeout(), 0));
}
channel.setFilenameEncoding(this.ftpConfig.getCharset().toString());
} catch (final JSchException | SftpException e) {
@ -169,7 +172,7 @@ public class JschSftp extends AbstractFtp {
@Override
public JschSftp reconnectIfTimeout() {
if (StrUtil.isBlank(this.ftpConfig.getHost())) {
if (StrUtil.isBlank(this.ftpConfig.getConnector().getHost())) {
throw new FtpException("Host is blank!");
}
try {
@ -638,10 +641,11 @@ public class JschSftp extends AbstractFtp {
@Override
public String toString() {
return "Sftp{" +
"host='" + this.ftpConfig.getHost() + '\'' +
", port=" + this.ftpConfig.getPort() +
", user='" + this.ftpConfig.getUser() + '\'' +
final Connector connector = this.ftpConfig.getConnector();
return "JschSftp{" +
"host='" + connector.getHost() + '\'' +
", port=" + connector.getPort() +
", user='" + connector.getUser() + '\'' +
'}';
}

View File

@ -20,7 +20,6 @@ import net.schmizz.sshj.xfer.FileSystemFile;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.extra.ftp.AbstractFtp;
import org.dromara.hutool.extra.ftp.FtpConfig;
import org.dromara.hutool.extra.ftp.FtpException;
@ -45,27 +44,17 @@ import java.util.List;
*/
public class SshjSftp extends AbstractFtp {
private SSHClient ssh;
private SFTPClient sftp;
/**
* 构造使用默认端口
*
* @param sshHost 主机
*/
public SshjSftp(final String sshHost) {
this(new FtpConfig(sshHost, 22, null, null, CharsetUtil.UTF_8));
}
// region ----- of
/**
* 构造
*
* @param sshHost 主机
* @param sshUser 用户名
* @param sshPass 密码
* @return SshjSftp
*/
public SshjSftp(final String sshHost, final String sshUser, final String sshPass) {
this(new FtpConfig(sshHost, 22, sshUser, sshPass, CharsetUtil.UTF_8));
public static SshjSftp of(final String sshHost, final String sshUser, final String sshPass) {
return of(sshHost, 22, sshUser, sshPass);
}
/**
@ -75,9 +64,10 @@ public class SshjSftp extends AbstractFtp {
* @param sshPort 端口
* @param sshUser 用户名
* @param sshPass 密码
* @return SshjSftp
*/
public SshjSftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass) {
this(new FtpConfig(sshHost, sshPort, sshUser, sshPass, CharsetUtil.UTF_8));
public static SshjSftp of(final String sshHost, final int sshPort, final String sshUser, final String sshPass) {
return of(sshHost, sshPort, sshUser, sshPass, DEFAULT_CHARSET);
}
/**
@ -88,10 +78,15 @@ public class SshjSftp extends AbstractFtp {
* @param sshUser 用户名
* @param sshPass 密码
* @param charset 编码
* @return SshjSftp
*/
public SshjSftp(final String sshHost, final int sshPort, final String sshUser, final String sshPass, final Charset charset) {
this(new FtpConfig(sshHost, sshPort, sshUser, sshPass, charset));
public static SshjSftp of(final String sshHost, final int sshPort, final String sshUser, final String sshPass, final Charset charset) {
return new SshjSftp(new FtpConfig(Connector.of(sshHost, sshPort, sshUser, sshPass), charset));
}
//endregion
private SSHClient ssh;
private SFTPClient sftp;
/**
* 构造
@ -106,6 +101,7 @@ public class SshjSftp extends AbstractFtp {
/**
* 构造
*
* @param sshClient {@link SSHClient}
* @param charset 编码
*/
@ -122,12 +118,7 @@ public class SshjSftp extends AbstractFtp {
* @since 5.7.18
*/
public void init() {
this.ssh = SshjUtil.openClient(new Connector(
ftpConfig.getHost(),
ftpConfig.getPort(),
ftpConfig.getUser(),
ftpConfig.getPassword(),
ftpConfig.getConnectionTimeout()));
this.ssh = SshjUtil.openClient(this.ftpConfig.getConnector());
try {
ssh.setRemoteCharset(ftpConfig.getCharset());
@ -139,7 +130,7 @@ public class SshjSftp extends AbstractFtp {
@Override
public AbstractFtp reconnectIfTimeout() {
if (StrUtil.isBlank(this.ftpConfig.getHost())) {
if (StrUtil.isBlank(this.ftpConfig.getConnector().getHost())) {
throw new FtpException("Host is blank!");
}
try {

View File

@ -28,7 +28,7 @@ public class FtpTest {
@Disabled
public void ftpsTest() {
final FTPSClient ftpsClient = new FTPSClient();
final Ftp ftp = new Ftp(ftpsClient);
final CommonsFtp ftp = new CommonsFtp(ftpsClient);
ftp.cd("/file/aaa");
Console.log(ftp.pwd());
@ -39,7 +39,7 @@ public class FtpTest {
@Test
@Disabled
public void cdTest() {
final Ftp ftp = new Ftp("looly.centos");
final CommonsFtp ftp = CommonsFtp.of("looly.centos");
ftp.cd("/file/aaa");
Console.log(ftp.pwd());
@ -50,7 +50,7 @@ public class FtpTest {
@Test
@Disabled
public void uploadTest() {
final Ftp ftp = new Ftp("localhost");
final CommonsFtp ftp = CommonsFtp.of("localhost");
final boolean upload = ftp.uploadFile("/temp", FileUtil.file("d:/test/test.zip"));
Console.log(upload);
@ -61,7 +61,7 @@ public class FtpTest {
@Test
@Disabled
public void reconnectIfTimeoutTest() throws InterruptedException {
final Ftp ftp = new Ftp("looly.centos");
final CommonsFtp ftp = CommonsFtp.of("looly.centos");
Console.log("打印pwd: " + ftp.pwd());
@ -71,7 +71,7 @@ public class FtpTest {
try {
Console.log("打印pwd: " + ftp.pwd());
} catch (final FtpException e) {
e.printStackTrace();
Console.error(e, e.getMessage());
}
Console.log("判断是否超时并重连...");
@ -85,7 +85,7 @@ public class FtpTest {
@Test
@Disabled
public void recursiveDownloadFolder() {
final Ftp ftp = new Ftp("looly.centos");
final CommonsFtp ftp = CommonsFtp.of("looly.centos");
ftp.recursiveDownloadFolder("/", FileUtil.file("d:/test/download"));
IoUtil.closeQuietly(ftp);
@ -94,7 +94,7 @@ public class FtpTest {
@Test
@Disabled
public void recursiveDownloadFolderSftp() {
final JschSftp ftp = new JschSftp("127.0.0.1", 22, "test", "test");
final JschSftp ftp = JschSftp.of("127.0.0.1", 22, "test", "test");
ftp.cd("/file/aaa");
Console.log(ftp.pwd());
@ -106,7 +106,7 @@ public class FtpTest {
@Test
@Disabled
public void downloadTest() {
final Ftp ftp = new Ftp("localhost");
final CommonsFtp ftp = CommonsFtp.of("localhost");
final List<String> fileNames = ftp.ls("temp/");
for (final String name : fileNames) {
@ -121,7 +121,7 @@ public class FtpTest {
@Test
@Disabled
public void isDirTest() throws Exception {
try (final Ftp ftp = new Ftp("127.0.0.1", 21)) {
try (final CommonsFtp ftp = CommonsFtp.of("127.0.0.1", 21)) {
Console.log(ftp.pwd());
ftp.isDir("/test");
Console.log(ftp.pwd());
@ -131,7 +131,7 @@ public class FtpTest {
@Test
@Disabled
public void existSftpTest() {
try (final JschSftp ftp = new JschSftp("127.0.0.1", 22, "test", "test")) {
try (final JschSftp ftp = JschSftp.of("127.0.0.1", 22, "test", "test")) {
Console.log(ftp.pwd());
Console.log(ftp.exist(null));
Console.log(ftp.exist(""));
@ -154,7 +154,7 @@ public class FtpTest {
@Test
@Disabled
public void existFtpTest() throws Exception {
try (final Ftp ftp = new Ftp("127.0.0.1", 21)) {
try (final CommonsFtp ftp = CommonsFtp.of("127.0.0.1", 21)) {
Console.log(ftp.pwd());
Console.log(ftp.exist(null));
Console.log(ftp.exist(""));

View File

@ -35,7 +35,7 @@ public class JschTest {
@Disabled
public void bindPortTest() {
//新建会话此会话用于ssh连接到跳板机堡垒机此处为10.1.1.1:22
final JschSession session = new JschSession(new Connector("looly.centos", 22, "test", "123456"));
final JschSession session = new JschSession(Connector.of("looly.centos", 22, "test", "123456"));
// 将堡垒机保护的内网8080端口映射到localhost我们就可以通过访问http://localhost:8080/访问内网服务了
session.bindLocalPort(8080, new InetSocketAddress("172.20.12.123", 8080));
}
@ -45,7 +45,7 @@ public class JschTest {
@Disabled
public void bindRemotePort() {
// 建立会话
final JschSession session = new JschSession(new Connector("looly.centos", 22, "test", "123456"));
final JschSession session = new JschSession(Connector.of("looly.centos", 22, "test", "123456"));
// 绑定ssh服务端8089端口到本机的8000端口上
session.bindRemotePort(new InetSocketAddress(8089), new InetSocketAddress("localhost", 8000));
// 保证一直运行
@ -55,7 +55,7 @@ public class JschTest {
@Test
@Disabled
public void sftpTest() {
final JschSession session = new JschSession(new Connector("looly.centos", 22, "root", "123456"));
final JschSession session = new JschSession(Connector.of("looly.centos", 22, "root", "123456"));
final JschSftp jschSftp = session.openSftp(CharsetUtil.UTF_8);
jschSftp.mkDirs("/opt/test/aaa/bbb");
Console.log("OK");
@ -65,7 +65,7 @@ public class JschTest {
@Test
@Disabled
public void reconnectIfTimeoutTest() throws InterruptedException {
final JschSession session = new JschSession(new Connector("sunnyserver", 22,"mysftp","liuyang1234"));
final JschSession session = new JschSession(Connector.of("sunnyserver", 22,"mysftp","liuyang1234"));
final JschSftp jschSftp = session.openSftp(CharsetUtil.UTF_8);
Console.log("打印pwd: " + jschSftp.pwd());

View File

@ -34,7 +34,7 @@ public class SftpTest {
@BeforeEach
@Disabled
public void init() {
sshjSftp = new SshjSftp("ip", 22, "test", "test", CharsetUtil.UTF_8);
sshjSftp = SshjSftp.of("ip", 22, "test", "test");
}
@Test