背景
近期在工作中需要实现文件的上传与下载,一开始打算使用一些高级的文件系统,比如:FastDFS,GlusterFS,CephFS,这些高级厉害的文件存储系统,当然博主也花了两周的时间把这三个FS都玩了一遍。个人认为FastDFS使用以及部署最简单,比较适合存储图片以及中小型文件(<500M),毕竟是国产框架(点赞);而GlusterFS和CephFS,GlusterFS部署和Java对接起来较为简单,CephFS部署很费劲,对Java使用不太友好(不太方便)。当然很大原因是博主技术不够,玩不过来。在使用这些框架之后,Leader感觉公司目前的技术储备还不够成熟,最终使用常用的SFTP实现文件上传和下载。背景介绍就到这里,接下来实战吧!
SFTP介绍
- SFTP是Secure File Transfer Protocol的缩写,安全文件传送协议。可以为传输文件提供一种安全的加密方法,语法几乎和FTP一致。
- 相比于FTP,SFTP更安全,但更安全带来副作用就是的效率比FTP要低些。
- SFTP是SSH的一部分,内部是采用SSH连接,所以在以下代码中进行文件的操作都会先cd到SFTP存放文件的根路径下。
- Reference:SFTP与FTP比较、浅谈SFTP与FTP。
实战
1. 相关依赖(基于SpringBoot)
org.springframework.boot
spring-boot-starter
org.projectlombok
lombok
true
com.jcraft
jsch
0.1.54
org.springframework.boot
spring-boot-starter-test
test
org.apache.commons
commons-lang3
2. 相关配置
#============================================================================
# SFTP Client Setting
#============================================================================
# 协议
sftp.client.protocol=sftp
# ip地址
sftp.client.host=127.0.0.1
# 端口
sftp.client.port=22
# 用户名
sftp.client.username=sftp
# 密码
sftp.client.password=sftp
# 根路径
sftp.client.root=/home/sftp/
# 密钥文件路径
sftp.client.privateKey=
# 密钥的密码
sftp.client.passphrase=
#
sftp.client.sessionStrictHostKeyChecking=no
# session连接超时时间
sftp.client.sessionConnectTimeout=15000
# channel连接超时时间
sftp.client.channelConnectedTimeout=15000
- 这里暂时没有使用到使用加密密钥的方式登陆,所以暂不填写
3. 将application.properties中配置转为一个Bean
@Getter
@Setter
@Component
@ConfigurationProperties(ignoreUnknownFields = false, prefix = "sftp.client")
public class SftpProperties {
private String host;
private Integer port;
private String protocol;
private String username;
private String password;
private String root;
private String privateKey;
private String passphrase;
private String sessionStrictHostKeyChecking;
private Integer sessionConnectTimeout;
private Integer channelConnectedTimeout;
}
4. 将上传下载文件封装成Service
- FileSystemService
/**
* @author jason.tang
* @create 2019-03-07 13:33
* @description
*/
public interface FileSystemService {
boolean uploadFile(String targetPath, InputStream inputStream) throws Exception;
boolean uploadFile(String targetPath, File file) throws Exception;
File downloadFile(String targetPath) throws Exception;
boolean deleteFile(String targetPath) throws Exception;
}
- 实现类:FileSystemServiceImpl(此处省略相关上传下载代码)
/**
* @author jason.tang
* @create 2019-03-07 13:33
* @description
*/
@Slf4j
@Service("fileSystemService")
public class FileSystemServiceImpl implements FileSystemService {
@Autowired
private SftpProperties config;
// 设置第一次登陆的时候提示,可选值:(ask | yes | no)
private static final String SESSION_CONFIG_STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";
/**
* 创建SFTP连接
* @return
* @throws Exception
*/
private ChannelSftp createSftp() throws Exception {
JSch jsch = new JSch();
log.info("Try to connect sftp[" + config.getUsername() + "@" + config.getHost() + "], use password[" + config.getPassword() + "]");
Session session = createSession(jsch, config.getHost(), config.getUsername(), config.getPort());
session.setPassword(config.getPassword());
session.connect(config.getSessionConnectTimeout());
log.info("Session connected to {}.", config.getHost());
Channel channel = session.openChannel(config.getProtocol());
channel.connect(config.getChannelConnectedTimeout());
log.info("Channel created to {}.", config.getHost());
return (ChannelSftp) channel;
}
/**
* 加密秘钥方式登陆
* @return
*/
private ChannelSftp connectByKey() throws Exception {
JSch jsch = new JSch();
// 设置密钥和密码 ,支持密钥的方式登陆
if (StringUtils.isNotBlank(config.getPrivateKey())) {
if (StringUtils.isNotBlank(config.getPassphrase())) {
// 设置带口令的密钥
jsch.addIdentity(config.getPrivateKey(), config.getPassphrase());
} else {
// 设置不带口令的密钥
jsch.addIdentity(config.getPrivateKey());
}
}
log.info("Try to connect sftp[" + config.getUsername() + "@" + config.getHost() + "], use private key[" + config.getPrivateKey()
+ "] with passphrase[" + config.getPassphrase() + "]");
Session session = createSession(jsch, config.getHost(), config.getUsername(), config.getPort());
// 设置登陆超时时间
session.connect(config.getSessionConnectTimeout());
log.info("Session connected to " + config.getHost() + ".");
// 创建sftp通信通道
Channel channel = session.openChannel(config.getProtocol());
channel.connect(config.getChannelConnectedTimeout());
log.info("Channel created to " + config.getHost() + ".");
return (ChannelSftp) channel;
}
/**
* 创建session
* @param jsch
* @param host
* @param username
* @param port
* @return
* @throws Exception
*/
private Session createSession(JSch jsch, String host, String username, Integer port) throws Exception {
Session session = null;
if (port <= 0) {
session = jsch.getSession(username, host);
} else {
session = jsch.getSession(username, host, port);
}
if (session == null) {
throw new Exception(host + " session is null");
}
session.setConfig(SESSION_CONFIG_STRICT_HOST_KEY_CHECKING, config.getSessionStrictHostKeyChecking());
return session;
}
/**
* 关闭连接
* @param sftp
*/
private void disconnect(ChannelSftp sftp) {
try {
if (sftp != null) {
if (sftp.isConnected()) {
sftp.disconnect();
} else if (sftp.isClosed()) {
log.info("sftp is closed already");
}
if (null != sftp.getSession()) {
sftp.getSession().disconnect();
}
}
} catch (JSchException e) {
e.printStackTrace();
}
}
}
5. 上传文件
- 5.1 将inputStream上传到指定路径下(单级或多级目录)
@Override
public boolean uploadFile(String targetPath, InputStream inputStream) throws Exception {
ChannelSftp sftp = this.createSftp();
try {
sftp.cd(config.getRoot());
log.info("Change path to {}", config.getRoot());
int index = targetPath.lastIndexOf("/");
String fileDir = targetPath.substring(0, index);
String fileName = targetPath.substring(index + 1);
boolean dirs = this.createDirs(fileDir, sftp);
if (!dirs) {
log.error("Remote path error. path:{}", targetPath);
throw new Exception("Upload File failure");
}
sftp.put(inputStream, fileName);
return true;
} catch (Exception e) {
log.error("Upload file failure. TargetPath: {}", targetPath, e);
throw new Exception("Upload File failure");
} finally {
this.disconnect(sftp);
}
}
- 5.2 创建多级目录
private boolean createDirs(String dirPath, ChannelSftp sftp) {
if (dirPath != null && !dirPath.isEmpty()
&& sftp != null) {
String[] dirs = Arrays.stream(dirPath.split("/"))
.filter(StringUtils::isNotBlank)
.toArray(String[]::new);
for (String dir : dirs) {
try {
sftp.cd(dir);
log.info("Change directory {}", dir);
} catch (Exception e) {
try {
sftp.mkdir(dir);
log.info("Create directory {}", dir);
} catch (SftpException e1) {
log.error("Create directory failure, directory:{}", dir, e1);
e1.printStackTrace();
}
try {
sftp.cd(dir);
log.info("Change directory {}", dir);
} catch (SftpException e1) {
log.error("Change directory failure, directory:{}", dir, e1);
e1.printStackTrace();
}
}
}
return true;
}
return false;
}
- 5.3 将文件上传到指定目录
@Override
public boolean uploadFile(String targetPath, File file) throws Exception {
return this.uploadFile(targetPath, new FileInputStream(file));
}
6. 下载文件
@Override
public File downloadFile(String targetPath) throws Exception {
ChannelSftp sftp = this.createSftp();
OutputStream outputStream = null;
try {
sftp.cd(config.getRoot());
log.info("Change path to {}", config.getRoot());
File file = new File(targetPath.substring(targetPath.lastIndexOf("/") + 1));
outputStream = new FileOutputStream(file);
sftp.get(targetPath, outputStream);
log.info("Download file success. TargetPath: {}", targetPath);
return file;
} catch (Exception e) {
log.error("Download file failure. TargetPath: {}", targetPath, e);
throw new Exception("Download File failure");
} finally {
if (outputStream != null) {
outputStream.close();
}
this.disconnect(sftp);
}
}
7. 删除文件
/**
* 删除文件
* @param targetPath
* @return
* @throws Exception
*/
@Override
public boolean deleteFile(String targetPath) throws Exception {
ChannelSftp sftp = null;
try {
sftp = this.createSftp();
sftp.cd(config.getRoot());
sftp.rm(targetPath);
return true;
} catch (Exception e) {
log.error("Delete file failure. TargetPath: {}", targetPath, e);
throw new Exception("Delete File failure");
} finally {
this.disconnect(sftp);
}
}
8. 最后
- 具体测试请看源码,这里不贴出相关测试,避免篇幅太长。
- 涉及到对文件的操作,一定记得将流关闭。
- 在使用中比如下载文件,请将生成的文件在使用后删除(file.delete()),避免在服务器中占据大量资源。
- application.proerties中SFTP相关配置,请自行更换。如有不对之处,请指出,感谢阅读!