文件管理
0、需求及前言
我们的需求是实现文件的增删查改和预览功能。服务器有两台,一个是文件服务器,专门用来存储文件;另一个是用来部署项目的服务器。
小白啊,IO操作什么的基本没弄过,网络学的也不好,就搞这个操作,颇费心力。在网上扒了无数的帖子,换了很多个版本,最终还是实现了。总结下来其实也没有那么那么难,下面把关键的实现过程分享出来。不足之处请多指教。
前端框架:Layui
前端工具:pdf.js
协议:FTP、HTTP
后端:Springboot
需求:把上传、删除/替换按钮和预览下载功能放在数据表格中。文件上传至文件服务器。点击文件名时,对pdf文件进行预览,其他格式文件直接下载。
使用范围:内网用户(外网连接可以在此基础上另外了解)
最终效果:
1、前端,上传按钮嵌入数据表格中
这部分我写到了另一个博客,Layui 数据表格嵌套文件上传按钮,根据行数据id上传文件。
2、利用IIS部署FTP文件服务器
在网上找了两个教程,跟我当时设置的流程差不多:
只要在网页上输入ftp://192.168.xxx.xxx:端口,然后输入用户名和密码(如果有的话)可以看到文件列表,就说明部署成功了。
我遇到的问题:
- 配置FTP服务时,登录后无法添加文件。
错误信息:“将文件复制到FTP服务器时发生错误”
解决办法: https://blog.youkuaiyun.com/hello_world_qwp/article/details/78717336
3、后台FTP连接和文件操作
在这里走了许多弯路。
网上有许多这种代码,大致是相同的,但是又有细微差别。我创建了一个工具类,便于其他controller调用。先把调试正常的代码放出来。遇到的问题后面会提到。
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.log4j.Logger;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.net.SocketException;
import java.util.Date;
/**
* @Author 27号白开水
* @Date 2020/6/21 15:54
*/
public class FtpUtil {
private static Logger logger = Logger.getLogger(FtpUtil.class);
//ftp服务器ip地址
private static final String FTP_ADDRESS = "192.168.xxx.xxx";
//端口号
private static final int FTP_PORT = 2333;
//用户名
private static final String FTP_USERNAME = "upload";
//密码
private static final String FTP_PASSWORD = "123456";
//本地字符编码
private static String LOCAL_CHARSET = "GBK";
// FTP协议里面,规定文件名编码为iso-8859-1
private static final String SERVER_CHARSET = "ISO-8859-1";
//附件路径,这里没用到
//private static String FTP_BASEPATH = "";
//连接ftp, 获取到FTPClient对象
public static FTPClient getFTPClient(){
FTPClient ftp = new FTPClient();
try {
int reply;
ftp.connect(FTP_ADDRESS, FTP_PORT);//连接FTP服务器
ftp.login(FTP_USERNAME, FTP_PASSWORD);//登录
ftp.setConnectTimeout(50000);// 设置连接超时时间,5000毫秒
if(!FTPReply.isPositiveCompletion(ftp.getReplyCode())){
logger.info("未连接到FTP,用户名或密码错误");
ftp.disconnect();
return ftp;
}else {
logger.info("FTP连接成功");
}
// 开启服务器对UTF-8的支持,如果服务器支持就用UTF-8编码,否则就使用本地编码(GBK)
if (FTPReply.isPositiveCompletion(ftp.sendCommand("OPTS UTF8", "ON"))) {
LOCAL_CHARSET = "UTF-8";
}
ftp.setControlEncoding(LOCAL_CHARSET);//设置字符集编码方式
} catch (SocketException e) {
e.printStackTrace();
logger.info("FTP的IP地址可能错误,请正确配置");
} catch (IOException e) {
e.printStackTrace();
logger.info("FTP的端口错误,请正确配置");
}
return ftp;
}
//关闭FTP方法
public static boolean closeFTP(FTPClient ftp){
try {
ftp.logout();
} catch (Exception e) {
logger.error("FTP关闭失败");
}finally{
if (ftp.isConnected()) {
try {
ftp.disconnect();
} catch (IOException ioe) {
ioe.printStackTrace();
logger.error("FTP关闭失败");
}
}
}
return false;
}
//上传文件
public static boolean uploadFile(FTPClient ftp, MultipartFile multipartFile, String filePath) throws IOException {
//获取上传的文件流
InputStream inputStream = multipartFile.getInputStream();
String fileName = multipartFile.getOriginalFilename();
boolean success = true;
try {
ftp.enterLocalPassiveMode();//设置被动传输
ftp.setFileType(FTPClient.BINARY_FILE_TYPE);//设置文件传输模式为二进制,可以保证传输的内容不会被改变,ASC容易造成文件损坏
String directory = filePath.substring(0, filePath.lastIndexOf("/") + 1);
// 如果远程目录不存在,则递归创建远程服务器目录,这里是用于多层文件夹嵌套新建的情况,如果只有一层,那么只需要 1:跳转目录 2:不存在就新建
if (!directory.equalsIgnoreCase("/") //忽略大小写进行比较
&& !ftp.changeWorkingDirectory(new String(filePath.getBytes(LOCAL_CHARSET),SERVER_CHARSET))) {
int start = 0;
int end = 0;
if (directory.startsWith("/")) {
start = 1;
} else {
start = 0;
}
end = directory.indexOf("/", start);//查询除开头“/”之外的第一个“/”的位置
while (true) {
String subDirectory = filePath.substring(start, end);
if (!ftp.changeWorkingDirectory(subDirectory)) {//跳转子目录
if (ftp.makeDirectory(new String(subDirectory.getBytes(LOCAL_CHARSET),SERVER_CHARSET))) {//新建子文件夹
ftp.changeWorkingDirectory(subDirectory);//再次尝跳转子目录
} else {
System.out.println("创建目录失败");
success = false;
return success;
}
}
start = end + 1;
end = directory.indexOf("/", start);
// 检查所有目录是否创建完毕
if (end <= start) {
break;
}
}
}
//跳转目标目录
ftp.changeWorkingDirectory(filePath);
success = ftp.storeFile(new String(fileName.getBytes(LOCAL_CHARSET),SERVER_CHARSET), inputStream); //存储
if(success){
logger.info("上传成功");
}else{
logger.error("上传失败");
}
} catch (IOException e) {
e.printStackTrace();
logger.error("上传失败");
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return success;
}
//替换文件(实际是先删除后上传)
public static Boolean replaceFile(MultipartFile file, String filePath, String fileName) throws IOException {
Boolean success = false;
FTPClient ftpClient = getFTPClient();
deleteFile(ftpClient, filePath,fileName); //删除文件
uploadFile(ftpClient, file, filePath);
closeFTP(ftpClient);
return success;
}
//删除文件
public static Boolean deleteFile(FTPClient ftpClient, String filePath, String fileName){
boolean flag = false;//转移至目标目录
try {
ftpClient.changeWorkingDirectory(new String(filePath.getBytes(LOCAL_CHARSET), SERVER_CHARSET));//跳转目录
flag = ftpClient.deleteFile(new String(fileName.getBytes(LOCAL_CHARSET), SERVER_CHARSET));//删除文件
if (!flag) {
throw new Exception("FTP附件删除失败!");
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
}
controller或者service调用就是这样:
//文件路径
String filePath = "/2020/";//路径不包含文件名
//调用自定义的FTP工具类上传文件
FTPClient ftpClient = FtpUtil.getFTPClient();
Boolean success = FtpUtil.uploadFile(ftpClient, multipartFile, filePath);//调用工具类上传
System.out.println(success? "上传成功": "上传失败");
FtpUtil.closeFTP(ftpClient);
我这里只是简单地用到了连接、上传、删除,还有5 中一个获取文件流,其他操作可以参考以下两位大佬的代码:
4、FTP遇到的问题和解决方案
- FtpClient.storeFile返回false
这个问题网上的解决方案一般是因为没有设置服务器被动连接模式,或者是因为中文文件名问题,可以参考这个回答:https://www.cnblogs.com/xiangpiaopiao2011/archive/2012/02/28/2371679.html
但是我没有解决。
我在公司是用笔记本连内网wifi的,可以创建文件夹,但是文件传输就是false。在网上找了半天,很多回答都是更改成被动模式,但我这边不适用。后来同事测试了一下发现他的可以传文件上去???
残念。
后来考虑到操作文件的人(公司内部用)都是用的内网网线,就直接用这个版本的代码没有再改了。 - 文件夹和文件乱码的问题
解决办法:
1、开启服务器对UTF-8的支持,但是有的服务器是不支持UTF-8的,这时就只好用本地GBK的编码。
2、FTP协议规定文件名编码为iso-8859-1,所以上传的文件目录或文件名需要转码。
就是上面带有new String(xxxx.getBytes(LOCAL_CHARSET),SERVER_CHARSET) 的那些。 - 目标目录已创建,但是文件没有传到目录中去,而是传到了根目录上
这仍然是编码的问题导致的。加上那句new String(xxxx.getBytes(LOCAL_CHARSET),SERVER_CHARSET) 就好了。
预览的问题困扰了我很久,因为不知道怎么实现。网上的教程大多是从服务器本地拿文件,少了文件服务器,实现方式跟我这不一样。后来看到pdf.js这个插件,只要获取到文件输入流就可以实现预览。然后在获取文件流上又弯弯绕绕一大圈。中间的心路历程堪称心酸,吃了没文化的苦。
这里我尝试了两个版本,一是FTP+临时文件的版本,二是HTTP版本。方法一在生成临时文件时耗费时间,有些影响用户体验。推荐使用HTTP。(这里因为工期有点紧了,上传那里没有再学习HTTP方法,日后有机会再更新)(用FTP不用临时文件应该也是可行的,不过我没找到合适的方法)。
5、预览PDF文件V1.0:FTP+临时文件
pdf.js的用法很简单,就是下载,然后放到项目静态文件目录中,可以参考这两个文章:
https://blog.youkuaiyun.com/semial/article/details/89510312
https://blog.youkuaiyun.com/qq_36537546/article/details/105793577
最终的实现过程是这样的:先从FTP获取文件流,在本地生成一个临时文件,然后用pdf.js渲染临时文件。
现在想来,其实关键在于获取临时文件,pdf.js的用处只是渲染的更好看一些。
下面是关键(所有)代码:
js:
//表头渲染,使点击文件名即可预览
, {field: 'file_name', title: '文件名称', width: 480
, templet: function (d) {
if (d.file_name != null && d.file_name != '' && d.file_name != undefined){
return '<a href="javascript:void(0);" style="color: #0B9EB0;size: 15px;" lay-event="detail">'+d.file_name+'</a>';
}else {
return '未上传';
}
}
}
//对行操作进行监听,调用pdf.js打开临时文件
window.open("/static/js/mes/fileManagement/web/viewer.html?file=" //前半句是pdf.js的viewer.html的路径
+ encodeURIComponent("/allFiles/showDetail?filePath="+filePath));
//后半句是controller的注解路径加传参,filePath是文件服务器路径+文件名。
//然后对URL进行编码,否则会因为出现两个问号而报错
Controller:
@RequestMapping("/showDetail")
public void showDetail(String filePath, HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("文件查看" + filePath);
// 编辑请求头部信息
// 解决请求头跨域问题(IE兼容性 也可使用该方法)
response.setHeader("Access-Control-Allow-Origin", "*");
response.setContentType("application/pdf");
FTPClient ftpClient = FtpUtil.getFTPClient();
FileInputStream inputStream = FtpUtil.getStream(ftpClient, filePath);
byte[] data = null;
data = new byte[inputStream.available()];
inputStream.read(data);
response.getOutputStream().write(data);
inputStream.close();
FtpUtil.closeFTP(ftpClient);
}
FtpUtil工具类:
//获取预览需要的文件流信息
public static FileInputStream getStream(FTPClient ftpClient, String filePath) throws IOException {//filePath是文件夹名加文件名
//在客户端本地生成一个临时文件
File tempFile = new File("E:/","mesTempFile.pdf");
//将预览文件放到临时文件
OutputStream outputStream = new FileOutputStream(tempFile);
ftpClient.retrieveFile(new String(filePath.getBytes(LOCAL_CHARSET), SERVER_CHARSET),outputStream);
outputStream.close();
//读取临时文件的文件流
FileInputStream fileInputStream = new FileInputStream(tempFile);
return fileInputStream;
}
6、预览/下载文件V2.0:HTTP
这里参见我的另一篇博客
====================================
发布时间2020.06.29
更新时间2020.08.05
====================================
基本内容就是这些。
不足之处,请多指教。