声明:本工具类中所有的路径,均采用绝对路径。
声明:不论FTP是否限制chroot_local_user、chroot_list_enable,本工具类均适用(即:不论FTP
用户刚进FTP时pwd得到的是“/”还是形如“/var/ftp/user_a”这样的,本工具类都适用)。
目录
指定目录下所有文件下载(不包括:该目录下的文件夹及其内部内容)测试
指定目录下所有文件、文件夹下载(还包括:该目录下的文件夹及其内部内容)测试
测试样例(图片版)
声明:本人的FTP搭建在虚拟机CentOS7上,在本地(Windows)上进行的程序测试。
上传测试
注意:.uploadFile(String remoteDir, String remoteFileName, File file)中,remoteDir统一写(以“/”分割的)
绝对路径;remoteDir可以是不存在的目录(会自动创建);更多详情见代码注释!
运行测试方法,可看到上传成功(本人直接以浏览器浏览FTP):
多次上传,得到现有目录:
说明:/abc/下有a、b、c这三个目录(每个目录下都有各自的文件);还有My备忘录.txt、JustryDeng.html、
nonsuffixFile这三个文件了。
由此可见:上传成功!
单文件下载测试:
注:.downloadFile(String remoteDirOrRemoteFile, String localDir)中,remoteDirOrRemoteFile统一
写(以“/”分割的)绝对路径;localDir可以是不存在的目录(会自动创建);更多详情见代码注释!
注:如果remoteDirOrRemoteFile不存在,那么不会下载下来任何东西。
运行测试方法,可看到下载成功:
注:下载下来的文件的内容也是正确无乱码的。
注:.recursiveDownloadFile(String remoteDirOrRemoteFile, String localDir)方法也支持单文件下载。
由此可见:单文件下载成功!
指定目录下所有文件下载(不包括:该目录下的文件夹及其内部内容)测试:
注:.downloadFile(String remoteDirOrRemoteFile, String localDir)中remoteDirOrRemoteFile统
一写(以“/”分割的)绝对路径;localDir可以是不存在的目录(会自动创建);更多详情见代码注释!
运行测试方法,可看到下载成功:
注:可以看到,只把FTP 的/abc/目录下的文件下载下来了,并没有下载/abc/目录下的a、b、c文件夹及里面的内容。
由此可见:指定目录下所有文件下载成功!
指定目录下所有文件、文件夹下载(还包括:该目录下的文件夹及其内部内容)测试:
注:.recursiveDownloadFile(String remoteDirOrRemoteFile, String localDir)中,
remoteDirOrRemoteFile统一写(以“/”分割的)绝对路径;localDir可以是不存在
的目录(会自动创建);更多详情见代码注释!
运行测试方法,可看到下载成功:
注:可以看到,不仅把FTP 的/abc/目录下的有My备忘录.txt、JustryDeng.html、nonsuffixFile这三个文件下载下
来了,还把/abc/目录下的a、b、c文件夹(以及每个文件夹里面的文件、子文件夹)都下载下来了。
由此可见:指定目录下所有文件、文件夹(递归)下载成功!
单文件删除测试:
注:.deleteBlankDirOrFile(String deletedBlankDirOrFile)中,deletedBlankDirOrFile统一写(以“/”分割的)
绝对路径,可以是一个明确的要删除的文件全路径;也可以是一个空的文件夹路径;更多详情见代码注释!
运行测试方法,可看到删除成功:
注:可以看到,FTP 的/test/目录下的JustryDeng.html文件已经被删除了。
由此可见:单文件删除成功!
指定目录删除(包括:删除该目录下的文件夹及其内部内容)测试:
删除前,FTP是有/abc/目录的,且其内容为:
调用FTP工具类删除:
运行测试方法,可以看见:
FTP目录下找不到abc目录了,即:删除abc文件夹(及里面的内容)成功!
由此可见:递归删除文件夹(及里面的文件、文件夹)成功!
正文 > 工具类代码(文字版)
准备工作:在pom.xml中引入依赖
<!--
Apache Commons Net library contains a collection of network utilities
and protocol implementations. Supported protocols include: Echo, Finger,
FTP, NNTP, NTP, POP3(S), SMTP(S), Telnet, Whois
-->
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
FTP工具类
import org.apache.commons.net.ftp.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* FTP上传、下载、删除 工具类
* 约定:统一使用绝对路径
*
* @author JustryDeng
* @date 2018年9月26日 上午10:38:46
*/
public class FtpUtil {
private final Logger LOGGER = LoggerFactory.getLogger(FtpUtil.class);
/** 路径分隔符 */
private final String SEPARATOR_STR = "/";
/** 点 */
private final String DOT_STR = ".";
/** ftp服务器地址 */
private String hostname;
/** 端口号 */
private Integer port;
/** ftp登录账号 */
private String username;
/** ftp登录密码 */
private String password;
/**
* 命令 语句 编码(控制发出去的命令的编码)
* 如:在删除时,发出去的指令由于此处的编码不对应的原因,乱码了;(找不到目标文件)导致删除失败
* 如:在下载时,发出去的指令由于此处的编码不对应的原因,乱码了;(找不到目标文件)导致下载失败
* 如:在上传时,发出去的指令由于此处的编码不对应的原因,乱码了;导致上传到FTP的文件的文件名乱码
*
* 注:根据不同的(Server/Client)情况,这里灵活设置
*/
private String sendCommandStringEncoding = "UTF-8";
/**
* 下载文件,文件名encode编码
*
* 注:根据不同的(Server/Client)情况,这里灵活设置
*/
private String downfileNameEncodingParam1 = "UTF-8";
/**
* 下载文件,文件名decode编码
*
* 注:根据不同的(Server/Client)情况,这里灵活设置
*/
private String downfileNameDecodingParam2 = "UTF-8";
/**
* 设置文件传输形式(使用FTP类静态常量赋值即可)
*
* 注:根据要下载上传的文件情况,这里灵活设置
*/
private Integer transportFileType = FTP.BINARY_FILE_TYPE;
/**
* 用户FTP对应的根目录, 形如 /var/ftpusers/justry_deng_root
* 注:初始化之后,其值不可能为 null
*
*/
private String userRootDir;
/** FTP客户端 */
private FTPClient ftpClient;
private FtpUtil(String hostname, Integer port, String username, String password) {
super();
this.hostname = hostname;
this.port = port;
this.username = username;
this.password = password;
}
/**
* 设置下载时,文件名的编码
* 即:new String(file.getName().getBytes(param1), param2) 中的param1
* 注:根据不同的(Server/Client)情况,这里灵活设置
*
* @date 2018年9月26日 下午7:34:26
*/
public void setDownfileNameEncodingParam1(String downfileNameEncodingParam1) {
this.downfileNameEncodingParam1 = downfileNameEncodingParam1;
}
/**
* 设置下载时,文件名的编码
* 即:new String(file.getName().getBytes(param1), param2) 中的param2
* 注:根据不同的(Server/Client)情况,这里灵活设置
*
* @date 2018年9月26日 下午7:34:26
*/
public void setDownfileNameDecodingParam2(String downfileNameDecodingParam2) {
this.downfileNameDecodingParam2 = downfileNameDecodingParam2;
}
/**
* 设置文件传输形式 -> 二进制
* 根据自己的时机情况,选择FTP.BINARY_FILE_TYPE或FTP.ASCII_FILE_TYPE等即可
* 注:根据不同的文件情况,这里灵活设置
*
* @date 2018年9月27日 上午9:48:51
*/
public void setTransportFileType(Integer transportFileType) {
if( transportFileType != null) {
this.transportFileType = transportFileType;
}
}
/**
* FTP的上传、下载、删除,底层还是 发送得命令语句; 这里就设置发送的命令语句的编码
* 如:在删除时,发出去的指令由于此处的编码不对应的原因,乱码了;(找不到目标文件)导致删除失败
* 如:在下载时,发出去的指令由于此处的编码不对应的原因,乱码了;(找不到目标文件)导致下载失败
* 如:在上传时,发出去的指令由于此处的编码不对应的原因,乱码了;导致上传到FTP的文件的文件名乱码
*
* Saves the character encoding to be used by the FTP control connection.
* Some FTP servers require that commands be issued in a non-ASCII
* encoding like UTF-8 so that filenames with multi-byte character
* representations (e.g, Big 8) can be specified.
*/
public void setSendCommandStringEncoding(String sendCommandStringEncoding) {
this.sendCommandStringEncoding = sendCommandStringEncoding;
}
/**
* @param hostname
* FTPServer ip
* @param port
* FTPServer 端口
* @param username
* 用户名
* @param password
* 密码
* @return FtpUtil实例
* @date 2018年9月26日 下午4:39:02
*/
public static FtpUtil getFtpUtilInstance(String hostname, Integer port, String username, String password) {
return new FtpUtil(hostname, port, username, password);
}
/**
* 初始化FTP服务器
*
* @throws IOException IO异常
* @date 2018年9月26日 下午1:37:14
*/
private void initFtpClient() throws IOException {
if(ftpClient == null){
ftpClient = new FTPClient();
}
// 设置编码(其是影响是否乱码的主要因素,在ftpClient.connect(hostname, port)连接前就要设置,否者不一定生效)
ftpClient.setControlEncoding(sendCommandStringEncoding);
// Returns the integer value of the reply code of the last FTP reply.
int replyCode = ftpClient.getReplyCode();
// 221表示 退出了网络,那么需要重新连接
int ftpDisconnectionStatusCode = 221;
// Determine if a reply code is a positive completion response.
// FTPReply.isPositiveCompletion(replyCode)可用于验证是否连接FTP
if (FTPReply.isPositiveCompletion(replyCode) && replyCode != ftpDisconnectionStatusCode) {
LOGGER.info(" FtpUtil -> alreadly connected FTPServer !");
return;
} else {
LOGGER.info(" FtpUtil -> connecting FTPServer -> {} : {}", this.hostname, this.port);
// 连接ftp服务器
ftpClient.connect(hostname, port);
// 登录ftp服务器
ftpClient.login(username, password);
LOGGER.info(" FtpUtil -> connect FTPServer success!");
// 初始化ftp用户根目录, 形如 /var/ftpusers/justry_deng_root
userRootDir = ftpClient.printWorkingDirectory();
if (userRootDir == null || "".equals(userRootDir.trim()) || SEPARATOR_STR.equals(userRootDir)) {
userRootDir = "";
}
}
// 设置文件传输形式
ftpClient.setFileType(transportFileType);
// 设置文件传输形式
ftpClient.setFileType(transportFileType);
// 设置FTP客户端(即本地)模式为被动模式
ftpClient.enterLocalPassiveMode();
/* TODO 此配置主要解决: 操作Linux下的FTP,.listFiles(xxx)方法不能获取到指定文件夹下 的 文件(夹)问题
* 引入此配置后,反而可能导致 操作Windows下的FTP时,.listFiles(xxx)方法不能获取到指定文件夹下 的 文件(夹),
* 所以,如果是操作Windows下的FTP,操作失败时,可考虑将 此配置注释掉
*/
// 由于apache不支持中文语言环境,通过定制类解析中文日期类型
ftpClient.configure(new FTPClientConfig("com.aspire.util.UnixFTPEntryParser"));
}
/**
* 上传文件至FTP
* 注:若有同名文件,那么原文件会被覆盖
*
* @param remoteDir
* 上传到指定目录(FTP用户pwd时的绝对路径)
* @param remoteFileName
* 上传到FTP,该文件的文件名
* @param file
* 要上传的本地文件
*
* @return 上传结果
* @throws IOException IO异常
* @date 2018年9月26日 下午1:35:27
*/
@SuppressWarnings("unused")
public boolean uploadFile(String remoteDir, String remoteFileName, File file) throws IOException{
boolean result;
remoteDir = handleRemoteDir(remoteDir);
try(InputStream inputStream = new FileInputStream(file)){
// 初始化
initFtpClient();
createDirecroty(remoteDir);
ftpClient.changeWorkingDirectory(remoteDir);
result = ftpClient.storeFile(remoteFileName, inputStream);
}
LOGGER.info(" FtpUtil -> uploadFile boolean result is -> {}", result);
return result;
}
/**
* 从FTP下载文件
* 注:如果remoteDirOrRemoteFile不存在,则不会下载下来任何东西
* 注:如果remoteDirOrRemoteFile不存在,localDir也不存在;再不会下载下来任何东西,
* 也不会在本地创建localDir目录
*
* @param remoteDirOrRemoteFile
* FTP中的某一个目录(此时下载该目录下的所有文件,该目录下的文件夹不会被下载);
* 或 FTP中的某一个文件全路径名(此时下载该文件)
* @param localDir
* 本地用于保存下载下来的文件的文件夹
*
* @return 下载了的文件个数
* @throws IOException IO异常
* @date 2018年9月26日 下午7:24:11
*/
public int downloadFile(String remoteDirOrRemoteFile, String localDir) throws IOException{
remoteDirOrRemoteFile = handleRemoteDir(remoteDirOrRemoteFile);
int successSum = 0;
int failSum = 0;
initFtpClient();
// 根据remoteDirOrRemoteFile是文件还是目录,来切换changeWorkingDirectory
if (!remoteDirOrRemoteFile.contains(DOT_STR)) {
// 切换至要下载的文件所在的目录,否者下载下来的文件大小为0
boolean flag = ftpClient.changeWorkingDirectory(remoteDirOrRemoteFile);
// 不排除那些 没有后缀名的文件 存在的可能;
// 如果切换至该目录失败,那么其可能是没有后缀名的文件,那么尝试着下载该文件
if (!flag) {
return downloadNonsuffixFile(remoteDirOrRemoteFile, localDir);
}
} else {
String tempWorkingDirectory;
int index = remoteDirOrRemoteFile.lastIndexOf(SEPARATOR_STR);
if (index > 0) {
tempWorkingDirectory = remoteDirOrRemoteFile.substring(0, index);
} else {
tempWorkingDirectory = SEPARATOR_STR;
}
// 切换至要下载的文件所在的目录,否者下载下来的文件大小为0
ftpClient.changeWorkingDirectory(tempWorkingDirectory);
}
File localFileDir = new File(localDir);
// 获取remoteDirOrRemoteFile目录下所有 文件以及文件夹 或 获取指定的文件
FTPFile[] ftpFiles = ftpClient.listFiles(remoteDirOrRemoteFile);
for (FTPFile file : ftpFiles) {
// 如果是文件夹,那么不下载 (因为:直接下载文件夹的话,是无效文件)
if (file.isDirectory()) {
continue;
}
//如果文件夹不存在则创建
if (!localFileDir.exists()) {
boolean result = localFileDir.mkdirs();
LOGGER.info(" {} is not exist, create this Dir! create result -> {}!",
localFileDir, result);
}
String name = new String(file.getName().getBytes(this.downfileNameEncodingParam1),
this.downfileNameDecodingParam2);
String tempLocalFile = localDir.endsWith(SEPARATOR_STR) ?
localDir + name :
localDir + SEPARATOR_STR + name;
File localFile = new File(tempLocalFile);
try (OutputStream os = new FileOutputStream(localFile)) {
boolean result = ftpClient.retrieveFile(file.getName(), os);
if (result) {
successSum++;
LOGGER.info(" already download normal file -> {}", name);
} else {
failSum++;
}
}
}
LOGGER.info(" FtpUtil -> downloadFile success download file total -> {}", successSum);
LOGGER.info(" FtpUtil -> downloadFile fail download file total -> {}", failSum);
return successSum;
}
/**
* downloadFile的升级版 -> 其功能如下:
* 1.remoteDirOrRemoteFile可为FTP上某一个文件的全路径名(绝对路径)
* -> 下载该文件,此处与downloadFile功能一致
*
* 2.remoteDirOrRemoteFile可为FTP上某一个文件目录名
* -> 下载该目录下的所有文件、文件夹(包括该文件夹中的所有文件文件夹并以此类推)
* 注:对比downloadFile方法可知,downloadFile只能下载该目录下的所有文件,不能递归下载
*
* @date 2018年9月26日 下午7:26:22
*/
public int recursiveDownloadFile(String remoteDirOrRemoteFile, String localDir) throws IOException {
remoteDirOrRemoteFile = handleRemoteDir(remoteDirOrRemoteFile);
int successSum = 0;
// remoteDirOrRemoteFile是一个明确的文件 还是 一个目录
if (remoteDirOrRemoteFile.contains(DOT_STR)) {
successSum = downloadFile(remoteDirOrRemoteFile, localDir);
} else {
/// 初步组装数据,调用递归方法;查询给定FTP目录以及其所有子孙目录,进而得到FTP目录与本地目录的对应关系Map
// 有序存放FTP remote文件夹路径
// 其实逻辑是:先往alreadyQueriedDirList里面存,再进行的查询。此处可以这么处理。
List<String> alreadyQueryDirList = new ArrayList<>(16);
alreadyQueryDirList.add(remoteDirOrRemoteFile);
// 有序存放FTP remote文件夹路径
List<String> requiredQueryDirList = new ArrayList<>(16);
requiredQueryDirList.add(remoteDirOrRemoteFile);
// 记录FTP目录与 本地目录对应关系
Map<String, String> storeDataMap = new HashMap<>(16);
storeDataMap.put(remoteDirOrRemoteFile, localDir);
queryFTPAllChildrenDirectory(storeDataMap, alreadyQueryDirList, requiredQueryDirList);
String tempPath;
// 循环调用downloadFile()方法,进行嵌套下载
for(String str : alreadyQueryDirList) {
// 将FTP用户的pwd的绝对路径转换为,用户输入的路径(因为 downloadFile方法会将用户输入的路径转化为pwd路径)
// 提示:用户刚登陆进FTP时,输入pwd,得到的不一定是"/",有可能时FTP对应的Linux上的文件夹路径,
// 这与FTP的设置有关,可详见《程序员成长笔记(四)》搭建FTP服务器相关章节
tempPath = str.length() > userRootDir.length() ?
str.substring(userRootDir.length()) :
SEPARATOR_STR;
int thiscount = downloadFile(tempPath, storeDataMap.get(str));
successSum += thiscount;
}
}
System.out.println(" FtpUtil -> recursiveDownloadFile(excluded created directories) "
+ " success download file total -> " + successSum);
return successSum;
}
/**
* 删除文件 或 删除空的文件夹
* 注:删除不存在的目录或文件 会导致删除失败
* 注: 出于保护措施,输入的“/”时,不可删除,直接返回false
*
* @param deletedBlankDirOrFile
* 要删除的文件的全路径名 或 要删除的空文件夹全路径名
* 统一:路径分割符 用“/”,而不用“\”;
*
* @return 删除成功与否
* @throws IOException IO异常
* @date 2018年9月26日 下午9:12:07
*/
public boolean deleteBlankDirOrFile(String deletedBlankDirOrFile) throws IOException{
if(deletedBlankDirOrFile == null || SEPARATOR_STR.equals(deletedBlankDirOrFile)) {
return false;
}
deletedBlankDirOrFile = handleRemoteDir(deletedBlankDirOrFile);
boolean flag;
initFtpClient();
// 根据remoteDirOrRemoteFile是文件还是目录,来切换changeWorkingDirectory
if (deletedBlankDirOrFile.lastIndexOf(DOT_STR) < 0) {
// 出于保护机制:如果当前文件夹中是空的,那么才能删除成功
flag = ftpClient.removeDirectory(deletedBlankDirOrFile);
// 不排除那些 没有后缀名的文件 存在的可能;
// 如果删除空文件夹失败,那么其可能是没有后缀名的文件,那么尝试着删除文件
if (!flag) {
flag = ftpClient.deleteFile(deletedBlankDirOrFile);
}
// 如果是文件,那么直接删除该文件
} else {
String tempWorkingDirectory;
int index = deletedBlankDirOrFile.lastIndexOf(SEPARATOR_STR);
if (index > 0) {
tempWorkingDirectory = deletedBlankDirOrFile.substring(0, index);
} else {
tempWorkingDirectory = SEPARATOR_STR;
}
// 切换至要下载的文件所在的目录,否者下载下来的文件大小为0
ftpClient.changeWorkingDirectory(tempWorkingDirectory);
flag = ftpClient.deleteFile(deletedBlankDirOrFile.substring(index + 1));
}
LOGGER.info(" FtpUtil -> deleteBlankDirOrFile [{}] boolean result is -> {}",
deletedBlankDirOrFile, flag);
return flag;
}
/**
* deleteBlankDirOrFile的加强版 -> 可删除文件、空文件夹、非空文件夹
* 注: 出于保护措施,输入的“/”时,不可删除,直接返回false
*
* @param deletedBlankDirOrFile
* 要删除的文件路径或文件夹
*
* @return 删除成功与否
* @throws IOException IO异常
* @date 2018年9月27日 上午1:25:16
*/
public boolean recursiveDeleteBlankDirOrFile(String deletedBlankDirOrFile) throws IOException{
if(deletedBlankDirOrFile == null || SEPARATOR_STR.equals(deletedBlankDirOrFile)) {
return false;
}
String realDeletedBlankDirOrFile = handleRemoteDir(deletedBlankDirOrFile);
boolean result = true;
initFtpClient();
if(!destDirExist(realDeletedBlankDirOrFile)) {
LOGGER.info(" {} maybe is a non-suffix file!, try delete!", realDeletedBlankDirOrFile);
boolean flag = deleteBlankDirOrFile(deletedBlankDirOrFile);
String flagIsTrue = " FtpUtil -> recursiveDeleteBlankDirOrFile "
+ realDeletedBlankDirOrFile + " -> success!";
String flagIsFalse = " FtpUtil -> recursiveDeleteBlankDirOrFile "
+ realDeletedBlankDirOrFile + " -> target file is not exist!";
LOGGER.info(flag ? flagIsTrue : flagIsFalse);
return true;
}
// remoteDirOrRemoteFile是一个明确的文件 还是 一个目录
if (realDeletedBlankDirOrFile.contains(DOT_STR) || !ftputilsChangeWorkingDirectory(realDeletedBlankDirOrFile)) {
result = deleteBlankDirOrFile(deletedBlankDirOrFile);
} else {
/// 初步组装数据,调用递归方法;查询给定FTP目录以及其所有子孙目录、子孙文件 (含其自身)
// 存放 文件夹路径
// 其实逻辑是:先往alreadyQueriedDirList里面存,再进行的查询。此处可以这么处理。
List<String> alreadyQueriedDirList = new ArrayList<>(16);
alreadyQueriedDirList.add(realDeletedBlankDirOrFile);
// 存放 文件路径
List<String> alreadyQueriedFileList = new ArrayList<>(16);
// 存放 文件夹路径
List<String> requiredQueryDirList = new ArrayList<>(16);
requiredQueryDirList.add(realDeletedBlankDirOrFile);
queryAllChildrenDirAndChildrenFile(alreadyQueriedDirList,
alreadyQueriedFileList,
requiredQueryDirList);
String tempPath;
// 循环调用deleteBlankDirOrFile()方法,删除文件
for (String filePath : alreadyQueriedFileList) {
tempPath = filePath.length() > userRootDir.length() ?
filePath.substring(userRootDir.length()) :
SEPARATOR_STR;
deleteBlankDirOrFile(tempPath);
}
// 对alreadyQueriedDirList进行排序,以保证等下删除时,先删除的空文件夹是 最下面的
String[] alreadyQueriedDirArray = new String[alreadyQueriedDirList.size()];
alreadyQueriedDirArray = alreadyQueriedDirList.toArray(alreadyQueriedDirArray);
sortArray(alreadyQueriedDirArray);
// 循环调用deleteBlankDirOrFile()方法,删除空的文件夹
for (String str : alreadyQueriedDirArray) {
tempPath = str.length() > userRootDir.length() ?
str.substring(userRootDir.length()) :
SEPARATOR_STR;
boolean isSuccess = deleteBlankDirOrFile(tempPath);
if (!isSuccess) {
result = false;
}
}
}
LOGGER.info(" FtpUtil -> recursiveDeleteBlankDirOrFile {} boolean result is -> {}",
realDeletedBlankDirOrFile, result);
return result;
}
/**
* 根据数组元素的长度,来进行排序(字符串长的,排在前面)
* 数组元素不能为null
*
* @date 2018年9月27日 上午12:54:03
*/
private void sortArray(String[] array) {
for (int i = 0; i < array.length - 1; i++) {
for(int j = 0; j < array.length - 1 - i; j++) {
if (array[j].length() - array[j+1].length() < 0) {
String flag=array[j];
array[j] = array[j+1];
array[j+1] = flag;
}
}
}
}
/**
*
* 根据给出的FTP目录、对应本地目录; 查询该FTP目录的所有子目录 , 以及获得与每一个子
* 目录对应的本地目录(含其自身以及与其自身对应的本地目录)
*
* @param storeDataMap
* 存储FTP目录与本地目录的对应关系;key -> FTP目录, value -> 与key对应的本地目录
* @param alreadyQueriedDirList
* 所有已经查询过了的FTP目录,即:key集合
* @param requiredQueryDirList
* 还需要查询的FTP目录
*
* @throws IOException IO异常
* @date 2018年9月26日 下午7:17:52
*/
private void queryFTPAllChildrenDirectory(Map<String, String> storeDataMap,
List<String> alreadyQueriedDirList,
List<String> requiredQueryDirList) throws IOException {
List<String> newRequiredQueryDirList = new ArrayList<>(16);
if(requiredQueryDirList.size() == 0) {
return;
}
for (String str : requiredQueryDirList) {
String rootLocalDir = storeDataMap.get(str);
// 获取rootRemoteDir目录下所有 文件以及文件夹(或 获取指定的文件)
FTPFile[] ftpFiles = ftpClient.listFiles(str);
for(FTPFile file : ftpFiles){
if (file.isDirectory()) {
String tempName = file.getName();
String ftpChildrenDir = str.endsWith(SEPARATOR_STR) ?
str + tempName :
str + SEPARATOR_STR + tempName;
String localChildrenDir = rootLocalDir.endsWith(SEPARATOR_STR) ?
rootLocalDir + tempName :
rootLocalDir + SEPARATOR_STR + tempName;
alreadyQueriedDirList.add(ftpChildrenDir);
newRequiredQueryDirList.add(ftpChildrenDir);
storeDataMap.put(ftpChildrenDir, localChildrenDir);
}
}
}
this.queryFTPAllChildrenDirectory(storeDataMap, alreadyQueriedDirList, newRequiredQueryDirList);
}
/**
* 根据给出的FTP目录,查询其所有子目录以及子文件(含其自身)
*
* @param alreadyQueriedDirList
* 所有已经查询出来了的目录
* @param alreadyQueriedFileList
* 所有已经查询出来了的文件
* @param requiredQueryDirList
* 还需要查询的FTP目录
* @throws IOException IO异常
* @date 2018年9月27日 上午12:12:53
*/
private void queryAllChildrenDirAndChildrenFile(List<String> alreadyQueriedDirList,
List<String> alreadyQueriedFileList,
List<String> requiredQueryDirList) throws IOException {
List<String> newRequiredQueryDirList = new ArrayList<>(16);
if (requiredQueryDirList.size() == 0) {
return;
}
initFtpClient();
for (String dirPath : requiredQueryDirList) {
// 获取dirPath目录下所有 文件以及文件夹(或 获取指定的文件)
FTPFile[] ftpFiles = ftpClient.listFiles(dirPath);
for (FTPFile file : ftpFiles) {
String tempName = file.getName();
String ftpChildrenName = dirPath.endsWith(SEPARATOR_STR) ?
dirPath + tempName :
dirPath + SEPARATOR_STR + tempName;
if (file.isDirectory()) {
alreadyQueriedDirList.add(ftpChildrenName);
newRequiredQueryDirList.add(ftpChildrenName);
} else {
alreadyQueriedFileList.add(ftpChildrenName);
}
}
}
this.queryAllChildrenDirAndChildrenFile(alreadyQueriedDirList, alreadyQueriedFileList, newRequiredQueryDirList);
}
/**
* 创建指定目录(注:如果要创建的目录已经存在,那么返回false)
*
* @param dir
* 目录路径,绝对路径,如: /abc 或 /abc/ 可以
* 相对路径,如: sss 或 sss/ 也可以
* 注:相对路径创建的文件夹所在位置时,相对于当前session所处目录位置。
* 提示: .changeWorkingDirectory() 可切换当前session所处目录位置
* @return 创建成功与否
* @throws IOException IO异常
* @date 2018年9月26日 下午3:42:20
*/
private boolean makeDirectory(String dir) throws IOException {
boolean flag;
flag = ftpClient.makeDirectory(dir);
if (flag) {
LOGGER.info(" FtpUtil -> makeDirectory -> create Dir [{}] success!", dir);
} else {
LOGGER.info(" FtpUtil -> makeDirectory -> create Dir [{}] fail!", dir);
}
return flag;
}
/**
* 在FTP服务器上创建remoteDir目录(不存在,则创建;存在,则不创建)
*
* @param directory
* 要创建的目录
* 注:此目录指的是FTP用户pwd时获取到的目录(这也与FTP服务器设置是否允许用户切出上级目录有关)
* 注:pwdRemoteDir不能为null或“”,pwdRemoteDir必须是绝对路径
*
* @throws IOException IO异常
* @date 2018年9月26日 下午2:19:37
*/
private void createDirecroty(String directory) throws IOException {
if (!directory.equals(userRootDir) && !ftpClient.changeWorkingDirectory(directory)) {
if (!directory.endsWith(SEPARATOR_STR)) {
directory = directory + SEPARATOR_STR;
}
// 获得每一个节点目录的起始位置
int start = userRootDir.length() + 1;
int end = directory.indexOf(SEPARATOR_STR, start);
// 循环创建目录
String dirPath = userRootDir;
String subDirectory;
boolean result;
while (end >= 0) {
subDirectory = directory.substring(start, end);
dirPath = dirPath + SEPARATOR_STR + subDirectory;
if (!ftpClient.changeWorkingDirectory(dirPath)) {
result = makeDirectory(dirPath);
LOGGER.info(" FtpUtil -> createDirecroty -> invoke makeDirectory got retrun -> {}!", result);
}
start = end + 1;
end = directory.indexOf(SEPARATOR_STR, start);
}
}
}
/**
* 避免在代码中频繁 initFtpClient、logout、disconnect;
* 这里包装一下FTPClient的.changeWorkingDirectory(String pathname)方法
*
* @param pathname
* 要切换(session)到FTP的哪一个目录下
* @date 2018年9月27日 上午11:24:25
*/
private boolean ftputilsChangeWorkingDirectory(String pathname) throws IOException{
boolean result;
initFtpClient();
result = ftpClient.changeWorkingDirectory(pathname);
return result;
}
/**
* 判断FTP上某目录是否存在
*
* @param pathname
* 要判断的路径(文件名全路径、文件夹全路径都可以)
* 注:此路径应从根目录开始
* @date 2018年9月27日 上午11:24:25
*/
private boolean destDirExist(String pathname) throws IOException{
boolean result;
if (pathname.contains(DOT_STR)) {
int index = pathname.lastIndexOf(SEPARATOR_STR);
if (index != 0) {
pathname = pathname.substring(0, index);
} else {
return true;
}
}
result = ftpClient.changeWorkingDirectory(pathname);
return result;
}
/**
* 处理用户输入的 FTP路径
* 注:这主要是为了 兼容 FTP(对是否允许用户切换到上级目录)的设置
*
* @param remoteDirOrFile
* 用户输入的FTP路径
* @return 处理后的路径
* @date 2019/2/1 14:00
*/
private String handleRemoteDir(String remoteDirOrFile) throws IOException {
initFtpClient();
if(remoteDirOrFile == null
|| "".equals(remoteDirOrFile.trim())
|| SEPARATOR_STR.equals(remoteDirOrFile)) {
remoteDirOrFile = userRootDir + SEPARATOR_STR;
} else if(remoteDirOrFile.startsWith(SEPARATOR_STR)) {
remoteDirOrFile = userRootDir + remoteDirOrFile;
} else {
remoteDirOrFile = userRootDir + SEPARATOR_STR + remoteDirOrFile;
}
return remoteDirOrFile;
}
/**
* 下载 无后缀名的文件
*
* @param remoteDirOrFile
* 经过handleRemoteDir()方法处理后的 FTP绝对路径
*
* @return 成功条数
* @throws IOException IO异常
* @date 2019/2/1 14:23
*/
private int downloadNonsuffixFile(String remoteDirOrFile, String localDir) throws IOException {
int successSum = 0;
int failSum = 0;
File localFileDir = new File(localDir);
String tempWorkingDirectory;
String tempTargetFileName;
int index = remoteDirOrFile.lastIndexOf(SEPARATOR_STR);
tempTargetFileName = remoteDirOrFile.substring(index + 1);
if(tempTargetFileName.length() > 0) {
if (index > 0) {
tempWorkingDirectory = remoteDirOrFile.substring(0, index);
}else {
tempWorkingDirectory = SEPARATOR_STR;
}
ftpClient.changeWorkingDirectory(tempWorkingDirectory);
// 获取tempWorkingDirectory目录下所有 文件以及文件夹 或 获取指定的文件
FTPFile[] ftpFiles = ftpClient.listFiles(tempWorkingDirectory);
for(FTPFile file : ftpFiles){
String name = new String(file.getName().getBytes(this.downfileNameEncodingParam1),
this.downfileNameDecodingParam2);
// 如果不是目标文件,那么不下载
if(!tempTargetFileName.equals(name)) {
continue;
}
//如果文件夹不存在则创建
if (!localFileDir.exists()) {
boolean result = localFileDir.mkdirs();
LOGGER.info(" {} is not exist, create this Dir! create result -> {}!",
localFileDir, result);
}
String tempLocalFile = localDir.endsWith(SEPARATOR_STR) ?
localDir + name :
localDir + SEPARATOR_STR + name;
File localFile = new File(tempLocalFile);
try (OutputStream os = new FileOutputStream(localFile)) {
boolean result = ftpClient.retrieveFile(file.getName(), os);
if (result) {
successSum++;
LOGGER.info(" already download nonsuffixname file -> {}", name);
} else {
failSum++;
}
}
LOGGER.info(" FtpUtil -> downloadFile success download item count -> {}", successSum);
LOGGER.info(" FtpUtil -> downloadFile fail download item count -> {}", failSum);
}
}
return successSum;
}
/**
* 释放资源
*
* 注:考虑到 递归下载、递归删除,如果将释放资源写在下载、删除逻辑里面,那么
* 当文件较多时,就不频繁的连接FTP、断开FTP;那么久非常影响效率;
* 所以干脆提供一个方法,在使用FTP结束后,需要主动调用此方法 释放资源
*
*
*/
public void releaseResource() throws IOException {
if (ftpClient == null) {
return;
}
try {
ftpClient.logout();
} catch (IOException e) {
// 连接未打开的话
// 忽略 java.io.IOException: Connection is not open
}
if (ftpClient.isConnected()) {
ftpClient.disconnect();
}
ftpClient = null;
}
}
辅助类FTPTimestampParserImplExZH:
import org.apache.commons.net.ftp.parser.FTPTimestampParserImpl;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* 解决apache ftp中文语言环境下,FTPClient.listFiles()为空的bug
*
* @author JustryDeng
* @date 2019/2/1 15:36
*/
@SuppressWarnings("all")
class FTPTimestampParserImplExZH extends FTPTimestampParserImpl {
private SimpleDateFormat defaultDateFormat = new SimpleDateFormat("mm d hh:mm");
private SimpleDateFormat recentDateFormat = new SimpleDateFormat("yyyy mm d");
/**
* @author hzwei206 将中文环境的时间格式进行转换
*/
private String formatDate_Zh2En(String timeStrZh) {
if (timeStrZh == null) {
return "";
}
int len = timeStrZh.length();
StringBuffer sb = new StringBuffer(len);
char ch = ' ';
for (int i = 0; i < len; i++) {
ch = timeStrZh.charAt(i);
if ((ch >= '0' && ch <= '9') || ch == ' ' || ch == ':') {
sb.append(ch);
}
}
return sb.toString();
}
/**
* Implements the one { FTPTimestampParser#parseTimestamp(String) method} in the { FTPTimestampParser
* FTPTimestampParser} interface according to this algorithm: If the recentDateFormat member has been defined, try
* to parse the supplied string with that. If that parse fails, or if the recentDateFormat member has not been
* defined, attempt to parse with the defaultDateFormat member. If that fails, throw a ParseException.
*
* @see org.apache.commons.net.ftp.parser.FTPTimestampParser#parseTimestamp(String)
*/
@Override
public Calendar parseTimestamp(String timestampStr) throws ParseException {
timestampStr = formatDate_Zh2En(timestampStr);
Calendar now = Calendar.getInstance();
now.setTimeZone(this.getServerTimeZone());
Calendar working = Calendar.getInstance();
working.setTimeZone(this.getServerTimeZone());
ParsePosition pp = new ParsePosition(0);
Date parsed = null;
if (this.recentDateFormat != null) {
parsed = recentDateFormat.parse(timestampStr, pp);
}
if (parsed != null && pp.getIndex() == timestampStr.length()) {
working.setTime(parsed);
working.set(Calendar.YEAR, now.get(Calendar.YEAR));
if (working.after(now)) {
working.add(Calendar.YEAR, -1);
}
} else {
pp = new ParsePosition(0);
parsed = defaultDateFormat.parse(timestampStr, pp);
// note, length checks are mandatory for us since
// SimpleDateFormat methods will succeed if less than
// full string is matched. They will also accept,
// despite "leniency" setting, a two-digit number as
// a valid year (e.g. 22:04 will parse as 22 A.D.)
// so could mistakenly confuse an hour with a year,
// if we don't insist on full length parsing.
if (parsed != null && pp.getIndex() == timestampStr.length()) {
working.setTime(parsed);
} else {
throw new ParseException("Timestamp could not be parsed with older or recent DateFormat", pp.getIndex());
}
}
return working;
}
}
辅助类UnixFTPEntryParser:
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.parser.ConfigurableFTPFileEntryParserImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.util.Calendar;
/**
* common-net-1.4.1.jar源码,修改对于日期中文格式的支持,从而解决FTPClient.listFiles()返回为空问题
*/
@SuppressWarnings("all")
public class UnixFTPEntryParser extends ConfigurableFTPFileEntryParserImpl {
private static Logger logger = LoggerFactory.getLogger(UnixFTPEntryParser.class);
/**
* months abbreviations looked for by this parser. Also used
* to determine which month is matched by the parser
*/
private static final String DEFAULT_MONTHS =
"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)";
static final String DEFAULT_DATE_FORMAT
= "MMM d yyyy"; //Nov 9 2001
static final String DEFAULT_RECENT_DATE_FORMAT
= "MMM d HH:mm"; //Nov 9 20:06
static final String NUMERIC_DATE_FORMAT
= "yyyy-MM-dd HH:mm"; //2001-11-09 20:06
/**
* Some Linux distributions are now shipping an FTP server which formats
* file listing dates in an all-numeric format:
* <code>"yyyy-MM-dd HH:mm</code>.
* This is a very welcome development, and hopefully it will soon become
* the standard. However, since it is so new, for now, and possibly
* forever, we merely accomodate it, but do not make it the default.
* <p>
* For now end users may specify this format only via
* <code>UnixFTPEntryParser(FTPClientConfig)</code>.
* Steve Cohen - 2005-04-17
*/
public static final FTPClientConfig NUMERIC_DATE_CONFIG =
new FTPClientConfig(
FTPClientConfig.SYST_UNIX,
NUMERIC_DATE_FORMAT,
null, null, null, null);
/**
* this is the regular expression used by this parser.
* <p>
* Permissions:
* r the file is readable
* w the file is writable
* x the file is executable
* - the indicated permission is not granted
* L mandatory locking occurs during access (the set-group-ID bit is
* on and the group execution bit is off)
* s the set-user-ID or set-group-ID bit is on, and the corresponding
* user or group execution bit is also on
* S undefined bit-state (the set-user-ID bit is on and the user
* execution bit is off)
* t the 1000 (octal) bit, or sticky bit, is on [see chmod(1)], and
* execution is on
* T the 1000 bit is turned on, and execution is off (undefined bit-
* state)
*/
private static final String REGEX =
"([bcdlfmpSs-])"
+ "(((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-])))\\+?\\s+"
+ "(\\d+)\\s+"
+ "(\\S+)\\s+"
+ "(?:(\\S+)\\s+)?"
+ "(\\d+)\\s+"
/*
numeric or standard format date
*/
//问题出在此处,这个匹配只匹配2中形式:
//(1)2008-08-03
//(2)Jan 9或4月 26
//而出错的hp机器下的显示为 8月20日(没有空格分开)
//故无法匹配而报错
//将下面字符串改为:
+ "((?:\\d+[-/]\\d+[-/]\\d+)|(?:\\S+\\s+\\S+)|(?:\\S+))\\s+"
//+ "((?:\\d+[-/]\\d+[-/]\\d+)|(?:\\S+\\s+\\S+))\\s+"
/*
year (for non-recent standard format)
or time (for numeric or recent standard format
*/
+ "(\\d+(?::\\d+)?)\\s+"
+ "(\\S*)(\\s*.*)";
/**
* The default constructor for a UnixFTPEntryParser object.
*
* @throws IllegalArgumentException
* Thrown if the regular expression is unparseable. Should not be seen
* under normal conditions. It it is seen, this is a sign that
* <code>REGEX</code> is not a valid regular expression.
*/
public UnixFTPEntryParser() {
this(null);
}
/**
* This constructor allows the creation of a UnixFTPEntryParser object with
* something other than the default configuration.
*
* @param config
* The {@link FTPClientConfig configuration} object used to
* configure this parser.
* @throws IllegalArgumentException
* Thrown if the regular expression is unparseable. Should not be seen
* under normal conditions. It it is seen, this is a sign that
* <code>REGEX</code> is not a valid regular expression.
* @since 1.4
*/
public UnixFTPEntryParser(FTPClientConfig config) {
super(REGEX);
configure(config);
}
/**
* Parses a line of a unix (standard) FTP server file listing and converts
* it into a usable format in the form of an <code> FTPFile </code>
* instance. If the file listing line doesn't describe a file,
* <code> null </code> is returned, otherwise a <code> FTPFile </code>
* instance representing the files in the directory is returned.
* <p>
*
* @param entry
* A line of text from the file listing
* @return An FTPFile instance corresponding to the supplied entry
*/
@Override
public FTPFile parseFTPEntry(String entry) {
FTPFile file = new FTPFile();
file.setRawListing(entry);
int type;
boolean isDevice = false;
if (matches(entry)) {
String typeStr = group(1);
String hardLinkCount = group(15);
String usr = group(16);
String grp = group(17);
String filesize = group(18);
String datestr = group(19) + " " + group(20);
String name = group(21);
String endtoken = group(22);
try {
//file.setTimestamp(super.parseTimestamp(datestr));
FTPTimestampParserImplExZH Zh2En = new FTPTimestampParserImplExZH();
file.setTimestamp(Zh2En.parseTimestamp(datestr));
} catch (ParseException e) {
//logger.error(e, e);
//return null; // this is a parsing failure too.
//logger.info(entry+":修改日期重置为当前时间");
file.setTimestamp(Calendar.getInstance());
}
// bcdlfmpSs-
switch (typeStr.charAt(0)) {
case 'd':
type = FTPFile.DIRECTORY_TYPE;
break;
case 'l':
type = FTPFile.SYMBOLIC_LINK_TYPE;
break;
case 'b':
case 'c':
isDevice = true;
// break; - fall through
case 'f':
case '-':
type = FTPFile.FILE_TYPE;
break;
default:
type = FTPFile.UNKNOWN_TYPE;
}
file.setType(type);
int g = 4;
for (int access = 0; access < 3; access++, g += 4) {
// Use != '-' to avoid having to check for suid and sticky bits
file.setPermission(access, FTPFile.READ_PERMISSION,
(!"-".equals(group(g))));
file.setPermission(access, FTPFile.WRITE_PERMISSION,
(!"-".equals(group(g + 1))));
String execPerm = group(g + 2);
if (!"-".equals(execPerm) && !Character.isUpperCase(execPerm.charAt(0))) {
file.setPermission(access, FTPFile.EXECUTE_PERMISSION, true);
} else {
file.setPermission(access, FTPFile.EXECUTE_PERMISSION, false);
}
}
if (!isDevice) {
try {
file.setHardLinkCount(Integer.parseInt(hardLinkCount));
} catch (NumberFormatException e) {
// intentionally do nothing
}
}
file.setUser(usr);
file.setGroup(grp);
try {
file.setSize(Long.parseLong(filesize));
} catch (NumberFormatException e) {
// intentionally do nothing
}
if (null == endtoken) {
file.setName(name);
} else {
// oddball cases like symbolic links, file names
// with spaces in them.
name += endtoken;
if (type == FTPFile.SYMBOLIC_LINK_TYPE) {
int end = name.indexOf(" -> ");
// Give up if no link indicator is present
if (end == -1) {
file.setName(name);
} else {
file.setName(name.substring(0, end));
file.setLink(name.substring(end + 4));
}
} else {
file.setName(name);
}
}
return file;
} else {
logger.info("matches(entry) failure:" + entry);
}
return null;
}
/**
* Defines a default configuration to be used when this class is
* instantiated without a {@link FTPClientConfig FTPClientConfig}
* parameter being specified.
*
* @return the default configuration for this parser.
*/
@Override
protected FTPClientConfig getDefaultConfiguration() {
return new FTPClientConfig(
FTPClientConfig.SYST_UNIX,
DEFAULT_DATE_FORMAT,
DEFAULT_RECENT_DATE_FORMAT,
null, null, null);
}
}
笔者寄语:
-
如果看此文后还是不太熟悉,可直接去我的GitHub上下载测试项目(链接在下面)。