多年以前编写了一个文件上传工具,能够通过ftp协议执行文件上传操作。
在工具的使用过程中,面临着远程服务器的不断增加、替换等,为了实现文件上传,需要的远程服务器上安装配置ftp服务,比较麻烦;而我们使用的远程服务器多数是Linux服务器,本身运行了ssh服务,可以通过sftp协议执行文件上传,于是在文件上传工具中,便增加了sftp协议的支持。
为了减少代码的修改量,将原来的文件上传操作类抽象为一个抽象类,维持文件上传操作API接口不变:
public abstract class FtpHelper
对应的,原来的ftp协议实现为:
public class MyFtpHelper extends FtpHelper
新增的sftp协议实现为:
public class MySftpHelper extends FtpHelper
如此,在文件上传工具中,文件上传操作部分的代码无需修改,只需要在创建FtpHelper时根据协议类型修改为创建MyFtpHelper或者MySftpHelper对象即可实现对ftp协议和sftp协议的支持。
后续FtpHelper、MyFtpHelper、MySftpHelper更是作为一个基础工具类包提供给其他程序使用,随着业务的增加,FtpHelper类中定义了更多的方法,除了文件上传之外、另外增加了文件下载、文件删除、文件遍历、文件时间戳修改等功能接口,并新增了断点续传等特性。
然而,伴随着FtpHelper、MyFtpHelper、MySftpHelper在使用过程中偶尔出现的bug,如中文文件名乱码等,以及新增的一些特性和性能的改进。代码上不断的修修补补,使得代码逐渐变得混乱起来。而且由于近来我们的远程服务器都是使用sftp协议,因此我在FtpHelper抽象类中新增的部分操作接口实际上仅仅在MySftpHelper子类中进行了实现,MyFtpHelper子类中对应的实现是留空的,这种情况下,一旦将远程服务器的文件操作协议由sftp协议修改为ftp协议,远程操作将不会如预期那样运行。
国庆前我正在为服务器监控程序ServerDog开发远程文件的备份和清理功能,FtpHelper、MyFtpHelper、MySftpHelper便是实现这一功能的依赖。正当我运筹帷幄、代码如飞时,突然思路被打断,意识到底层工具类FtpHelper中没有远程删除非空文件夹的接口。罢了,先记着,加上该接口的声明,实现暂时留空,待以后再补上。终于,没有远程清理非空文件夹功能的ServerDog开发完成了。然而测试时发现,当远程服务器上存在一个文件夹,并且该文件夹下有文件和文件夹的名称相同时,遍历远程文件存在bug。
在MySftpHelper类中查找bug所在时,被混乱的代码彻底激怒,再加之FtpHelper中定义的部分接口在MyFtpHelper没有得到实现,而后续我将使用到仅安装了ftp服务的远程windows服务器,于是怒删代码,决定彻底重写FtpHelper、MyFtpHelper、MySftpHelper。
以下便是重新编码后的的代码整理。
注:sftp操作依赖jsch.jar,ftp操作依赖commons-net.jar。
一. FtpHelper
FtpHelper仅仅是一个抽象类,定义了远程服务器上的文件操作接口。
package com.dancen.util.ftp;
import java.util.Collection;
import java.util.List;
import org.apache.log4j.Logger;
import com.dancen.util.MyLogManager;
/**
* 能够执行远程文件的上传、下载、删除、遍历等操作的助手
* 路径变量的命名遵守以下规范:
* 文件:filePath
* 文件夹:directoryPath
* 文件或文件夹:path
*
* @author dancen
*
*/
public abstract class FtpHelper
{
protected String host;
protected int port;
protected String user;
protected String password;
protected boolean isBinary; //是否使用二进制传输模式,否则对应文本传输模式
protected boolean isResume; //是否使用断点续传
protected boolean isOverwrite; //是否使用覆写模式
protected boolean isValidate; //是否验证上传结果:由于ftp服务器防火墙的原因,防火墙规则可能以特殊方式处理ftp上传的数据包,导致ftp认为上传成功,实际上传的文件与源文件不一致,开启验证会降低上传性能
protected Logger logger;
public abstract boolean tryGetConnection(int time);
public abstract void getConnection() throws FtpException;
public abstract void closeConnection();
public abstract void upload(String localFilePath, String remoteFilePath) throws FtpException;
public abstract void upload(String localFilePath, String remoteFilePath, boolean isOverwrite) throws FtpException;
public abstract void upload(String localFilePath, String remoteFilePath, boolean isBinary, boolean isResume) throws FtpException;
public abstract void upload(String localFilePath, String remoteFilePath, boolean isBinary, boolean isResume, boolean isOverwrite) throws FtpException;
public abstract void upload(String localFilePath, String remoteFilePath, boolean isBinary, boolean isResume, boolean isOverwrite, boolean isValidate) throws FtpException;
public abstract void upload(Collection<FileEntry> fileEntries) throws FtpException;
public abstract void upload(Collection<FileEntry> fileEntries, boolean isOverwrite) throws FtpException;
public abstract void upload(Collection<FileEntry> fileEntries, boolean isBinary, boolean isResume) throws FtpException;
public abstract void upload(Collection<FileEntry> fileEntries, boolean isBinary, boolean isResume, boolean isOverwrite) throws FtpException;
public abstract void upload(Collection<FileEntry> fileEntries, boolean isBinary, boolean isResume, boolean isOverwrite, boolean isValidate) throws FtpException;
public abstract void download(String remoteFilePath, String localFilePath) throws FtpException;
public abstract void download(String remoteFilePath, String localFilePath, boolean isOverwrite) throws FtpException;
public abstract void download(String remoteFilePath, String localFilePath, boolean isBinary, boolean isResume) throws FtpException;
public abstract void download(String remoteFilePath, String localFilePath, boolean isBinary, boolean isResume, boolean isOverwrite) throws FtpException;
public abstract void delete(String remoteFilePath) throws FtpException; //删除文件
public abstract void deleteAll(String remotePath) throws FtpException; //删除文件和文件夹
public abstract void setLastModified(String remoteFilePath, int lastModified) throws FtpException; //设置文件修改时间,仅精确至秒
public abstract List<MyFtpFile> listFiles(String remotePath, boolean isRecursive) throws FtpException;
public void init(String host, int port, String user, String password)
{
this.host = host;
this.port = port;
this.user = user;
this.password = password;
this.isBinary = true;
this.isResume = false;
this.isOverwrite = true;
this.isValidate = true;
this.logger = MyLogManager.getInstance().getLogger();
}
public String getHost()
{
return this.host;
}
public int getPort()
{
return this.port;
}
public String getUser()
{
return this.user;
}
public void setBinary(boolean isBinary)
{
this.isBinary = isBinary;
}
public boolean isBinary()
{
return this.isBinary;
}
public void setResume(boolean isResume)
{
this.isResume = isResume;
}
public boolean isResume()
{
return this.isResume;
}
public void setOverwrite(boolean isOverwrite)
{
this.isOverwrite = isOverwrite;
}
public boolean isOverwrite()
{
return this.isOverwrite;
}
public void setValidate(boolean isValidate)
{
this.isValidate = isValidate;
}
public boolean isValidate()
{
return this.isValidate;
}
public void setLogger(Logger logger)
{
this.logger = logger;
}
public Logger getLogger()
{
return this.logger;
}
protected void infoDownload(String protocolName, String remoteFilePath, String localFilePath)
{
if(null != this.logger)
{
String msg = String.format("%s [%s]: download %s from %s", protocolName, this.host, localFilePath, remoteFilePath);
this.logger.info(msg);
}
}
protected void infoUpload(String protocolName, String localFilePath, String remoteFilePath)
{
if(null != this.logger)
{
String msg = String.format("%s [%s]: upload %s to %s", protocolName, this.host, localFilePath, remoteFilePath);
this.logger.info(msg);
}
}
protected void infoDelete(String protocolName, String remoteFilePath)
{
if(null != this.logger)
{
String msg = String.format("%s [%s]: delete %s", protocolName, this.host, remoteFilePath);
this.logger.info(msg);
}
}
protected void infoDeleteAll(String protocolName, String remoteFilePath)
{
if(null != this.logger)
{
String msg = String.format("%s [%s]: deleteAll %s", protocolName, this.host, remoteFilePath);
this.logger.info(msg);
}
}
protected void infoSetLastModified(String protocolName, String remoteFilePath, int lastModified)
{
if(null != this.logger)
{
String msg = String.format("%s [%s]: setLastModified %s %s", protocolName, this.host, remoteFilePath, lastModified);
this.logger.info(msg);
}
}
protected void info(String protocolName, String msg)
{
if(null != this.logger)
{
this.logger.info(String.format("%s [%s]: %s", protocolName, this.host, msg));
}
}
protected void error(String protocolName, String msg)
{
if(null != this.logger)
{
this.logger.error(String.format("%s [%s]: %s", protocolName, this.host, msg));
}
}
protected void error(String protocolName, Throwable t)
{
if(null != this.logger)
{
this.logger.error(String.format("%s [%s]: %s", protocolName, this.host, t.getMessage()), t);
}
}
}
二. MySftpHelper
MySftpHelper是FtpHelper在sftp协议下的实现。
package com.dancen.util.ftp;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.dancen.util.file.MyFilePath;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.ChannelSftp.LsEntry;
/**
* sftp工具类
* 鉴于linux默认皆有sftp服务而无ftp服务,开发此类以在必要时候替代ftp。
* sftp无法以ASC2模式传输,只能以二进制模式传输文件,该类保留isBinary参数仅是对父类FtpHelper的兼容。
* 在已测试过的环境中,sftp不存在中文路径的乱码问题。
*
* @author dancen
* @date 2014-02-19
* @date 2020-10-14 彻底重写
*/
public class MySftpHelper extends FtpHelper
{
public static final String PROTOCOL_NAME = "sftp";
public static final int DEFAULT_PORT = 22;
private static MySftpHelper instance = null;
private JSch jSch;
private Lock lock;
private Session session;
private ChannelSftp channel;
public static void main(String[] args)
{
MySftpHelper mySftpHelper = new MySftpHelper();
mySftpHelper.init("10.xx.xx.xx", 22, "xx", "xxxx");
mySftpHelper.tryGetConnection(3);
try
{
RemoteParsedPath remoteParsedFilePath = new RemoteParsedPath("/home/dancen/btest/test2/z.txt");
mySftpHelper.mkdirRecursivelyByDirectoryPath(remoteParsedFilePath);
mySftpHelper.mkdirRecursivelyByFilePath(remoteParsedFilePath);
mySftpHelper.deleteAll("/home/dancen/btest/");
List<MyFtpFile> myFtpFiles = mySftpHelper.listFiles("/home/dancen/test", true);
if(null != myFtpFiles)
{
for(MyFtpFile myFtpFile : myFtpFiles)
{
System.out.println(myFtpFile.getPath());
}
}
mySftpHelper.upload("C:\\Users\\xxx\\Desktop\\11\\1测试.txt", "/home/dancen/test/测试12abc.txt", true, true, true, true);
mySftpHelper.download("/home/dancen/test/1.txt", "C:\\Users\\xxx\\Desktop\\11\\1测试.txt", true, true, true);
mySftpHelper.setLastModified("/home/dancen/test/1.txt", (int)(System.currentTimeMillis() / 1000));
}
catch(Exception e)
{
e.printStackTrace();
}
finally
{
mySftpHelper.closeConnection();
}
}
public static MySftpHelper getInstance()
{
if(null == instance)
{
instance = new MySftpHelper();
}
return instance;
}
public MySftpHelper()
{
this.jSch = new JSch();
this.lock = new ReentrantLock();
}
@Override
public void init(String host, int port, String user, String password)
{
super.init(host, port, user, password);
}
@Override
public boolean tryGetConnection(int time)
{
this.lock.lock();
try
{
boolean rs = false;
int i = 0;
do
{
i++;
try
{
this.connect(this.host, this.port, this.user, this.password);
rs = true;
break;
}
catch(JSchException e)
{
e.printStackTrace();
}
}
while(i < time);
return rs;
}
finally
{
this.lock.unlock();
}
}
@Override
public void getConnection() throws FtpException
{
this.lock.lock();
try
{
this.connect(this.host, this.port, this.user, this.password);
}
catch(JSchException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void closeConnection()
{
this.lock.lock();
try
{
this.disconnect();
}
finally
{
this.lock.unlock();
}
}
@Override
public void upload(String localFilePath, String remoteFilePath) throws FtpException
{
this.upload(localFilePath, remoteFilePath, this.isBinary, this.isResume, this.isOverwrite, this.isValidate);
}
@Override
public void upload(String localFilePath, String remoteFilePath, boolean isOverwrite) throws FtpException
{
this.upload(localFilePath, remoteFilePath, this.isBinary, this.isResume, isOverwrite, this.isValidate);
}
@Override
public void upload(String localFilePath, String remoteFilePath, boolean isBinary, boolean isResume) throws FtpException
{
this.upload(localFilePath, remoteFilePath, isBinary, isResume, this.isOverwrite, this.isValidate);
}
@Override
public void upload(String localFilePath, String remoteFilePath, boolean isBinary, boolean isResume, boolean isOverwrite) throws FtpException
{
this.upload(localFilePath, remoteFilePath, isBinary, isResume, isOverwrite, this.isValidate);
}
@Override
public void upload(String localFilePath, String remoteFilePath, boolean isBinary, boolean isResume, boolean isOverwrite, boolean isValidate) throws FtpException
{
this.lock.lock();
try
{
if(null != localFilePath && null != remoteFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoUpload(PROTOCOL_NAME, localFilePath, remoteParsedFilePath.getPath());
this.put(localFilePath, remoteParsedFilePath, isBinary, isResume, isOverwrite, isValidate);
}
}
catch(SftpException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void upload(Collection<FileEntry> fileEntries) throws FtpException
{
this.upload(fileEntries, this.isBinary, this.isResume, this.isOverwrite, this.isValidate);
}
@Override
public void upload(Collection<FileEntry> fileEntries, boolean isOverwrite) throws FtpException
{
this.upload(fileEntries, this.isBinary, this.isResume, isOverwrite, this.isValidate);
}
@Override
public void upload(Collection<FileEntry> fileEntries, boolean isBinary, boolean isResume) throws FtpException
{
this.upload(fileEntries, isBinary, isResume, this.isOverwrite, this.isValidate);
}
@Override
public void upload(Collection<FileEntry> fileEntries, boolean isBinary, boolean isResume, boolean isOverwrite) throws FtpException
{
this.upload(fileEntries, isBinary, isResume, isOverwrite, this.isValidate);
}
@Override
public void upload(Collection<FileEntry> fileEntries, boolean isBinary, boolean isResume, boolean isOverwrite, boolean isValidate) throws FtpException
{
this.lock.lock();
try
{
if(null != fileEntries)
{
for(FileEntry fileEntry : fileEntries)
{
if(null != fileEntry)
{
String localFilePath = fileEntry.getLocalPath();
String remoteFilePath = fileEntry.getRemotePath();
if(null != localFilePath && null != remoteFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoUpload(PROTOCOL_NAME, localFilePath, remoteParsedFilePath.getPath());
this.put(localFilePath, remoteParsedFilePath, isBinary, isResume, isOverwrite, isValidate);
}
}
}
}
}
catch(SftpException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void download(String remoteFilePath, String localFilePath) throws FtpException
{
this.download(remoteFilePath, localFilePath, this.isBinary, this.isResume, this.isOverwrite);
}
@Override
public void download(String remoteFilePath, String localFilePath, boolean isOverwrite) throws FtpException
{
this.download(remoteFilePath, localFilePath, this.isBinary, this.isResume, isOverwrite);
}
@Override
public void download(String remoteFilePath, String localFilePath, boolean isBinary, boolean isResume) throws FtpException
{
this.download(remoteFilePath, localFilePath, isBinary, isResume, true);
}
@Override
public void download(String remoteFilePath, String localFilePath, boolean isBinary, boolean isResume, boolean isOverwrite) throws FtpException
{
this.lock.lock();
try
{
if(null != remoteFilePath && null != localFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoDownload(PROTOCOL_NAME, remoteParsedFilePath.getPath(), localFilePath);
this.get(remoteParsedFilePath.getPath(), localFilePath, isBinary, isResume, isOverwrite);
}
}
catch(SftpException e)
{
throw new FtpException(e);
}
catch(IOException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void delete(String remoteFilePath) throws FtpException
{
this.lock.lock();
try
{
if(null != remoteFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoDelete(PROTOCOL_NAME, remoteParsedFilePath.getPath());
this.rm(remoteParsedFilePath.getPath());
}
}
catch(SftpException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void deleteAll(String remotePath) throws FtpException
{
this.lock.lock();
try
{
if(null != remotePath)
{
RemoteParsedPath remoteParsedPath = this.createRemoteParsedPath(remotePath);
this.infoDeleteAll(PROTOCOL_NAME, remoteParsedPath.getPath());
this.rmAll(remoteParsedPath.getPath());
}
}
catch(SftpException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void setLastModified(String remoteFilePath, int lastModified) throws FtpException
{
this.lock.lock();
try
{
if(null != remoteFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoSetLastModified(PROTOCOL_NAME, remoteParsedFilePath.getPath(), lastModified);
this.setMtime(remoteParsedFilePath.getPath(), lastModified);
}
}
catch(SftpException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public List<MyFtpFile> listFiles(String remotePath, boolean isRecursive) throws FtpException
{
this.lock.lock();
try
{
List<MyFtpFile> rs = null;
if(null != remotePath)
{
rs = new ArrayList<MyFtpFile>();
RemoteParsedPath remoteParsedPath = this.createRemoteParsedPath(remotePath);
Collection<MyFtpFile> ftpFiles = this.ls(remoteParsedPath.getPath(), isRecursive);
if(null != ftpFiles)
{
for(MyFtpFile ftpFile : ftpFiles)
{
if(ftpFile.isReg())
{
rs.add(ftpFile);
}
}
}
}
return rs;
}
catch(SftpException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
/**
* 建立连接
* @param host
* @param port
* @param user
* @param password
* @throws JSchException
*/
private void connect(String host, int port, String user, String password) throws JSchException
{
try
{
if(null == this.session)
{
this.session = jSch.getSession(user, host, port);
this.session.setPassword(password);
Properties properties = new Properties();
properties.put("StrictHostKeyChecking", "no");
this.session.setConfig(properties);
}
if(!this.session.isConnected())
{
this.session.connect();
}
if(null == this.channel)
{
this.channel = (ChannelSftp)this.session.openChannel("sftp");
}
if(!this.channel.isConnected())
{
this.channel.connect();
}
}
catch(JSchException e)
{
this.disconnect();
throw e;
}
catch(Throwable t)
{
this.disconnect();
throw new JSchException("connect failed", t);
}
}
/**
* 断开连接
*/
private void disconnect()
{
if(null != this.channel)
{
if(this.channel.isConnected())
{
this.channel.disconnect();
}
this.channel = null;
}
if(null != this.session)
{
if(this.session.isConnected())
{
this.session.disconnect();
}
this.session = null;
}
}
/**
* 下载文件
* @param remoteFilePath
* @param localFilePath
* @param isBinary
* @param isResume
* @param isOverwrite
* @throws IOException
* @throws SftpException
*/
private void get(String remoteFilePath, String localFilePath, boolean isBinary, boolean isResume, boolean isOverwrite) throws IOException, SftpException
{
if(null != remoteFilePath && null != localFilePath)
{
if(isOverwrite && !isResume)
{
MyFilePath.mkdirs(localFilePath, true);
this.get(remoteFilePath, localFilePath, ChannelSftp.OVERWRITE);
}
else
{
File localFile = new File(localFilePath);
if(localFile.isFile())
{
SftpATTRS sftpATTRS = this.lstat(remoteFilePath);
if(null == sftpATTRS || !sftpATTRS.isReg())
{
throw new SftpException(ChannelSftp.SSH_FX_FAILURE, "get error: remote file not exist");
}
if(isOverwrite)
{
long remoteSize = sftpATTRS.getSize();
long localSize = localFile.length();
if(localSize > remoteSize)
{
this.get(remoteFilePath, localFilePath, ChannelSftp.OVERWRITE);
}
else if(localSize < remoteSize)
{
this.get(remoteFilePath, localFilePath, ChannelSftp.RESUME);
}
}
}
else
{
MyFilePath.mkdirs(localFilePath, true);
this.get(remoteFilePath, localFilePath, ChannelSftp.OVERWRITE);
}
}
}
}
/**
* 下载文件
* @param remoteFilePath
* @param localFilePath
* @param mode
* @throws SftpException
*/
private void get(String remoteFilePath, String localFilePath, int mode) throws SftpException
{
if(null != remoteFilePath && null != localFilePath)
{
this.channel.get(remoteFilePath, localFilePath, null, mode);
}
}
/**
* 上传文件
* @param localFilePath
* @param remoteParsedFilePath
* @param isBinary
* @param isResume
* @param isOverwrite
* @param isValidate
* @throws SftpException
*/
private void put(String localFilePath, RemoteParsedPath remoteParsedFilePath, boolean isBinary, boolean isResume, boolean isOverwrite, boolean isValidate) throws SftpException
{
if(null != localFilePath && null != remoteParsedFilePath)
{
File localFile = new File(localFilePath);
if(isOverwrite && !isResume)
{
this.mkdirRecursivelyByFilePath(remoteParsedFilePath);
this.put(localFile, remoteParsedFilePath.getPath(), ChannelSftp.OVERWRITE, isValidate);
}
else
{
SftpATTRS sftpATTRS = this.lstat(remoteParsedFilePath.getPath());
if(null != sftpATTRS)
{
if(sftpATTRS.isReg())
{
if(isOverwrite)
{
long remoteSize = sftpATTRS.getSize();
long localSize = localFile.length();
if(remoteSize > localSize)
{
this.put(localFile, remoteParsedFilePath.getPath(), ChannelSftp.OVERWRITE, isValidate);
}
else if(remoteSize < localSize)
{
this.put(localFile, remoteParsedFilePath.getPath(), ChannelSftp.RESUME, isValidate);
}
}
}
else
{
throw new SftpException(ChannelSftp.SSH_FX_FAILURE, "put error: remote file exist but not a regular file");
}
}
else
{
this.mkdirRecursivelyByFilePath(remoteParsedFilePath);
this.put(localFile, remoteParsedFilePath.getPath(), ChannelSftp.OVERWRITE, isValidate);
}
}
}
}
/**
* 上传文件
* @param localFile
* @param remoteFilePath
* @param mode
* @param isValidate
* @throws SftpException
*/
private void put(File localFile, String remoteFilePath, int mode, boolean isValidate) throws SftpException
{
if(null != localFile && null != remoteFilePath)
{
this.channel.put(localFile.getAbsolutePath(), remoteFilePath, mode);
if(isValidate)
{
SftpATTRS sftpATTRS = this.lstat(remoteFilePath);
if(null == sftpATTRS || !sftpATTRS.isReg() || sftpATTRS.getSize() != localFile.length())
{
throw new SftpException(ChannelSftp.SSH_FX_FAILURE, "put error: put finished but validate failed");
}
}
}
}
/**
* 创建文件夹
* @param remoteParsedFilePath
* @throws SftpException
*/
private void mkdirRecursivelyByFilePath(RemoteParsedPath remoteParsedFilePath) throws SftpException
{
if(null != remoteParsedFilePath)
{
SftpATTRS sftpATTRS = this.lstat(remoteParsedFilePath.getParentPath());
if(null == sftpATTRS || !sftpATTRS.isDir())
{
this.mkdir(remoteParsedFilePath.getParentPaths());
}
}
}
/**
*创建文件夹
* 先直接检测文件夹是否存在以避免文件夹已经存在时仍然层层检测和创建文件夹
* @param remoteParsedDirectoryPath
* @throws SftpException
*/
private void mkdirRecursivelyByDirectoryPath(RemoteParsedPath remoteParsedDirectoryPath) throws SftpException
{
if(null != remoteParsedDirectoryPath)
{
SftpATTRS sftpATTRS = this.lstat(remoteParsedDirectoryPath.getPath());
if(null == sftpATTRS || !sftpATTRS.isDir())
{
this.mkdir(remoteParsedDirectoryPath.getParentPaths());
this.mkdir(remoteParsedDirectoryPath.getPath());
}
}
}
/**
* 创建文件夹
* @param parentPaths
* @throws SftpException
*/
private void mkdir(Collection<String> parentPaths) throws SftpException
{
if(null != parentPaths)
{
for(String parentPath : parentPaths)
{
this.mkdir(parentPath);
}
}
}
/**
* 创建文件夹
* 当文件夹已经存在时,不抛出异常
* 不能递归创建文件夹
* @param remoteDirectoryPath
* @throws SftpException
*/
private void mkdir(String remoteDirectoryPath) throws SftpException
{
if(null != remoteDirectoryPath && !remoteDirectoryPath.isEmpty())
{
SftpATTRS sftpATTRS = this.lstat(remoteDirectoryPath);
if(null == sftpATTRS || !sftpATTRS.isDir())
{
this.channel.mkdir(remoteDirectoryPath);
}
}
}
/**
* 删除文件和文件夹
* 递归删除
* @param remotePath
* @throws SftpException
*/
private void rmAll(String remotePath) throws SftpException
{
if(null != remotePath)
{
SftpATTRS sftpATTRS = this.lstat(remotePath);
if(null != sftpATTRS)
{
if(sftpATTRS.isDir())
{
Collection<MyFtpFile> ftpFiles = this.ls(remotePath, true);
List<MyFtpFile> ftpFileList = this.sort(ftpFiles);
if(null != ftpFileList)
{
for(MyFtpFile ftpFile : ftpFileList)
{
if(ftpFile.isDir())
{
this.rmdir(ftpFile.getPath());
}
else
{
this.rm(ftpFile.getPath());
}
}
}
this.rmdir(remotePath);
}
else
{
this.rm(remotePath);
}
}
}
}
/**
* 删除文件
* 当文件不存在时,不抛出异常
* @param remoteFilePath
* @throws SftpException
*/
private void rm(String remoteFilePath) throws SftpException
{
if(null != remoteFilePath && !remoteFilePath.isEmpty())
{
try
{
this.channel.rm(remoteFilePath);
}
catch(SftpException e)
{
if(ChannelSftp.SSH_FX_NO_SUCH_FILE != e.id)
{
throw e;
}
}
}
}
/**
* 删除文件夹
* 当文件夹不存在时,不抛出异常
* 仅能删除空文件夹
* @param remoteDirectoryPath
* @throws SftpException
*/
private void rmdir(String remoteDirectoryPath) throws SftpException
{
if(null != remoteDirectoryPath && !remoteDirectoryPath.isEmpty())
{
try
{
this.channel.rmdir(remoteDirectoryPath);
}
catch(SftpException e)
{
if(ChannelSftp.SSH_FX_NO_SUCH_FILE != e.id)
{
throw e;
}
}
}
}
/**
* 文件排序
* 将文件按照文件路径字符串倒序排序,以方便进行非空文件夹的删除操作
* @param ftpFiles
* @return
*/
private List<MyFtpFile> sort(Collection<MyFtpFile> ftpFiles)
{
List<MyFtpFile> rs = null;
if(null != ftpFiles)
{
rs = new ArrayList<MyFtpFile>();
rs.addAll(ftpFiles);
Collections.sort(rs, MyFtpFile.COMPARATOR_PATH_DESC);
}
return rs;
}
/**
* 设置文件修改时间
* @param remoteFilePath
* @param lastModified
* @throws SftpException
*/
private void setMtime(String remoteFilePath, int lastModified) throws SftpException
{
if(null != remoteFilePath && !remoteFilePath.isEmpty())
{
this.channel.setMtime(remoteFilePath, lastModified);
}
}
/**
* 递归列出所有类型的文件,包含文件、文件夹、链接
* @param remotePath
* @param isRecursive
* @return
* @throws SftpException
*/
private Collection<MyFtpFile> ls(String remotePath, boolean isRecursive) throws SftpException
{
Collection<MyFtpFile> rs = null;
if(null != remotePath && !remotePath.isEmpty())
{
SftpATTRS sftpATTRS = this.lstat(remotePath);
if(null != sftpATTRS)
{
rs = new ArrayList<MyFtpFile>();
if(!sftpATTRS.isDir())
{
rs.add(new MyFtpFile(remotePath, sftpATTRS));
}
else
{
Collection<LsEntry> lsEntries = this.ls(remotePath);
if(null != lsEntries)
{
for(LsEntry lsEntry : lsEntries)
{
MyFtpFile ftpFile = new MyFtpFile(remotePath, lsEntry);
rs.add(ftpFile);
if(isRecursive && lsEntry.getAttrs().isDir())
{
Collection<MyFtpFile> ftpFiles = this.ls(ftpFile.getPath(), isRecursive);
if(null != ftpFiles)
{
rs.addAll(ftpFiles);
}
}
}
}
}
}
}
return rs;
}
/**
* 列出文件列表
* 当文件不存在时,直接返回空,而不是抛出异常
* @param remotePath
* @return
* @throws SftpException
*/
private Collection<LsEntry> ls(String remotePath) throws SftpException
{
Collection<LsEntry> rs = null;
if(null != remotePath && !remotePath.isEmpty())
{
try
{
MyLsAllEntrySelector selector = new MyLsAllEntrySelector();
this.channel.ls(remotePath, selector);
rs = selector.getLsEntries();
}
catch(SftpException e)
{
if(ChannelSftp.SSH_FX_NO_SUCH_FILE != e.id)
{
throw e;
}
}
}
return rs;
}
/**
* 获取文件属性
* 当文件不存在时,直接返回空,而不是抛出异常
* @param remotePath
* @return
* @throws SftpException
*/
private SftpATTRS lstat(String remotePath) throws SftpException
{
SftpATTRS rs = null;
if(null != remotePath && !remotePath.isEmpty())
{
try
{
rs = this.channel.lstat(remotePath);
}
catch(SftpException e)
{
if(ChannelSftp.SSH_FX_NO_SUCH_FILE != e.id)
{
throw e;
}
}
}
return rs;
}
/**
* 创建规范化的远程路径
* @param remotePath
* @return
*/
private RemoteParsedPath createRemoteParsedPath(String remotePath)
{
RemoteParsedPath rs = null;
if(null != remotePath)
{
rs = new RemoteParsedPath(remotePath);
}
return rs;
}
}
三. MyFtpHelper
MyFtpHelper是FtpHelper在ftp协议下的实现,在被删除的第一版代码中,我是先实现了MyFtpHelper,再大致依照它实现了MySftpHelper。而代码重写后,顺序换了过来,我先实现了MySftpHelper,再依照MySftpHelper来实现MyFtpHelper,因此MyFtpHelper中部分私有方法的命名上和MySftpHelper中一致,而和jar包中FtpClient对应的方法名称有明显差异,这一点不用感到奇怪。
package com.dancen.util.ftp;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import com.dancen.util.MyDocumentManager;
import com.dancen.util.file.MyFilePath;
/**
* 支持断点续传的FTP工具类
* isBinary参数控制着文件传输使用二进制模式或者文本模式,文本传输模式会根据平台对数据进行某些转换,可能造成本地文件与远程文件不一致,建议始终采用二进制传输模式。
* ftp有主动连接模式PORT和被动连接模式PASV,主动连接模式可能被客户端防火墙拦截,因此这里采用被动连接模式。
* ftp对中文路径支持不佳,本类已尝试对路径重新执行编码和解码操作,在已测试过的环境中能够解决中文乱码问题。
*
* @author dancen
* @date 2013-08-09
* @date 2020-10-14 彻底重写
*/
public class MyFtpHelper extends FtpHelper
{
public static final String PROTOCOL_NAME = "ftp";
public static final int DEFAULT_PORT = 21;
public static final String ENCODING_UTF8 = "UTF-8";
public static final String ENCODING_ZH_CN = "GBK";
public static final String ENCODING_EN = "iso-8859-1";
public static final int BUFFERED_SIZE = 1024;
public static final ThreadLocal<DateFormat> DATE_FORMAT;
private static MyFtpHelper instance = null;
private FTPClient ftpClient;
private Lock lock;
private String localEncoding;
static
{
DATE_FORMAT = new ThreadLocal<DateFormat>()
{
@Override
public DateFormat initialValue()
{
DateFormat rs = new SimpleDateFormat("yyyyMMddHHmmss");
TimeZone timeZone = TimeZone.getTimeZone("Etc/GMT");
rs.setTimeZone(timeZone);
return rs;
}
};
}
public static void main(String[] args)
{
System.out.println("Just for test ...");
MyFtpHelper myFtpHelper = MyFtpHelper.getInstance();
myFtpHelper.init("10.xx.xx.xx", 21, "xxxx", "xxxxx");
try
{
myFtpHelper.getConnection();
RemoteParsedPath remoteParsedPath = myFtpHelper.createRemoteParsedPath("/test/1013/a/b/c");
myFtpHelper.mkdirRecursivelyByDirectoryPath(remoteParsedPath);
myFtpHelper.delete("/t/test.png");
myFtpHelper.upload("C:\\Users\\xxx\\Desktop\\1.txt", "/test/1013/a/b/c/测试了以后.txt", true, true, true, true);
myFtpHelper.download("/test/1013/a/b/c/1.txt", "C:\\Users\\xxx\\Desktop\\2.txt", true, true, true);
long timeInMills = System.currentTimeMillis();
myFtpHelper.setLastModified("/test/1013/a/b/c/测试了以后.txt", (int)(timeInMills / 1000));
Collection<MyFtpFile> myFtpFiles = myFtpHelper.listFiles("/test", true);
if(null != myFtpFiles)
{
for(MyFtpFile myFtpFile : myFtpFiles)
{
System.out.println(myFtpFile.getPath());
}
}
myFtpHelper.deleteAll("/test/1013");
}
catch(Exception e)
{
e.printStackTrace();
}
finally
{
myFtpHelper.closeConnection();
}
System.out.println("... end");
}
/**
* 将时间戳转换为ftp时间字符串
* 返回的ftp时间字符串的格式为yyyyMMddHHmmss,采用GMT时区
* 注意,官方API中描述:The modification string should be in the ISO 3077 form "YYYYMMDDhhmmss
* 其所述时间格式与实际不符
* @param timeInMillis
* @return
*/
public static String getFtpTimeText(long timeInMillis)
{
return DATE_FORMAT.get().format(timeInMillis);
}
/**
* 将ftp时间字符串转换为时间戳
* 提交的参数中ftp时间字符串的格式为yyyyMMddHHmmss,采用GMT时区
* 注意,官方API中描述:The modification string should be in the ISO 3077 form "YYYYMMDDhhmmss
* 其所述时间格式与实际不符
* @param ftpTimeText
* @return
* @throws ParseException
*/
public static long getFtpTimeInMillis(String ftpTimeText) throws ParseException
{
long rs = 0;
if(null != ftpTimeText)
{
rs = DATE_FORMAT.get().parse(ftpTimeText).getTime();
}
return rs;
}
public static MyFtpHelper getInstance()
{
if(null == instance)
{
instance = new MyFtpHelper();
}
return instance;
}
public MyFtpHelper()
{
this.ftpClient = new FTPClient();
this.lock = new ReentrantLock();
this.localEncoding = ENCODING_ZH_CN;
}
public String getLocalEncoding()
{
return this.localEncoding;
}
@Override
public void init(String host, int port, String user, String password)
{
super.init(host, port, user, password);
}
@Override
public boolean tryGetConnection(int time)
{
this.lock.lock();
try
{
boolean rs = false;
int i = 0;
do
{
i++;
try
{
this.connect(this.host, this.port, this.user, this.password);
rs = true;
break;
}
catch(IOException e)
{
e.printStackTrace();
}
}
while(i < time);
return rs;
}
finally
{
this.lock.unlock();
}
}
@Override
public void getConnection() throws FtpException
{
this.lock.lock();
try
{
this.connect(this.host, this.port, this.user, this.password);
}
catch(IOException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void closeConnection()
{
this.lock.lock();
try
{
this.disconnect();
}
finally
{
this.lock.unlock();
}
}
@Override
public void upload(String localFilePath, String remoteFilePath) throws FtpException
{
this.upload(localFilePath, remoteFilePath, this.isBinary, this.isResume, this.isOverwrite, this.isValidate);
}
@Override
public void upload(String localFilePath, String remoteFilePath, boolean isOverwrite) throws FtpException
{
this.upload(localFilePath, remoteFilePath, this.isBinary, this.isResume, isOverwrite, this.isValidate);
}
@Override
public void upload(String localFilePath, String remoteFilePath, boolean isBinary, boolean isResume) throws FtpException
{
this.upload(localFilePath, remoteFilePath, isBinary, isResume, this.isOverwrite, this.isValidate);
}
@Override
public void upload(String localFilePath, String remoteFilePath, boolean isBinary, boolean isResume, boolean isOverwrite) throws FtpException
{
this.upload(localFilePath, remoteFilePath, isBinary, isResume, isOverwrite, this.isValidate);
}
@Override
public void upload(String localFilePath, String remoteFilePath, boolean isBinary, boolean isResume, boolean isOverwrite, boolean isValidate) throws FtpException
{
this.lock.lock();
try
{
if(null != localFilePath && null != remoteFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoUpload(PROTOCOL_NAME, localFilePath, remoteParsedFilePath.getPath());
this.put(localFilePath, remoteParsedFilePath, isBinary, isResume, isOverwrite, isValidate);
}
}
catch(IOException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void upload(Collection<FileEntry> fileEntries) throws FtpException
{
this.upload(fileEntries, this.isBinary, this.isResume, this.isOverwrite, this.isValidate);
}
@Override
public void upload(Collection<FileEntry> fileEntries, boolean isOverwrite) throws FtpException
{
this.upload(fileEntries, this.isBinary, this.isResume, isOverwrite, this.isValidate);
}
@Override
public void upload(Collection<FileEntry> fileEntries, boolean isBinary, boolean isResume) throws FtpException
{
this.upload(fileEntries, isBinary, isResume, this.isOverwrite, this.isValidate);
}
@Override
public void upload(Collection<FileEntry> fileEntries, boolean isBinary, boolean isResume, boolean isOverwrite) throws FtpException
{
this.upload(fileEntries, isBinary, isResume, isOverwrite, this.isValidate);
}
@Override
public void upload(Collection<FileEntry> fileEntries, boolean isBinary, boolean isResume, boolean isOverwrite, boolean isValidate) throws FtpException
{
this.lock.lock();
try
{
if(null != fileEntries)
{
for(FileEntry fileEntry : fileEntries)
{
if(null != fileEntry)
{
String localFilePath = fileEntry.getLocalPath();
String remoteFilePath = fileEntry.getRemotePath();
if(null != localFilePath && null != remoteFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoUpload(PROTOCOL_NAME, localFilePath, remoteParsedFilePath.getPath());
this.put(localFilePath, remoteParsedFilePath, isBinary, isResume, isOverwrite, isValidate);
}
}
}
}
}
catch(IOException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void download(String remoteFilePath, String localFilePath) throws FtpException
{
this.download(remoteFilePath, localFilePath, this.isBinary, this.isResume, this.isOverwrite);
}
@Override
public void download(String remoteFilePath, String localFilePath, boolean isOverwrite) throws FtpException
{
this.download(remoteFilePath, localFilePath, this.isBinary, this.isResume, isOverwrite);
}
@Override
public void download(String remoteFilePath, String localFilePath, boolean isBinary, boolean isResume) throws FtpException
{
this.download(remoteFilePath, localFilePath, isBinary, isResume, true);
}
@Override
public void download(String remoteFilePath, String localFilePath, boolean isBinary, boolean isResume, boolean isOverwrite) throws FtpException
{
this.lock.lock();
try
{
if(null != remoteFilePath && null != localFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoDownload(PROTOCOL_NAME, remoteParsedFilePath.getPath(), localFilePath);
this.get(remoteParsedFilePath.getPath(), localFilePath, isBinary, isResume, isOverwrite);
}
}
catch(IOException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void delete(String remoteFilePath) throws FtpException
{
this.lock.lock();
try
{
if(null != remoteFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoDelete(PROTOCOL_NAME, remoteParsedFilePath.getPath());
this.rm(remoteParsedFilePath.getPath());
}
}
catch(IOException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void deleteAll(String remotePath) throws FtpException
{
this.lock.lock();
try
{
if(null != remotePath)
{
RemoteParsedPath remoteParsedPath = this.createRemoteParsedPath(remotePath);
this.infoDeleteAll(PROTOCOL_NAME, remoteParsedPath.getPath());
this.rmAll(remoteParsedPath.getPath());
}
}
catch(IOException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public void setLastModified(String remoteFilePath, int lastModified) throws FtpException
{
this.lock.lock();
try
{
if(null != remoteFilePath)
{
RemoteParsedPath remoteParsedFilePath = this.createRemoteParsedPath(remoteFilePath);
this.infoSetLastModified(PROTOCOL_NAME, remoteParsedFilePath.getPath(), lastModified);
//需将lastModified * 1000由int转换为long
String lastModifiedText = getFtpTimeText(lastModified * 1000L);
this.setMtime(remoteParsedFilePath.getPath(), lastModifiedText);
}
}
catch(IOException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
@Override
public List<MyFtpFile> listFiles(String remotePath, boolean isRecursive) throws FtpException
{
this.lock.lock();
try
{
List<MyFtpFile> rs = null;
if(null != remotePath)
{
rs = new ArrayList<MyFtpFile>();
RemoteParsedPath remoteParsedPath = this.createRemoteParsedPath(remotePath);
Collection<MyFtpFile> ftpFiles = this.ls(remoteParsedPath.getPath(), isRecursive);
if(null != ftpFiles)
{
for(MyFtpFile ftpFile : ftpFiles)
{
if(ftpFile.isReg())
{
rs.add(ftpFile);
}
}
}
}
return rs;
}
catch(IOException e)
{
throw new FtpException(e);
}
finally
{
this.lock.unlock();
}
}
/**
* 建立连接
* @param host
* @param port
* @param user
* @param password
* @throws IOException
*/
private void connect(String host, int port, String user, String password) throws IOException
{
if(!this.ftpClient.isConnected())
{
try
{
this.ftpClient.connect(host, port);
if(FTPReply.isPositiveCompletion(this.ftpClient.getReplyCode()))
{
if(this.ftpClient.login(user, password))
{
//尝试设置ftp编码
if(FTPReply.isPositiveCompletion(this.ftpClient.sendCommand("OPTS UTF8", "ON")))
{
this.localEncoding = ENCODING_UTF8;
}
else
{
this.localEncoding = ENCODING_ZH_CN;
}
this.ftpClient.enterLocalPassiveMode();
}
else
{
this.disconnect();
throw new IOException("connect error: login failed");
}
}
else
{
this.disconnect();
throw new IOException("connect failed");
}
}
catch(IOException e)
{
this.disconnect();
throw e;
}
catch(Throwable t)
{
this.disconnect();
throw new IOException("connect failed", t);
}
}
}
/**
* 断开连接
*/
private void disconnect()
{
if(this.ftpClient.isConnected())
{
try
{
this.ftpClient.logout();
}
catch(IOException e)
{
e.printStackTrace();
}
try
{
this.ftpClient.disconnect();
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
/**
* 下载文件
* @param remoteFilePath
* @param localFilePath
* @param isBinary
* @param isResume
* @param isOverwrite
* @throws IOException
*/
private void get(String remoteFilePath, String localFilePath, boolean isBinary, boolean isResume, boolean isOverwrite) throws IOException
{
if(null != remoteFilePath && null != localFilePath)
{
this.ftpClient.enterLocalPassiveMode();
if(!this.ftpClient.setFileType(isBinary ? FTP.BINARY_FILE_TYPE : FTP.ASCII_FILE_TYPE))
{
throw new IOException("download error: set file type failed");
}
if(isOverwrite && !isResume)
{
MyFilePath.mkdirs(localFilePath, true);
this.get(remoteFilePath, localFilePath, 0L);
}
else
{
File localFile = new File(localFilePath);
if(localFile.isFile())
{
FTPFile ftpFile = this.lstat(remoteFilePath);
if(null == ftpFile || !ftpFile.isFile())
{
throw new IOException("download error: remote file not exist");
}
if(isOverwrite)
{
long remoteSize = ftpFile.getSize();
long localSize = localFile.length();
if(localSize > remoteSize)
{
this.get(remoteFilePath, localFilePath, 0L);
}
else if(localSize < remoteSize)
{
this.get(remoteFilePath, localFilePath, localSize);
}
}
}
else
{
MyFilePath.mkdirs(localFilePath, true);
this.get(remoteFilePath, localFilePath, 0L);
}
}
}
}
/**
* 下载文件
* @param remoteFilePath
* @param localFilePath
* @param offset
* @throws IOException
*/
private void get(String remoteFilePath, String localFilePath, long offset) throws IOException
{
if(null != remoteFilePath && null != localFilePath)
{
RandomAccessFile raf = null;
InputStream is = null;
try
{
if(0 >= offset)
{
MyFilePath.delete(localFilePath);
}
//is和raf的创建顺序需要关注,如果反过来,则远程is创建失败时,仍然会在本地创建一个新文件
is = this.getInputStream(remoteFilePath, offset);
raf = this.getRandomAccessFile(localFilePath, "rw", offset);
byte[] bytes = new byte[BUFFERED_SIZE];
int count;
while(-1 != (count = is.read(bytes)))
{
raf.write(bytes, 0, count);
}
}
finally
{
MyDocumentManager.close(is);
MyDocumentManager.close(raf);
}
if(!this.ftpClient.completePendingCommand())
{
throw new IOException("download failed");
}
}
}
/**
* 上传文件
* @param localFilePath
* @param remoteParsedFilePath
* @param isBinary
* @param isResume
* @param isOverwrite
* @param isValidate
* @throws IOException
*/
private void put(String localFilePath, RemoteParsedPath remoteParsedFilePath, boolean isBinary, boolean isResume, boolean isOverwrite, boolean isValidate) throws IOException
{
if(null != localFilePath && null != remoteParsedFilePath)
{
this.ftpClient.enterLocalPassiveMode();
if(!this.ftpClient.setFileType(isBinary ? FTP.BINARY_FILE_TYPE : FTP.ASCII_FILE_TYPE))
{
throw new IOException("upload error: set file type failed");
}
File localFile = new File(localFilePath);
if(isOverwrite && !isResume)
{
this.mkdirRecursivelyByFilePath(remoteParsedFilePath);
this.put(localFile, remoteParsedFilePath.getPath(), 0L, isValidate);
}
else
{
FTPFile ftpFile = this.lstat(remoteParsedFilePath.getPath());
if(null != ftpFile)
{
if(ftpFile.isFile())
{
if(isOverwrite)
{
long remoteSize = ftpFile.getSize();
long localSize = localFile.length();
if(remoteSize > localSize)
{
this.put(localFile, remoteParsedFilePath.getPath(), 0L, isValidate);
}
else if(remoteSize < localSize)
{
this.put(localFile, remoteParsedFilePath.getPath(), remoteSize, isValidate);
}
}
}
else
{
throw new IOException("upload error: remote file exist but not a regular file");
}
}
else
{
this.mkdirRecursivelyByFilePath(remoteParsedFilePath);
this.put(localFile, remoteParsedFilePath.getPath(), 0L, isValidate);
}
}
}
}
/**
* 上传文件
* @param localFile
* @param remoteFilePath
* @param offset
* @param isValidate
* @throws IOException
*/
private void put(File localFile, String remoteFilePath, long offset, boolean isValidate) throws IOException
{
if(null != localFile && null != remoteFilePath)
{
RandomAccessFile raf = null;
OutputStream os = null;
try
{
if(0 >= offset)
{
this.rm(remoteFilePath);
}
//raf和os的创建顺序需要关注,如果反过来,则本地raf创建失败时,仍然会在远程创建一个新文件
raf = this.getRandomAccessFile(localFile.getAbsolutePath(), "r", offset);
os = this.getOutputStream(remoteFilePath, offset);
byte[] bytes = new byte[BUFFERED_SIZE];
int count;
while(-1 != (count = raf.read(bytes)))
{
os.write(bytes, 0, count);
}
os.flush();
}
finally
{
MyDocumentManager.close(raf);
MyDocumentManager.close(os);
}
if(this.ftpClient.completePendingCommand())
{
if(isValidate)
{
FTPFile ftpFile = this.lstat(remoteFilePath);
if(null == ftpFile || !ftpFile.isFile() || ftpFile.getSize() != localFile.length())
{
throw new IOException("upload error: upload finished but validate failed");
}
}
}
else
{
throw new IOException("upload failed");
}
}
}
/**
* 递归创建文件夹
* ftpClient自身似乎已经能够递归创建文件夹,但不确定是否所有的ftp服务器都支持递归创建文件夹,因此此处另行实现递归操作
* @param remoteParsedFilePath
* @throws IOException
*/
private void mkdirRecursivelyByFilePath(RemoteParsedPath remoteParsedFilePath) throws IOException
{
if(null != remoteParsedFilePath)
{
FTPFile ftpFile = this.lstat(remoteParsedFilePath.getParentPath());
if(null == ftpFile || !ftpFile.isDirectory())
{
this.mkdir(remoteParsedFilePath.getParentPaths());
}
}
}
/**
* 递归创建文件夹
* ftpClient自身似乎已经能够递归创建文件夹,但不确定是否所有的ftp服务器都支持递归创建文件夹,因此此处另行实现递归操作
* @param remoteParsedDirectoryPath
* @throws IOException
*/
private void mkdirRecursivelyByDirectoryPath(RemoteParsedPath remoteParsedDirectoryPath) throws IOException
{
if(null != remoteParsedDirectoryPath)
{
FTPFile ftpFile = this.lstat(remoteParsedDirectoryPath.getPath());
if(null == ftpFile || !ftpFile.isDirectory())
{
this.mkdir(remoteParsedDirectoryPath.getParentPaths());
this.mkdir(remoteParsedDirectoryPath.getPath());
}
}
}
/**
* 创建文件夹
* @param parentPaths
* @throws IOException
*/
private void mkdir(Collection<String> parentPaths) throws IOException
{
if(null != parentPaths)
{
for(String parentPath : parentPaths)
{
this.mkdir(parentPath);
}
}
}
/**
* 创建文件夹
* @param remoteDirectoryPath
* @throws IOException
*/
private void mkdir(String remoteDirectoryPath) throws IOException
{
if(null != remoteDirectoryPath && !remoteDirectoryPath.isEmpty())
{
if(!this.makeDirectory(remoteDirectoryPath))
{
FTPFile ftpFile = this.lstat(remoteDirectoryPath);
if(null == ftpFile || !ftpFile.isDirectory())
{
throw new IOException("make directory failed");
}
}
}
}
/**
* 创建文件夹
* @param remoteDirectoryPath
* @return
* @throws IOException
*/
private boolean makeDirectory(String remoteDirectoryPath) throws IOException
{
boolean rs = false;
if(null != remoteDirectoryPath && !remoteDirectoryPath.isEmpty())
{
rs = this.ftpClient.makeDirectory(this.translateToRemote(remoteDirectoryPath));
}
return rs;
}
/**
* 删除文件和文件夹
* 支持删除非空文件夹
* @param remotePath
* @throws IOException
*/
private void rmAll(String remotePath) throws IOException
{
if(null != remotePath)
{
FTPFile ftpFile = this.lstat(remotePath);
if(null != ftpFile)
{
if(ftpFile.isDirectory())
{
Collection<MyFtpFile> myFtpFiles = this.ls(remotePath, true);
List<MyFtpFile> myFtpFileList = this.sort(myFtpFiles);
if(null != myFtpFileList)
{
for(MyFtpFile myFtpFile : myFtpFileList)
{
if(myFtpFile.isDir())
{
this.rmdir(myFtpFile.getPath());
}
else
{
this.rm(myFtpFile.getPath());
}
}
}
this.rmdir(remotePath);
}
else
{
this.rm(remotePath);
}
}
}
}
/**
* 删除文件
* @param remoteFilePath
* @throws IOException
*/
private void rm(String remoteFilePath) throws IOException
{
if(null != remoteFilePath && !remoteFilePath.isEmpty())
{
if(!this.deleteFile(remoteFilePath))
{
FTPFile ftpFile = this.lstat(remoteFilePath);
if(null != ftpFile)
{
throw new IOException("delete file failed");
}
}
}
}
/**
* 删除文件
* @param remoteFilePath
* @return
* @throws IOException
*/
private boolean deleteFile(String remoteFilePath) throws IOException
{
boolean rs = false;
if(null != remoteFilePath && !remoteFilePath.isEmpty())
{
rs = this.ftpClient.deleteFile(this.translateToRemote(remoteFilePath));
}
return rs;
}
/**
* 删除文件夹
* 仅能删除空文件夹
* @param remoteDirectoryPath
* @throws IOException
*/
private void rmdir(String remoteDirectoryPath) throws IOException
{
if(null != remoteDirectoryPath && !remoteDirectoryPath.isEmpty())
{
if(!this.removeDirectory(remoteDirectoryPath))
{
FTPFile ftpFile = this.lstat(remoteDirectoryPath);
if(null != ftpFile)
{
throw new IOException("remove directory failed");
}
}
}
}
/**
* 删除文件夹
* @param remoteDirectoryPath
* @return
* @throws IOException
*/
private boolean removeDirectory(String remoteDirectoryPath) throws IOException
{
boolean rs = false;
if(null != remoteDirectoryPath && !remoteDirectoryPath.isEmpty())
{
rs = this.ftpClient.removeDirectory(this.translateToRemote(remoteDirectoryPath));
}
return rs;
}
/**
* 文件排序
* 将文件按照名称倒序排序以方便非空文件夹的递归删除操作
* @param ftpFiles
* @return
*/
private List<MyFtpFile> sort(Collection<MyFtpFile> ftpFiles)
{
List<MyFtpFile> rs = null;
if(null != ftpFiles)
{
rs = new ArrayList<MyFtpFile>();
rs.addAll(ftpFiles);
Collections.sort(rs, MyFtpFile.COMPARATOR_PATH_DESC);
}
return rs;
}
/**
* 设置文件的修改时间
* @param remoteFilePath
* @param lastModifiedText
* @throws IOException
*/
private void setMtime(String remoteFilePath, String lastModifiedText) throws IOException
{
if(null != remoteFilePath && !remoteFilePath.isEmpty() && null != lastModifiedText)
{
if(!this.ftpClient.setModificationTime(this.translateToRemote(remoteFilePath), lastModifiedText))
{
throw new IOException("set modification time failed");
}
}
}
/**
* 列出文件列表
* @param remotePath
* @param isRecursive
* @return
* @throws IOException
*/
private Collection<MyFtpFile> ls(String remotePath, boolean isRecursive) throws IOException
{
Collection<MyFtpFile> rs = null;
if(null != remotePath && !remotePath.isEmpty())
{
FTPFile ftpFile = this.lstat(remotePath);
if(null != ftpFile)
{
rs = new ArrayList<MyFtpFile>();
if(!ftpFile.isDirectory())
{
rs.add(new MyFtpFile(remotePath, ftpFile));
}
else
{
FTPFile[] ftpFiles = this.ls(remotePath);
if(null != ftpFiles)
{
for(FTPFile subFtpFile : ftpFiles)
{
String name = new RemoteParsedPath(this.translateToLocal(subFtpFile.getName())).getName();
MyFtpFile myFtpFile = new MyFtpFile(remotePath + "/" + name, subFtpFile);
rs.add(myFtpFile);
if(isRecursive && myFtpFile.isDir())
{
Collection<MyFtpFile> myFtpFiles = this.ls(myFtpFile.getPath(), isRecursive);
if(null != myFtpFiles)
{
rs.addAll(myFtpFiles);
}
}
}
}
}
}
}
return rs;
}
/**
* 列出文件列表
* 返回的FTPFile其getName返回的是相对路径
* @param remotePath
* @return
* @throws IOException
*/
private FTPFile[] ls(String remotePath) throws IOException
{
FTPFile[] rs = null;
if(null != remotePath && !remotePath.isEmpty())
{
MyLsAllEntryFilter filter = new MyLsAllEntryFilter();
rs = this.ftpClient.mlistDir(this.translateToRemote(remotePath), filter);
}
return rs;
}
/**
* 查询文件信息
* 返回的FTPFile其getName返回的是绝对路径
* @param remotePath
* @return
* @throws IOException
*/
private FTPFile lstat(String remotePath) throws IOException
{
FTPFile rs = null;
if(null != remotePath && !remotePath.isEmpty())
{
rs = this.ftpClient.mlistFile(this.translateToRemote(remotePath));
}
return rs;
}
/**
* 获取输入流
* 必须先设置偏移量然后再获取流,否则偏移量设置无效
* @param remoteFilePath
* @param offset
* @return
* @throws IOException
*/
private InputStream getInputStream(String remoteFilePath, long offset) throws IOException
{
InputStream rs = null;
if(null != remoteFilePath)
{
if(0 < offset)
{
this.ftpClient.setRestartOffset(offset);
}
rs = this.ftpClient.retrieveFileStream(this.translateToRemote(remoteFilePath));
if(null == rs)
{
throw new IOException("open inputStream failed");
}
}
return rs;
}
/**
* 获取输出流
* 必须先设置偏移量然后再获取流,否则偏移量设置无效
* @param remoteFilePath
* @param offset
* @return
* @throws IOException
*/
private OutputStream getOutputStream(String remoteFilePath, long offset) throws IOException
{
OutputStream rs = null;
if(null != remoteFilePath)
{
if(0 < offset)
{
this.ftpClient.setRestartOffset(offset);
}
rs = this.ftpClient.appendFileStream(this.translateToRemote(remoteFilePath));
if(null == rs)
{
throw new IOException("open outputStream failed, maybe the ftp server is not writable");
}
}
return rs;
}
/**
* 获取随机访问文件
* @param localFilePath
* @param mode
* @param offset
* @return
* @throws IOException
*/
private RandomAccessFile getRandomAccessFile(String localFilePath, String mode, long offset) throws IOException
{
RandomAccessFile rs = null;
if(null != localFilePath)
{
if(null == mode)
{
mode = "r";
}
try
{
rs = new RandomAccessFile(localFilePath, mode);
if(0 < offset)
{
rs.seek(offset);
}
}
catch(Exception e)
{
throw new IOException(e);
}
}
return rs;
}
/**
* 创建规范化的远程路径
* @param remotePath
* @return
*/
private RemoteParsedPath createRemoteParsedPath(String remotePath)
{
RemoteParsedPath rs = null;
if(null != remotePath)
{
rs = new RemoteParsedPath(remotePath);
}
return rs;
}
/**
* 将路径转换为远程ftp能够识别的正确编码的路径
* @param path
* @return
* @throws IOException
*/
private String translateToRemote(String path) throws IOException
{
String rs = null;
if(null != path)
{
rs = new String(path.getBytes(this.localEncoding), ENCODING_EN);
}
return rs;
}
/**
* 将远程ftp获取的路径转换为本地能够识别的正确编码的路径
* @param path
* @return
* @throws IOException
*/
private String translateToLocal(String path) throws IOException
{
String rs = null;
if(null != path)
{
rs = new String(path.getBytes(ENCODING_EN), this.localEncoding);
}
return rs;
}
}
三. 其他辅助类
以下仅列出一些重要的辅助类。
1. RemoteParsedPath
package com.dancen.util.ftp;
import java.util.ArrayList;
import java.util.Collection;
import com.dancen.util.MyStringUtil;
/**
* 规范化的远程路径
* 规范化表示远程路径:
* 1. 远程路径不得使用相对路径,所有相对路径会被强制转换为绝对路径。
* 2. 规范化路径分隔符。
* 3. 将路径解析以方便获取文件名等。
*
* @author dancen
*
*/
public class RemoteParsedPath
{
//略
}
2. MyLsAllEntrySelector
package com.dancen.util.ftp;
import java.util.ArrayList;
import java.util.Collection;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.ChannelSftp.LsEntrySelector;
/**
*
* @author dancen
*
*/
public class MyLsAllEntrySelector implements LsEntrySelector
{
//略
}
3. MyLsAllEntryFilter
package com.dancen.util.ftp;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPFileFilter;
/**
*
* @author dancen
*
*/
public class MyLsAllEntryFilter implements FTPFileFilter
{
//略
}
4.MyFtpFile
package com.dancen.util.ftp;
import java.util.Comparator;
import org.apache.commons.net.ftp.FTPFile;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.SftpATTRS;
/**
* 远程文件信息
* 远程文件的类型存在多种表示方式,如远程ftp的文件类为FTPFile,而远程sftp的文件类则为LsEntry或者SftpATTRS,为了方便
* 远程文件的处理,此处将远程文件的信息表示类统一封装。
*
* @author dancen
*
*/
public class MyFtpFile implements Comparable<MyFtpFile>
{
public static final Comparator<MyFtpFile> COMPARATOR_PATH;
public static final Comparator<MyFtpFile> COMPARATOR_PATH_DESC;
private String name;
private String path;
private long size;
private int lastModified; //文件最后修改时间, 仅精确至秒
private boolean isBlk;
private boolean isChr;
private boolean isDir;
private boolean isFifo;
private boolean isLink;
private boolean isReg;
private boolean isSock;
static
{
COMPARATOR_PATH = new Comparator<MyFtpFile>()
{
@Override
public int compare(MyFtpFile srcFtpFile, MyFtpFile targetFtpFile)
{
int rs = 0;
if(null == srcFtpFile && null == targetFtpFile)
{
rs = 0;
}
else if(null == srcFtpFile)
{
rs = -1;
}
else if(null == targetFtpFile)
{
rs = 1;
}
else
{
rs = srcFtpFile.getPath().compareTo(targetFtpFile.getPath());
}
return rs;
}
};
COMPARATOR_PATH_DESC = new Comparator<MyFtpFile>()
{
@Override
public int compare(MyFtpFile srcFtpFile, MyFtpFile targetFtpFile)
{
return COMPARATOR_PATH.compare(targetFtpFile, srcFtpFile);
}
};
}
public static void main(String[] args)
{
String directoryPath = "/";
if(directoryPath.endsWith("/"))
{
directoryPath = directoryPath.substring(0, directoryPath.length() - 1);
}
System.out.println(directoryPath);
}
/**
* FTPFile.getName方法返回的文件路径没有标准,有时为绝对路径,有时为相对路径,具体和
* FTPFile的生成方式有关,例如mlistFile方法产生的FTPFile,其getName方法返回绝对路径,而
* mlistDir方法产生的FTPFile,其 getName方法返回相对路径,因此,path应由外部处理好之后提供
* @param path 文件的路径
* @param ftpFile
*/
public MyFtpFile(String path, FTPFile ftpFile)
{
if(null == path)
{
throw new IllegalArgumentException("the path is null");
}
if(null == ftpFile)
{
throw new IllegalArgumentException("the ftpFile is null");
}
RemoteParsedPath remoteParsedPath = new RemoteParsedPath(path);
this.name = remoteParsedPath.getName();
this.path = remoteParsedPath.getPath();
this.size = ftpFile.getSize();
this.lastModified = (int)(ftpFile.getTimestamp().getTimeInMillis() / 1000);
this.isBlk = false;
this.isChr = false;
this.isDir = ftpFile.isDirectory();
this.isFifo = false;
this.isLink = ftpFile.isSymbolicLink();
this.isReg = ftpFile.isFile();
this.isSock = false;
}
public MyFtpFile(String path, SftpATTRS sftpATTRS)
{
if(null == path)
{
throw new IllegalArgumentException("the path is null");
}
if(null == sftpATTRS)
{
throw new IllegalArgumentException("the sftpATTRS is null");
}
RemoteParsedPath remoteParsedPath = new RemoteParsedPath(path);
this.name = remoteParsedPath.getName();
this.path = remoteParsedPath.getPath();
this.size = sftpATTRS.getSize();
this.lastModified = sftpATTRS.getMTime();
this.isBlk = sftpATTRS.isBlk();
this.isChr = sftpATTRS.isChr();
this.isDir = sftpATTRS.isDir();
this.isFifo = sftpATTRS.isFifo();
this.isLink = sftpATTRS.isLink();
this.isReg = sftpATTRS.isReg();
this.isSock = sftpATTRS.isSock();
}
/**
*
* @param directoryPath 文件所在文件夹的路径
* @param lsEntry
*/
public MyFtpFile(String directoryPath, LsEntry lsEntry)
{
if(null == directoryPath)
{
throw new IllegalArgumentException("the directoryPath is null");
}
if(null == lsEntry)
{
throw new IllegalArgumentException("the lsEntry is null");
}
directoryPath = directoryPath.trim();
if(directoryPath.endsWith("/"))
{
directoryPath = directoryPath.substring(0, directoryPath.length() - 1);
}
this.name = lsEntry.getFilename();
this.path = directoryPath + "/" + this.name;
this.size = lsEntry.getAttrs().getSize();
this.lastModified = lsEntry.getAttrs().getMTime();
this.isBlk = lsEntry.getAttrs().isBlk();
this.isChr = lsEntry.getAttrs().isChr();
this.isDir = lsEntry.getAttrs().isDir();
this.isFifo = lsEntry.getAttrs().isFifo();
this.isLink = lsEntry.getAttrs().isLink();
this.isReg = lsEntry.getAttrs().isReg();
this.isSock = lsEntry.getAttrs().isSock();
}
public String getName()
{
return this.name;
}
public String getPath()
{
return this.path;
}
public long getSize()
{
return this.size;
}
public int getLastModified()
{
return this.lastModified;
}
public boolean isBlk()
{
return this.isBlk;
}
public boolean isChr()
{
return this.isChr;
}
public boolean isDir()
{
return this.isDir;
}
public boolean isFifo()
{
return this.isFifo;
}
public boolean isLink()
{
return this.isLink;
}
public boolean isReg()
{
return this.isReg;
}
public boolean isSock()
{
return this.isSock;
}
@Override
public int compareTo(MyFtpFile ftpFile)
{
int rs = 0;
if(null == ftpFile)
{
rs = -1;
}
else
{
rs = this.path.compareTo(ftpFile.getPath());
}
return rs;
}
}
5. FileEntry
package com.dancen.util.ftp;
/**
*
* @author dancen
*
*/
public class FileEntry
{
//略
}
6. FtpException
package com.dancen.util.ftp;
/**
* 远程操作过程中产生的异常
* 统一化表示ftp和sftp远程连接、上传、下载、遍历、删除等操作过程中可能出现的各种异常。
*
* @author dancen
*
*/
public class FtpException extends Exception
{
//略
}