Browse Source

add recursiveDownloadFolder

Looly 5 years ago
parent
commit
fa936f4742

+ 3 - 1
CHANGELOG.md

@@ -3,9 +3,11 @@
 
 
 -------------------------------------------------------------------------------------------------------------
 -------------------------------------------------------------------------------------------------------------
 
 
-## 5.3.5 (2020-05-10)
+## 5.3.5 (2020-05-11)
 
 
 ### 新特性
 ### 新特性
+* 【core   】     增加CollUtil.map方法
+* 【extra  】     增加Sftp.lsEntries方法,Ftp和Sftp增加recursiveDownloadFolder(pr#121@Gitee)
 ### Bug修复
 ### Bug修复
 
 
 -------------------------------------------------------------------------------------------------------------
 -------------------------------------------------------------------------------------------------------------

+ 21 - 5
hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java

@@ -49,6 +49,7 @@ import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.LinkedBlockingDeque;
+import java.util.function.Function;
 
 
 /**
 /**
  * 集合相关工具类
  * 集合相关工具类
@@ -1170,16 +1171,31 @@ public class CollUtil {
 	 * @param ignoreNull 是否忽略空值
 	 * @param ignoreNull 是否忽略空值
 	 * @return 抽取后的新列表
 	 * @return 抽取后的新列表
 	 * @since 4.5.7
 	 * @since 4.5.7
+	 * @see #map(Iterable, Function, boolean)
 	 */
 	 */
 	public static List<Object> extract(Iterable<?> collection, Editor<Object> editor, boolean ignoreNull) {
 	public static List<Object> extract(Iterable<?> collection, Editor<Object> editor, boolean ignoreNull) {
-		final List<Object> fieldValueList = new ArrayList<>();
+		return map(collection, editor::edit, ignoreNull);
+	}
+
+	/**
+	 * 通过func自定义一个规则,此规则将原集合中的元素转换成新的元素,生成新的列表返回<br>
+	 * 例如提供的是一个Bean列表,通过Function接口实现获取某个字段值,返回这个字段值组成的新列表
+	 *
+	 * @param collection 原集合
+	 * @param func       编辑函数
+	 * @param ignoreNull 是否忽略空值
+	 * @return 抽取后的新列表
+	 * @since 5.3.5
+	 */
+	public static <T, R> List<R> map(Iterable<T> collection, Function<T, R> func, boolean ignoreNull) {
+		final List<R> fieldValueList = new ArrayList<>();
 		if (null == collection) {
 		if (null == collection) {
 			return fieldValueList;
 			return fieldValueList;
 		}
 		}
 
 
-		Object value;
-		for (Object bean : collection) {
-			value = editor.edit(bean);
+		R value;
+		for (T bean : collection) {
+			value = func.apply(bean);
 			if (null == value && ignoreNull) {
 			if (null == value && ignoreNull) {
 				continue;
 				continue;
 			}
 			}
@@ -1212,7 +1228,7 @@ public class CollUtil {
 	 * @since 4.5.7
 	 * @since 4.5.7
 	 */
 	 */
 	public static List<Object> getFieldValues(Iterable<?> collection, final String fieldName, boolean ignoreNull) {
 	public static List<Object> getFieldValues(Iterable<?> collection, final String fieldName, boolean ignoreNull) {
-		return extract(collection, bean -> {
+		return map(collection, bean -> {
 			if (bean instanceof Map) {
 			if (bean instanceof Map) {
 				return ((Map<?, ?>) bean).get(fieldName);
 				return ((Map<?, ?>) bean).get(fieldName);
 			} else {
 			} else {

+ 1 - 1
hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java

@@ -567,7 +567,7 @@ public class FileUtil {
 	 * @return 最后修改时间
 	 * @return 最后修改时间
 	 */
 	 */
 	public static Date lastModifiedTime(File file) {
 	public static Date lastModifiedTime(File file) {
-		if (!exist(file)) {
+		if (false == exist(file)) {
 			return null;
 			return null;
 		}
 		}
 
 

+ 28 - 26
hutool-extra/src/main/java/cn/hutool/extra/ftp/AbstractFtp.java

@@ -12,13 +12,13 @@ import java.util.List;
 
 
 /**
 /**
  * 抽象FTP类,用于定义通用的FTP方法
  * 抽象FTP类,用于定义通用的FTP方法
- * 
+ *
  * @author looly
  * @author looly
  * @since 4.1.14
  * @since 4.1.14
  */
  */
 public abstract class AbstractFtp implements Closeable {
 public abstract class AbstractFtp implements Closeable {
-	
-	public static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8 ;
+
+	public static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;
 
 
 	protected FtpConfig ftpConfig;
 	protected FtpConfig ftpConfig;
 
 
@@ -28,21 +28,21 @@ public abstract class AbstractFtp implements Closeable {
 	 * @param config FTP配置
 	 * @param config FTP配置
 	 * @since 5.3.3
 	 * @since 5.3.3
 	 */
 	 */
-	protected AbstractFtp(FtpConfig config){
+	protected AbstractFtp(FtpConfig config) {
 		this.ftpConfig = config;
 		this.ftpConfig = config;
 	}
 	}
 
 
 	/**
 	/**
 	 * 如果连接超时的话,重新进行连接
 	 * 如果连接超时的话,重新进行连接
-	 * @since 4.5.2
-	 * 
+	 *
 	 * @return this
 	 * @return this
+	 * @since 4.5.2
 	 */
 	 */
 	public abstract AbstractFtp reconnectIfTimeout();
 	public abstract AbstractFtp reconnectIfTimeout();
-	
+
 	/**
 	/**
 	 * 打开指定目录
 	 * 打开指定目录
-	 * 
+	 *
 	 * @param directory directory
 	 * @param directory directory
 	 * @return 是否打开目录
 	 * @return 是否打开目录
 	 */
 	 */
@@ -50,7 +50,7 @@ public abstract class AbstractFtp implements Closeable {
 
 
 	/**
 	/**
 	 * 打开上级目录
 	 * 打开上级目录
-	 * 
+	 *
 	 * @return 是否打开目录
 	 * @return 是否打开目录
 	 * @since 4.0.5
 	 * @since 4.0.5
 	 */
 	 */
@@ -60,14 +60,14 @@ public abstract class AbstractFtp implements Closeable {
 
 
 	/**
 	/**
 	 * 远程当前目录(工作目录)
 	 * 远程当前目录(工作目录)
-	 * 
+	 *
 	 * @return 远程当前目录
 	 * @return 远程当前目录
 	 */
 	 */
 	public abstract String pwd();
 	public abstract String pwd();
 
 
 	/**
 	/**
 	 * 在当前远程目录(工作目录)下创建新的目录
 	 * 在当前远程目录(工作目录)下创建新的目录
-	 * 
+	 *
 	 * @param dir 目录名
 	 * @param dir 目录名
 	 * @return 是否创建成功
 	 * @return 是否创建成功
 	 */
 	 */
@@ -75,7 +75,7 @@ public abstract class AbstractFtp implements Closeable {
 
 
 	/**
 	/**
 	 * 文件或目录是否存在
 	 * 文件或目录是否存在
-	 * 
+	 *
 	 * @param path 目录
 	 * @param path 目录
 	 * @return 是否存在
 	 * @return 是否存在
 	 */
 	 */
@@ -88,7 +88,7 @@ public abstract class AbstractFtp implements Closeable {
 
 
 	/**
 	/**
 	 * 遍历某个目录下所有文件和目录,不会递归遍历
 	 * 遍历某个目录下所有文件和目录,不会递归遍历
-	 * 
+	 *
 	 * @param path 需要遍历的目录
 	 * @param path 需要遍历的目录
 	 * @return 文件和目录列表
 	 * @return 文件和目录列表
 	 */
 	 */
@@ -96,7 +96,7 @@ public abstract class AbstractFtp implements Closeable {
 
 
 	/**
 	/**
 	 * 删除指定目录下的指定文件
 	 * 删除指定目录下的指定文件
-	 * 
+	 *
 	 * @param path 目录路径
 	 * @param path 目录路径
 	 * @return 是否存在
 	 * @return 是否存在
 	 */
 	 */
@@ -104,7 +104,7 @@ public abstract class AbstractFtp implements Closeable {
 
 
 	/**
 	/**
 	 * 删除文件夹及其文件夹下的所有文件
 	 * 删除文件夹及其文件夹下的所有文件
-	 * 
+	 *
 	 * @param dirPath 文件夹路径
 	 * @param dirPath 文件夹路径
 	 * @return boolean 是否删除成功
 	 * @return boolean 是否删除成功
 	 */
 	 */
@@ -112,14 +112,14 @@ public abstract class AbstractFtp implements Closeable {
 
 
 	/**
 	/**
 	 * 创建指定文件夹及其父目录,从根目录开始创建,创建完成后回到默认的工作目录
 	 * 创建指定文件夹及其父目录,从根目录开始创建,创建完成后回到默认的工作目录
-	 * 
+	 *
 	 * @param dir 文件夹路径,绝对路径
 	 * @param dir 文件夹路径,绝对路径
 	 */
 	 */
 	public void mkDirs(String dir) {
 	public void mkDirs(String dir) {
 		final String[] dirs = StrUtil.trim(dir).split("[\\\\/]+");
 		final String[] dirs = StrUtil.trim(dir).split("[\\\\/]+");
 
 
 		final String now = pwd();
 		final String now = pwd();
-		if(dirs.length > 0 && StrUtil.isEmpty(dirs[0])) {
+		if (dirs.length > 0 && StrUtil.isEmpty(dirs[0])) {
 			//首位为空,表示以/开头
 			//首位为空,表示以/开头
 			this.cd(StrUtil.SLASH);
 			this.cd(StrUtil.SLASH);
 		}
 		}
@@ -139,17 +139,17 @@ public abstract class AbstractFtp implements Closeable {
 	/**
 	/**
 	 * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与file文件名相同。
 	 * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与file文件名相同。
 	 * 覆盖模式
 	 * 覆盖模式
-	 * 
+	 *
 	 * @param destPath 服务端路径,可以为{@code null} 或者相对路径或绝对路径
 	 * @param destPath 服务端路径,可以为{@code null} 或者相对路径或绝对路径
-	 * @param file 需要上传的文件
+	 * @param file     需要上传的文件
 	 * @return 是否成功
 	 * @return 是否成功
 	 */
 	 */
 	public abstract boolean upload(String destPath, File file);
 	public abstract boolean upload(String destPath, File file);
 
 
 	/**
 	/**
 	 * 下载文件
 	 * 下载文件
-	 * 
-	 * @param path 文件路径
+	 *
+	 * @param path    文件路径
 	 * @param outFile 输出文件或目录
 	 * @param outFile 输出文件或目录
 	 */
 	 */
 	public abstract void download(String path, File outFile);
 	public abstract void download(String path, File outFile);
@@ -157,16 +157,18 @@ public abstract class AbstractFtp implements Closeable {
 	/**
 	/**
 	 * 递归下载FTP服务器上文件到本地(文件目录和服务器同步), 服务器上有新文件会覆盖本地文件
 	 * 递归下载FTP服务器上文件到本地(文件目录和服务器同步), 服务器上有新文件会覆盖本地文件
 	 *
 	 *
-	 * @param sourcePath      ftp服务器目录
-	 * @param destinationPath 本地目录
+	 * @param sourcePath ftp服务器目录
+	 * @param destDir    本地目录
+	 * @since 5.3.5
 	 */
 	 */
-	public abstract void recursiveDownloadFolder(String sourcePath, String destinationPath) throws Exception;
+	public abstract void recursiveDownloadFolder(String sourcePath, File destDir);
 
 
 	// ---------------------------------------------------------------------------------------------------------------------------------------- Private method start
 	// ---------------------------------------------------------------------------------------------------------------------------------------- Private method start
+
 	/**
 	/**
 	 * 是否包含指定字符串,忽略大小写
 	 * 是否包含指定字符串,忽略大小写
-	 * 
-	 * @param names 文件或目录名列表
+	 *
+	 * @param names      文件或目录名列表
 	 * @param nameToFind 要查找的文件或目录名
 	 * @param nameToFind 要查找的文件或目录名
 	 * @return 是否包含
 	 * @return 是否包含
 	 */
 	 */

+ 50 - 19
hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java

@@ -1,7 +1,9 @@
 package cn.hutool.extra.ftp;
 package cn.hutool.extra.ftp;
 
 
+import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.lang.Filter;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.CharsetUtil;
 import cn.hutool.core.util.CharsetUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
@@ -158,7 +160,7 @@ public class Ftp extends AbstractFtp {
 		try {
 		try {
 			// 连接ftp服务器
 			// 连接ftp服务器
 			client.connect(config.getHost(), config.getPort());
 			client.connect(config.getHost(), config.getPort());
-			client.setSoTimeout((int)config.getSoTimeout());
+			client.setSoTimeout((int) config.getSoTimeout());
 			// 登录ftp服务器
 			// 登录ftp服务器
 			client.login(config.getUser(), config.getPassword());
 			client.login(config.getUser(), config.getPassword());
 		} catch (IOException e) {
 		} catch (IOException e) {
@@ -278,6 +280,34 @@ public class Ftp extends AbstractFtp {
 	}
 	}
 
 
 	/**
 	/**
+	 * 遍历某个目录下所有文件和目录,不会递归遍历<br>
+	 * 此方法自动过滤"."和".."两种目录
+	 *
+	 * @param path   目录
+	 * @param filter 过滤器,null表示不过滤,默认去掉"."和".."两种目录
+	 * @return 文件或目录列表
+	 * @since 5.3.5
+	 */
+	public List<FTPFile> lsFiles(String path, Filter<FTPFile> filter) {
+		final FTPFile[] ftpFiles = lsFiles(path);
+		if (ArrayUtil.isEmpty(ftpFiles)) {
+			return ListUtil.empty();
+		}
+
+		final List<FTPFile> result = new ArrayList<>(ftpFiles.length - 2);
+		String fileName;
+		for (FTPFile ftpFile : ftpFiles) {
+			fileName = ftpFile.getName();
+			if (false == StrUtil.equals(".", fileName) && false == StrUtil.equals("..", fileName)) {
+				if (null == filter || filter.accept(ftpFile)) {
+					result.add(ftpFile);
+				}
+			}
+		}
+		return result;
+	}
+
+	/**
 	 * 遍历某个目录下所有文件和目录,不会递归遍历
 	 * 遍历某个目录下所有文件和目录,不会递归遍历
 	 *
 	 *
 	 * @param path 目录
 	 * @param path 目录
@@ -479,28 +509,29 @@ public class Ftp extends AbstractFtp {
 	/**
 	/**
 	 * 递归下载FTP服务器上文件到本地(文件目录和服务器同步)
 	 * 递归下载FTP服务器上文件到本地(文件目录和服务器同步)
 	 *
 	 *
-	 * @param sourcePath      ftp服务器目录
-	 * @param destinationPath 本地目录
+	 * @param sourcePath ftp服务器目录
+	 * @param destDir    本地目录
 	 */
 	 */
 	@Override
 	@Override
-	public void recursiveDownloadFolder(String sourcePath, String destinationPath) {
-		String pathSeparator = "/";
-		FTPFile[] lsFiles = lsFiles(sourcePath);
-
-		for (FTPFile ftpFile : lsFiles) {
-			String sourcePathPathFile = sourcePath + pathSeparator + ftpFile.getName();
-			String destinationPathFile = destinationPath + pathSeparator + ftpFile.getName();
-
-			if (!ftpFile.isDirectory()) {
+	public void recursiveDownloadFolder(String sourcePath, File destDir) {
+		String fileName;
+		String srcFile;
+		File destFile;
+		for (FTPFile ftpFile : lsFiles(sourcePath, null)) {
+			fileName = ftpFile.getName();
+			srcFile = StrUtil.format("{}/{}", sourcePath, fileName);
+			destFile = FileUtil.file(destDir, fileName);
+
+			if (false == ftpFile.isDirectory()) {
 				// 本地不存在文件或者ftp上文件有修改则下载
 				// 本地不存在文件或者ftp上文件有修改则下载
-				if (!FileUtil.exist(destinationPathFile)
-						|| (ftpFile.getTimestamp().getTimeInMillis() > FileUtil.lastModifiedTime(destinationPathFile).getTime())) {
-					// Download file from source (source filename, destination filename).
-					download(sourcePathPathFile, FileUtil.file(destinationPathFile));
+				if (false == FileUtil.exist(destFile)
+						|| (ftpFile.getTimestamp().getTimeInMillis() > destFile.lastModified())) {
+					download(srcFile, destFile);
 				}
 				}
-			} else if (!(".".equals(ftpFile.getName()) || "..".equals(ftpFile.getName()))) {
-				FileUtil.mkdir(destinationPathFile);
-				recursiveDownloadFolder(sourcePathPathFile, destinationPathFile);
+			} else {
+				// 服务端依旧是目录,继续递归
+				FileUtil.mkdir(destFile);
+				recursiveDownloadFolder(srcFile, destFile);
 			}
 			}
 		}
 		}
 	}
 	}

+ 76 - 39
hutool-extra/src/main/java/cn/hutool/extra/ssh/Sftp.java

@@ -1,5 +1,7 @@
 package cn.hutool.extra.ssh;
 package cn.hutool.extra.ssh;
 
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.lang.Filter;
 import cn.hutool.core.lang.Filter;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
@@ -37,6 +39,7 @@ public class Sftp extends AbstractFtp {
 	private ChannelSftp channel;
 	private ChannelSftp channel;
 
 
 	// ---------------------------------------------------------------------------------------- Constructor start
 	// ---------------------------------------------------------------------------------------- Constructor start
+
 	/**
 	/**
 	 * 构造
 	 * 构造
 	 *
 	 *
@@ -147,7 +150,7 @@ public class Sftp extends AbstractFtp {
 	 */
 	 */
 	public void init(Session session, Charset charset) {
 	public void init(Session session, Charset charset) {
 		this.session = session;
 		this.session = session;
-		init(JschUtil.openSftp(session, (int)this.ftpConfig.getConnectionTimeout()), charset);
+		init(JschUtil.openSftp(session, (int) this.ftpConfig.getConnectionTimeout()), charset);
 	}
 	}
 
 
 	/**
 	/**
@@ -247,32 +250,62 @@ public class Sftp extends AbstractFtp {
 	}
 	}
 
 
 	/**
 	/**
-	 * 遍历某个目录下所有文件或目录,不会递归遍历
+	 * 遍历某个目录下所有文件或目录,不会递归遍历<br>
+	 * 此方法自动过滤"."和".."两种目录
 	 *
 	 *
-	 * @param path 遍历某个目录下所有文件或目录
+	 * @param path   遍历某个目录下所有文件或目录
 	 * @param filter 文件或目录过滤器,可以实现过滤器返回自己需要的文件或目录名列表
 	 * @param filter 文件或目录过滤器,可以实现过滤器返回自己需要的文件或目录名列表
 	 * @return 目录或文件名列表
 	 * @return 目录或文件名列表
 	 * @since 4.0.5
 	 * @since 4.0.5
 	 */
 	 */
 	public List<String> ls(String path, final Filter<LsEntry> filter) {
 	public List<String> ls(String path, final Filter<LsEntry> filter) {
-		final List<String> fileNames = new ArrayList<>();
+		final List<LsEntry> entries = lsEntries(path, filter);
+		if (CollUtil.isEmpty(entries)) {
+			return ListUtil.empty();
+		}
+		return CollUtil.map(entries, LsEntry::getFilename, true);
+	}
+
+	/**
+	 * 遍历某个目录下所有文件或目录,生成LsEntry列表,不会递归遍历<br>
+	 * 此方法自动过滤"."和".."两种目录
+	 *
+	 * @param path 遍历某个目录下所有文件或目录
+	 * @return 目录或文件名列表
+	 * @since 5.3.5
+	 */
+	public List<LsEntry> lsEntries(String path) {
+		return lsEntries(path, null);
+	}
+
+	/**
+	 * 遍历某个目录下所有文件或目录,生成LsEntry列表,不会递归遍历<br>
+	 * 此方法自动过滤"."和".."两种目录
+	 *
+	 * @param path   遍历某个目录下所有文件或目录
+	 * @param filter 文件或目录过滤器,可以实现过滤器返回自己需要的文件或目录名列表
+	 * @return 目录或文件名列表
+	 * @since 5.3.5
+	 */
+	public List<LsEntry> lsEntries(String path, Filter<LsEntry> filter) {
+		final List<LsEntry> entryList = new ArrayList<>();
 		try {
 		try {
 			channel.ls(path, entry -> {
 			channel.ls(path, entry -> {
-				String fileName = entry.getFilename();
+				final String fileName = entry.getFilename();
 				if (false == StrUtil.equals(".", fileName) && false == StrUtil.equals("..", fileName)) {
 				if (false == StrUtil.equals(".", fileName) && false == StrUtil.equals("..", fileName)) {
 					if (null == filter || filter.accept(entry)) {
 					if (null == filter || filter.accept(entry)) {
-						fileNames.add(entry.getFilename());
+						entryList.add(entry);
 					}
 					}
 				}
 				}
 				return LsEntrySelector.CONTINUE;
 				return LsEntrySelector.CONTINUE;
 			});
 			});
 		} catch (SftpException e) {
 		} catch (SftpException e) {
-			if(false == StrUtil.startWithIgnoreCase(e.getMessage(), "No such file")){
+			if (false == StrUtil.startWithIgnoreCase(e.getMessage(), "No such file")) {
 				throw new JschRuntimeException(e);
 				throw new JschRuntimeException(e);
 			}
 			}
 			// 文件不存在忽略
 			// 文件不存在忽略
 		}
 		}
-		return fileNames;
+		return entryList;
 	}
 	}
 
 
 	@Override
 	@Override
@@ -375,7 +408,7 @@ public class Sftp extends AbstractFtp {
 	 * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。覆盖模式
 	 * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。覆盖模式
 	 *
 	 *
 	 * @param srcFilePath 本地文件路径
 	 * @param srcFilePath 本地文件路径
-	 * @param destPath 目标路径,
+	 * @param destPath    目标路径,
 	 * @return this
 	 * @return this
 	 */
 	 */
 	public Sftp put(String srcFilePath, String destPath) {
 	public Sftp put(String srcFilePath, String destPath) {
@@ -386,8 +419,8 @@ public class Sftp extends AbstractFtp {
 	 * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。
 	 * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。
 	 *
 	 *
 	 * @param srcFilePath 本地文件路径
 	 * @param srcFilePath 本地文件路径
-	 * @param destPath 目标路径,
-	 * @param mode {@link Mode} 模式
+	 * @param destPath    目标路径,
+	 * @param mode        {@link Mode} 模式
 	 * @return this
 	 * @return this
 	 */
 	 */
 	public Sftp put(String srcFilePath, String destPath, Mode mode) {
 	public Sftp put(String srcFilePath, String destPath, Mode mode) {
@@ -398,9 +431,9 @@ public class Sftp extends AbstractFtp {
 	 * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。
 	 * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。
 	 *
 	 *
 	 * @param srcFilePath 本地文件路径
 	 * @param srcFilePath 本地文件路径
-	 * @param destPath 目标路径,
-	 * @param monitor 上传进度监控,通过实现此接口完成进度显示
-	 * @param mode {@link Mode} 模式
+	 * @param destPath    目标路径,
+	 * @param monitor     上传进度监控,通过实现此接口完成进度显示
+	 * @param mode        {@link Mode} 模式
 	 * @return this
 	 * @return this
 	 * @since 4.6.5
 	 * @since 4.6.5
 	 */
 	 */
@@ -421,30 +454,29 @@ public class Sftp extends AbstractFtp {
 	/**
 	/**
 	 * 递归下载FTP服务器上文件到本地(文件目录和服务器同步)
 	 * 递归下载FTP服务器上文件到本地(文件目录和服务器同步)
 	 *
 	 *
-	 * @param sourcePath      ftp服务器目录
-	 * @param destinationPath 本地目录
+	 * @param sourcePath ftp服务器目录,必须为目录
+	 * @param destDir    本地目录
 	 */
 	 */
 	@Override
 	@Override
-	public void recursiveDownloadFolder(String sourcePath, String destinationPath) throws Exception {
-		String pathSeparator = "/";
-		Vector<ChannelSftp.LsEntry> fileAndFolderList = channel.ls(sourcePath);
-
-		//Iterate through list of folder content
-		for (ChannelSftp.LsEntry item : fileAndFolderList) {
-
-			String sourcePathPathFile = sourcePath + pathSeparator + item.getFilename();
-			String destinationPathFile = destinationPath + pathSeparator + item.getFilename();
-
-			if (!item.getAttrs().isDir()) {
+	public void recursiveDownloadFolder(String sourcePath, File destDir) throws JschRuntimeException {
+		String fileName;
+		String srcFile;
+		File destFile;
+		for (LsEntry item : lsEntries(sourcePath)) {
+			fileName = item.getFilename();
+			srcFile = StrUtil.format("{}/{}", sourcePath, fileName);
+			destFile = FileUtil.file(destDir, fileName);
+
+			if (false == item.getAttrs().isDir()) {
 				// 本地不存在文件或者ftp上文件有修改则下载
 				// 本地不存在文件或者ftp上文件有修改则下载
-				if (!FileUtil.exist(destinationPathFile)
-						|| (item.getAttrs().getMTime() > (FileUtil.lastModifiedTime(destinationPathFile).getTime() / 1000))) {
-					// Download file from source (source filename, destination filename).
-					channel.get(sourcePathPathFile, destinationPathFile);
+				if (false == FileUtil.exist(destFile)
+						|| (item.getAttrs().getMTime() > (destFile.lastModified() / 1000))) {
+					download(srcFile, destFile);
 				}
 				}
-			} else if (!(".".equals(item.getFilename()) || "..".equals(item.getFilename()))) {
-				FileUtil.mkdir(destinationPathFile);
-				recursiveDownloadFolder(sourcePathPathFile, destinationPathFile);
+			} else {
+				// 服务端依旧是目录,继续递归
+				FileUtil.mkdir(destFile);
+				recursiveDownloadFolder(srcFile, destFile);
 			}
 			}
 		}
 		}
 
 
@@ -453,7 +485,7 @@ public class Sftp extends AbstractFtp {
 	/**
 	/**
 	 * 获取远程文件
 	 * 获取远程文件
 	 *
 	 *
-	 * @param src 远程文件路径
+	 * @param src  远程文件路径
 	 * @param dest 目标文件路径
 	 * @param dest 目标文件路径
 	 * @return this
 	 * @return this
 	 */
 	 */
@@ -485,14 +517,19 @@ public class Sftp extends AbstractFtp {
 	 * JSch支持的三种文件传输模式
 	 * JSch支持的三种文件传输模式
 	 *
 	 *
 	 * @author looly
 	 * @author looly
-	 *
 	 */
 	 */
 	public enum Mode {
 	public enum Mode {
-		/** 完全覆盖模式,这是JSch的默认文件传输模式,即如果目标文件已经存在,传输的文件将完全覆盖目标文件,产生新的文件。 */
+		/**
+		 * 完全覆盖模式,这是JSch的默认文件传输模式,即如果目标文件已经存在,传输的文件将完全覆盖目标文件,产生新的文件。
+		 */
 		OVERWRITE,
 		OVERWRITE,
-		/** 恢复模式,如果文件已经传输一部分,这时由于网络或其他任何原因导致文件传输中断,如果下一次传输相同的文件,则会从上一次中断的地方续传。 */
+		/**
+		 * 恢复模式,如果文件已经传输一部分,这时由于网络或其他任何原因导致文件传输中断,如果下一次传输相同的文件,则会从上一次中断的地方续传。
+		 */
 		RESUME,
 		RESUME,
-		/** 追加模式,如果目标文件已存在,传输的文件将在目标文件后追加。 */
+		/**
+		 * 追加模式,如果目标文件已存在,传输的文件将在目标文件后追加。
+		 */
 		APPEND
 		APPEND
 	}
 	}
 }
 }

+ 8 - 9
hutool-extra/src/test/java/cn/hutool/extra/ftp/FtpTest.java

@@ -1,14 +1,13 @@
 package cn.hutool.extra.ftp;
 package cn.hutool.extra.ftp;
 
 
-import java.util.List;
-
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.lang.Console;
 import cn.hutool.extra.ssh.Sftp;
 import cn.hutool.extra.ssh.Sftp;
 import org.junit.Ignore;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.Test;
 
 
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.lang.Console;
+import java.util.List;
 
 
 public class FtpTest {
 public class FtpTest {
 
 
@@ -63,21 +62,21 @@ public class FtpTest {
 
 
 	@Test
 	@Test
 	@Ignore
 	@Ignore
-	public void recursiveDownloadFolder() throws Exception {
+	public void recursiveDownloadFolder() {
 		Ftp ftp = new Ftp("looly.centos");
 		Ftp ftp = new Ftp("looly.centos");
-		ftp.recursiveDownloadFolder("/","d:/test/download");
+		ftp.recursiveDownloadFolder("/",FileUtil.file("d:/test/download"));
 
 
 		IoUtil.close(ftp);
 		IoUtil.close(ftp);
 	}
 	}
 
 
 	@Test
 	@Test
 	@Ignore
 	@Ignore
-	public void recursiveDownloadFolderSftp() throws Exception {
+	public void recursiveDownloadFolderSftp() {
 		Sftp ftp = new Sftp("127.0.0.1", 22, "test", "test");
 		Sftp ftp = new Sftp("127.0.0.1", 22, "test", "test");
 
 
 		ftp.cd("/file/aaa");
 		ftp.cd("/file/aaa");
 		Console.log(ftp.pwd());
 		Console.log(ftp.pwd());
-		ftp.recursiveDownloadFolder("/","d:/test/download");
+		ftp.recursiveDownloadFolder("/",FileUtil.file("d:/test/download"));
 
 
 		IoUtil.close(ftp);
 		IoUtil.close(ftp);
 	}
 	}