Springboot + layui + FTP文件上传删除 + HTTP文件下载预览 + pdf.js文件预览(项目实战总结)

0、需求及前言

我们的需求是实现文件的增删查改和预览功能。服务器有两台,一个是文件服务器,专门用来存储文件;另一个是用来部署项目的服务器。

小白啊,IO操作什么的基本没弄过,网络学的也不好,就搞这个操作,颇费心力。在网上扒了无数的帖子,换了很多个版本,最终还是实现了。总结下来其实也没有那么那么难,下面把关键的实现过程分享出来。不足之处请多指教。

前端框架:Layui
前端工具:pdf.js
协议:FTP、HTTP
后端:Springboot
需求:把上传、删除/替换按钮和预览下载功能放在数据表格中。文件上传至文件服务器。点击文件名时,对pdf文件进行预览,其他格式文件直接下载。
使用范围:内网用户(外网连接可以在此基础上另外了解)
最终效果:实现效果

1、前端,上传按钮嵌入数据表格中

这部分我写到了另一个博客,Layui 数据表格嵌套文件上传按钮,根据行数据id上传文件

2、利用IIS部署FTP文件服务器

在网上找了两个教程,跟我当时设置的流程差不多:

只要在网页上输入ftp://192.168.xxx.xxx:端口,然后输入用户名和密码(如果有的话)可以看到文件列表,就说明部署成功了。

我遇到的问题:

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 中一个获取文件流,其他操作可以参考以下两位大佬的代码:

  1. JAVA FTPClient FTP简单操作
  2. java上传、下载、预览、删除ftp服务器上的文件

4、FTP遇到的问题和解决方案

  1. FtpClient.storeFile返回false
    这个问题网上的解决方案一般是因为没有设置服务器被动连接模式,或者是因为中文文件名问题,可以参考这个回答:https://www.cnblogs.com/xiangpiaopiao2011/archive/2012/02/28/2371679.html
    但是我没有解决。
    我在公司是用笔记本连内网wifi的,可以创建文件夹,但是文件传输就是false。在网上找了半天,很多回答都是更改成被动模式,但我这边不适用。后来同事测试了一下发现他的可以传文件上去???
    残念。
    后来考虑到操作文件的人(公司内部用)都是用的内网网线,就直接用这个版本的代码没有再改了。
  2. 文件夹和文件乱码的问题
    解决办法:
    1、开启服务器对UTF-8的支持,但是有的服务器是不支持UTF-8的,这时就只好用本地GBK的编码。
    2、FTP协议规定文件名编码为iso-8859-1,所以上传的文件目录或文件名需要转码。
    就是上面带有new String(xxxx.getBytes(LOCAL_CHARSET),SERVER_CHARSET) 的那些。
  3. 目标目录已创建,但是文件没有传到目录中去,而是传到了根目录上
    这仍然是编码的问题导致的。加上那句new String(xxxx.getBytes(LOCAL_CHARSET),SERVER_CHARSET) 就好了。

预览的问题困扰了我很久,因为不知道怎么实现。网上的教程大多是从服务器本地拿文件,少了文件服务器,实现方式跟我这不一样。后来看到pdf.js这个插件,只要获取到文件输入流就可以实现预览。然后在获取文件流上又弯弯绕绕一大圈。中间的心路历程堪称心酸,吃了没文化的苦。

这里我尝试了两个版本,一是FTP+临时文件的版本,二是HTTP版本。方法一在生成临时文件时耗费时间,有些影响用户体验。推荐使用HTTP。(这里因为工期有点紧了,上传那里没有再学习HTTP方法,日后有机会再更新)(用FTP不用临时文件应该也是可行的,不过我没找到合适的方法)。

5、预览PDF文件V1.0:FTP+临时文件

pdf.js的用法很简单,就是下载,然后放到项目静态文件目录中,可以参考这两个文章:
https://blog.csdn.net/semial/article/details/89510312
https://blog.csdn.net/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

====================================

基本内容就是这些。
不足之处,请多指教。

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值