博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
无插件实现大文件分片上传,断点续传
阅读量:7060 次
发布时间:2019-06-28

本文共 10435 字,大约阅读时间需要 34 分钟。

文件上传.gif
1. 简介:

本篇文章基于实际项目的开发,将介绍项目中关于大文件分片上传、文件验证、断点续传、手动重试上传等需求的使用场景及实现;

2. 项目需求
  1. 在一个音视频的添加中,既要有音视频的简介(如音视频内容文字介绍、自定义主题名称等一些基本的信息),又要有音视频所需要的多个文件(就像电视剧,一部电视剧有多集一样)。在数据库中具体表现为一对多的关系,即一个视频对应多个文件。下文就以电视剧为例
  2. 如果一个电视剧中,既有上百兆的,也有几十兆的视频,但是如果在不稳定的一个网络环境中,c传输大文件时,客户想先把小的视频上传了,之后再来继续传未传完的大文件声誉部分,这就需要断点续传的功能;
  3. 电视剧中至少有一集(至少有一个文件),无文件的电视剧基本信息无效;
3. 需求分析
  1. 确定电视剧基本信息(自定义名称,内容简介、演员简介、播出时间等)及文件的上传方式

    • 基本信息和音视频文件分开上传(因为在原有的数据库表设计中,文件表是关联于基本信息,所以必须要有音视频主键,才能在数据库添加对应的文件信息),取得基本信息主键之后再去上传文件;
  2. 文件断点续传中,如何分片;文件接收方式;服务器端如何判断是哪个文件的分片;如何拼接各个分片;上传过程中发生意外情况(如断网,关闭浏览器),如何处理?

    • 分片方式: 在客户端进行分片;
    • 服务器端接收方式:使用MultipartFile接收文件
    • 服务器端确定是哪个文件的分片: 在客户端按照一定规则(UUID或其他方式)生成唯一名称,在服务器端直接找到与该名称相同的文件片段;
    • 拼接文件分片: 使用NIO的方式,将分片追加到已有分片的后面;
    • 上传中发生意外:
      A. 断网: 该情况类似于暂停上传,上传到文件处于暂停状态,网络恢复,即可点击继续上传按钮,继续上传;
      B. 关闭浏览器: 在关闭时,给用户提示框,询问是否继续保存,若不保存,则根据视频基本信息表的主键的删除脏数据;
      C. 第一个文件在上传时候,被用户取消或者断网,则服务器端未修改基本信息为有效,并且也未标记该文件为有效记录,可以理解为脏数据,但不需要清理这些数据(在查询的时候,不能查出这些无效记录,可以在更新视频基本信息记录的时候,查找这些脏数据,并清理磁盘上及数据表中的记录);
4. 实现

根据需求,已经确认了先上传基本信息,后上传文件,基本信息的提交(标题、简介、封面)比较简单,只需要在前端提交数据表所需字段,然后后台返回插入的主键即可,所以基本信息的提交及返回不过多说明,仅仅通过前端页面截图及后端部分代码进行简要说明,实现部分主要讲解分析文件上传部分的代码;

说明:本篇文章主要涉及到两张表的操作,两张表的数据结构如下:

DataFile表.png

microClass表.png
  • 4.1.1 前端提交基本信息页面

    提交页面使用模板+原生html+css实现,每个人的页面、所需参数各不相同,所以在前端代码中,没有多大的参考价值,所以直接使用截图,来表现我需要做的工作、传的参数。

1-基本信息.png
  • 4.1.2 前端保存基本信息代码

    可以根据自己的业务使用FormData封装传递的参数

    //点击保存按钮后保存数据      function save_microClass() {              //获取form的基本信息              var classTitle = $("#classTitle").val(),              classDes = $("#classDes").val(),              coverFile = document.getElementById("coverFileName").files[0],              //构造一个新表单,FormData是HTML5新增的,因为基本信息中存在封面图片,所以需要使用表单提交数据                          var form = new FormData();                          form.append("title", classTitle);               form.append("desc", classDes);               form.append("coverFile", coverFile);              //Ajax提交                           $.ajax({                                  url: "micro/save",                                 type: "POST",                                  data: form,                                  async: true,        //异步                                  processData: false,  //很重要,告诉jquery不要对form进行处理                                  contentType: false,  //很重要,指定为false才能形成正确的Content-Type                                  success: function(data){                      //在添加音视频基本信息之后,服务器端返回新增音视频的主键,以便之后上传文件时,与文件进行关联                      if(data.length>0){                          $("#microClassId").val(data);                          submitFile();//提交文件                      }                  }              });          }      }复制代码
  • 4.1.3 基本信息服务器端接收方法

    服务器端需要与前端协同作战,接收参数需要与前端传递过来的参数对应上,以免报出400的错误。

    /**   * @Description: 新增基本信息时保存form表单   * @param title   *            音视频名称   * @param coverFile   *            封面对象   * @param desc   *            音视频描述字段   */  @RequestMapping(value = "/save", method = RequestMethod.POST)  @ResponseBody  public Integer create(String title,String desc, MultipartFile coverFile) {      try {           //保存基本信息逻辑,自己实现,然后返回基本信息插入后的主键           //request,即HttpServletRequest对象,在项目启动时候就被注入,如下:           /**            * @Autowired            * private HttpServletRequest request;           **/           Integer videoId = videoService.save(title,desc, coverFile, request);           return videoId;      } catch (Exception e) {          e.printStackTrace();      }      return null;  }复制代码
  • 4.2.1 前端上传文件示例

文件上传.gif
  • 4.2.2 前端实现

    在前端实现中,采用纯JavaScript+html+css来实现按钮的删除、添加、文件的添加、上传时暂停、继续、以及文件的顺序控制;

添加视频内容
视频格式仅支持MP4及AVI,文件大小需小于500M
复制代码

以上代码,就是文件上传部分的html代码,无需关心css样式,但需要注意每个元素的id/name/事件函数;以下就是JavaScript实现文件上传逻辑的代码;

复制代码

以上代码为前端控制文件上传所需的代码,之后介绍后台部分的实现

  • 4.3.3 后台实现

后台实现,主要是接收文件的分片,利用前端传入唯一的新文件名称,使用NIO的方式,将分片进行合并。

Controller实现

/**     * @Description: 上传文件分片     *      * @param data     *            分片文件     * @param fileType     *            文件类型 video/avi audio/mp3     * @param name     *            文件名称(newName),由Client生成,即NewName 如     *            MICRO_CLASS_1502179979829.mp4     * @param total     *            分片总数     * @param index     *            当前分片数     * @param microClassId     *            文件关联的MicroClass主键,通过先保存基本信息取得并返回(DataFile中的OwerID)     * @param oldName     *            由用户自己输入的节目名称,需要与文件对应起来,如 实现两个100年奋斗目标     * @param seq     *            文件的顺序,如第一集对应1,第二集对应2....     *      * @return index 当前合并到文件的分片数     *      * @throws Exception     */    @RequestMapping("/uploadSlice")    @ResponseBody    public Integer uploadSlice(MultipartFile data, String fileType, String name, Integer total, Integer index,            Integer microClassId, String oldName, int seq) throws Exception {        int countFile = 0;// 记录一次保存中上传的文件数目        /**         * oldName:使用输入框中的字符串与fileId拼接,形如D1#D2 如:         * 实现两个100年奋斗目标#36,其中“实现两个100年奋斗目标”在数据库中存为OldName,36表示数据库中fileData主键         *          * D1: 可以为空,表示用户将Client获取到的文件名称删除,并且未输入任何字符串,可以为空; D2:         * 可以为空,表示新增的文件,不为空,则表示在修改页面,传回的fileID;         **/        String[] str=oldName.split("#");        String oldNameFiled=null;        String fileDataId=null;        System.out.println(str);        switch (str.length) {        case 0:            break;        case 1:            oldNameFiled = oldName.split("#")[0];            break;        case 2:            oldNameFiled = oldName.split("#")[0];            fileDataId = oldName.split("#")[1];            break;        default:            break;        }        if (total == 0) {            // 表示在修改页面,用户只可能修改了oldName,但是未修改文件            if (fileDataId != null && !fileDataId.equals("")) {                if (oldNameFiled != null && !oldNameFiled.equals("")) {                    dataFileService.updateOldNameById(fileDataId, oldNameFiled);//                }            }        } else {            if (index <= total) {
//说明是有分片 String dirType = fileType.split("/")[0];// 文件类型,用于创建不同的目录,如(video/audio) String fileExt = "." + fileType.split("/")[1];// 文件扩展名,如.mp3/.mp4/.avi System.out.println(data.getSize() + "----" + name + "-----" + total + "----" + index); // 追加分片到已有的分片上,返回保存文件的路径,如/fileDate/video/2017/08/09 String savePath = FileUtil.randomWrite(request, data.getBytes(), name, dirType, fileExt); if (index == 1 && savePath != null) {
// 说明是新的文件的第一个分片,在数据库中创建相应的记录,并且状态为无效,等到全部上传完毕之后在修改为有效 dataFile = new DataFile(); dataFile.setOldName(oldNameFiled); dataFile.setFileUrl(savePath); dataFile.setNewName(name + fileExt); dataFile.setOwerId(microClassId); dataFile.setSeq(seq); dataFile.setStatus(1); dataFileService.saveDataFile(dataFile); } if (index == total) {
// 说明已经成功上传一个文件 // 根据文件名称和OwerId来更新文件记录,把记录的状态修改为0(有效) dataFileService.updateByNewNameAndOwerId(name+fileExt, microClassId); countFile++; if (countFile == 1) {
// 说明已经上传成功一个文件,则吧MicroClass的状态改为0(有效); microClassService.updateMicroClass(microClassId);// 根据microClassId来修改status } LOGGER.info("已上传 " + countFile + " 个文件"); } return index++; } else { return 0; } } return 0; }复制代码

FileUtil中randomWrite方法实现

/**     * @Description: 分片文件追加     * @param request     * @param sliceFile  分片文件     * @param name   文件名称     * @param dirType  文件夹类型 如video/audio     * @param fileExt  文件扩展名 如.mp4/.avi  ./mp3     * @return     */    public static String randomWrite(HttpServletRequest request, byte[] sliceFile, String name, String dirType,String fileExt) {        try {            /** 以读写的方式建立一个RandomAccessFile对象 **/             //获取相对路径/home/gzxiaoi/apache-tomcat-8.0.45/webapps            String realPath=getRealPath(request);             //拼接文件保存路径 /fileDate/video/2017/08/09  如果没有该文件夹,则创建            String savePath=getSavePath(realPath,dirType);                        String realName = name;            String saveFile =realPath+ savePath + realName+fileExt;            RandomAccessFile raf = new RandomAccessFile(saveFile, "rw");            // 将记录指针移动到文件最后            raf.seek(raf.length());            raf.write(sliceFile);            return savePath;        } catch (Exception e) {            e.printStackTrace();        }        return null;    }    /**     * @Description: 取得tomcat中的webapps目录 如: /home/software/apache-tomcat-8.0.45/webapps     * @param request     * @return     */    public static String getRealPath(HttpServletRequest request) {        String realPath = request.getSession().getServletContext().getRealPath(File.separator);        realPath = realPath.substring(0, realPath.length() - 1);        int aString = realPath.lastIndexOf(File.separator);        realPath = realPath.substring(0, aString);        return realPath;    }/**     * @Description: 获取文件保存的路径,如果没有该目录,则创建     * @param realPath 相对路径 ,如   /home/software/apache-tomcat-8.0.45/webapps     * @param fileType  文件类型 如: images/video/audio用于拼接文件保存路径,区分音视频     * @return     */    public static String getSavePath(String realPath, String fileType) {        SimpleDateFormat year = new SimpleDateFormat("yyyy");        SimpleDateFormat m = new SimpleDateFormat("MM");        SimpleDateFormat d = new SimpleDateFormat("dd");        Date date = new Date();        String sp=File.separator + "fileDate" + File.separator +fileType + File.separator + year.format(date) + File.separator                + m.format(date) + File.separator + d.format(date) + File.separator;        String savePath = realPath+ sp;        File folder = new File(savePath);        if (!folder.exists()) {            folder.mkdirs();        }        return sp;    }复制代码
5. 总结

本篇文章主要从实际项目出发,介绍了文件上传中所常见的一些情况,以及具体的实现。在断点续传中,需要注意的关键点:

  • 浏览器端(前端)需要获取文件的大小,去计算总分片数,并且需要校验文件的合法性,在上传过程中,需要及时的获取到当前传输文件的当前分片数,以更新下一次需要传输文件的范围大小;
  • 服务器端,需要根据前端传入的参数,去确定分片所属的源文件,并追加;(或者根据一定规则,创建临时目录,将属于同一文件的分片,放在同一目录,在将所有分片全部上传完毕之后<即当前分片数=总分片数>,在合并所有的分片);

转载地址:http://ndfll.baihongyu.com/

你可能感兴趣的文章
[XML] CoolFormat
查看>>
我是如何做列表页的
查看>>
transmission简单使用
查看>>
6-8-并查集(等价类)-树和二叉树-第6章-《数据结构》课本源码-严蔚敏吴伟民版...
查看>>
Log4j 输出的日志中时间比系统时间少了8小时的解决方法,log4j日志文件重复输出...
查看>>
UML用例图总结
查看>>
[改善Java代码]优先使用整型池
查看>>
iOS中设置导航栏标题的字体颜色和大小
查看>>
h.264并行解码算法分析
查看>>
ALSA声音编程介绍
查看>>
bootstrap fileinput 文件上传工具
查看>>
C# String 前面不足位数补零的方法
查看>>
route命令
查看>>
KETTLE、spoon使用
查看>>
Python学习--03变量类型
查看>>
parquet文件格式——本质上是将多个rows作为一个chunk,同一个chunk里每一个单独的column使用列存储格式,这样获取某一row数据时候不需要跨机器获取...
查看>>
NFS安装及优化过程--centos6.6
查看>>
使用tmpfs的好处
查看>>
angularjs中的验证input输入框只能输入数字和小数点
查看>>
ThinkPHP整合cropper剪裁图片上传功能
查看>>