前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >大文件分片上传Java版简单实现

大文件分片上传Java版简单实现

作者头像
WindCoder
发布2020-06-28 11:57:01
8.3K3
发布2020-06-28 11:57:01
举报
文章被收录于专栏:WindCoderWindCoder

本文用于整理记录大文件分片上传、断点续传、极速秒传的Java版简单实现。

关于上传的文章 FTP文件上传下载

1. 分片上传

分片上传的核心思路:

  • 1.将文件按一定的分割规则(静态或动态设定,如手动设置20M为一个分片),用slice分割成多个数据块。
  • 2.为每个文件生成一个唯一标识Key,用于多数据块上传时区分所属文件。
  • 3.所有分片上传完成,服务端校验合并标识为Key的所有分片为一个最终文件。

分片上传到意义:

  • 将文件分片上传,在网络环境不佳时,可以对文件上传失败的部分重新上传,避免了每次上传都需要从文件起始位置上传到问题。
  • 分片的附带好处还能很方便的实现进度条。 1.2 实例 本代码基于Vue + SpringBoot 简单演示,篇幅有限仅放出关键代码,完整代码可在文章最后获取。

该实例是一个串行上传分片数据的实例,一个文件仅在数据库中保存了一条记录,每次上传一个分片时更新一次该记录,直到该文件到所有分片上传完成。

1.2.3 Vue

1.2.3.1 template

模板部分包含一个“上传”Button ,和一个隐藏的 <input type="file">

点击 Button 触发 input从而选择文件并上传。

代码语言:javascript
复制
<template>
    <div>
      <button type="button" v-on:click="selectFile()" class="btn btn-white btn-default btn-round">
          <i class="ace-icon fa fa-upload"></i>{{text}}
      </button>
      <input class="hidden" type="file" ref="file" v-on:change="uploadFile()" v-bind:id="inputId+'-input'">

    </div>
</template>

1.2.3.2 selectFile

点击 Button 【上传】,触发 隐藏 input 的点击事件,选择文件。

代码语言:javascript
复制
    /**
     * 点击【上传】
     */
    selectFile () {
        let _this = this;
        $("#" + _this.inputId + "-input").trigger("click");
    },

1.2.3.3 uploadFile

检测到选择好文件,input 执行该方法,完成文件上传。

代码语言:javascript
复制
     /**
       * 上传文件
       */
      uploadFile() {
        let _this = this; 
        // 1. 获取 input 中被选中的文件
        let file = _this.$refs.file.files[0];


        // 2. 生成文件标识,标识多次上传的是不是同一个文件
         let key = hex_md5(file.name + file.size + file.type);
        let key10 = parseInt(key, 16);
        let key62 = Tool._10to62(key10);

        // 判断文件格式 (非必选,根据实际情况选择是否需要限制文件上传类型)
        let suffixs = _this.suffixs;
        let fileName = file.name;
        let suffix = fileName.substring(fileName.lastIndexOf(".")+1, fileName.length).toLowerCase();
        if(!(!suffixs || JSON.stringify(suffixs) === "{}" || suffixs.length === 0)) {
            let validateSuffix = false;
            for(let s of suffixs) {
                if(s.toLocaleLowerCase() === suffix) {
                    validateSuffix = true;
                    break;
                }
            }
            if(!validateSuffix) {
                Toast.warning("文件格式不正确!只支持上传:" + suffixs.join(","));
                $("#" + _this0.inputId + "-input").val("");
                return;
            }
        }

        // 3. 文件分片开始
            // 3.1 设置与计算分片必选参数
        let shardSize = 20 * 1024 *1024; // 20M为一个分片
        let shardIndex = 1;   // 分片索引,1表示第1个分片
        let size = file.size; // 文件的总大小
        let shardTotal = Math.ceil(size / shardSize); // 总分片数

            //  3.2 拼接将要传递到参数, use 非必选,这里用来标识文件用途。
        let param = {
          'shardIndex': shardIndex,
          'shardSize': shardSize,
          'shardTotal': shardTotal,
          'use': _this.use,
          'name': file.name,
          'suffix': suffix,
          'size': file.size,
          'key': key62
        };

        //  3.3  传递分片参数,通过递归完成分片上传。
        _this.upload(param);

      },

1.2.3.4 upload

递归上传分片的过程

代码语言:javascript
复制
      /**
       * 递归上传分片
       */
      upload(param) {
        let _this = this;
        let shardIndex = param.shardIndex;
        let shardTotal = param.shardTotal;
        let shardSize = param.shardSize;
        // 3.3.1 根据参数,获取文件分片
        let fileShard = _this.getFileShard(shardIndex,shardSize);


        // 3.3.2 将文件分片转为base64进行传输
        let fileReader = new FileReader();
       // 读取并转化 fileShard 为 base64
        fileReader.readAsDataURL(fileShard);
        //  readAsDataURL 读取后的回调,
            // 将 经过  base64 编码的  分片 整合到 param ,发送给后端,从而上传分片。
        fileReader.onload = function (e) {
          let base64 = e.target.result;
          param.shard = base64;
          Loading.show();
          _this.$ajax.post(process.env.VUE_APP_SERVER + "/file/admin/big-upload", param).then((res)=> {
              Loading.hide();
              let resp = res.data; 
              // 上传结果
                // 当前分片索引小于 分片总数,继续执行分派,反之 则表示全部上传成功。
              if(shardIndex < shardTotal) {
                // 上传下一个分片
                param.shardIndex = param.shardIndex + 1;
                _this.upload(param);
              } else {
                  // 文件上传成功后的回调
                 _this.afterUpload(resp);
              }
              $("#" + _this.inputId + "-input").val("");
          });
        };
      },

1.2.3.5 getFileShard

1.2.3.4 upload 中根据传参,使用slice进行文件分片的函数。

代码语言:javascript
复制
       /**
        * 文件分片函数
        */
      getFileShard(shardIndex, shardSize) {
        let _this = this;
        let file = _this.$refs.file.files[0];
        let start = (shardIndex - 1) * shardSize; // 当前分片起始位置
        let end = Math.min(file.size, start + shardSize); // 当前分片结束位置
        let fileShard = file.slice(start, end); // 从文件中截取当前的分片数据
        return fileShard;
      },
1.2.4 Java

该部分由上传api函数和合并函数组成。

1.2.4.1 uploadOfMerge

文件上传的api函数,前端将分片数据通过该api上传并保存到对应目录下,当全部分片上传成功,将所有分片合并成文件,同时将相关信息保存到数据库。

合并部分可以考虑通过定时任务、MQ等方式优化。

代码语言:javascript
复制
    @PostMapping("/big-upload")
    public ResponseDto uploadOfMerge(@RequestBody FileDto fileDto) throws IOException {
        log.info("上传文件开始");


        String use = fileDto.getUse();
        String key = fileDto.getKey();
        String suffix = fileDto.getSuffix();
        String shardBase64 = fileDto.getShard();
        // 1. 将分片转为 MultipartFile
        MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(fileDto.getShard());
        //  获取分片要保存到的路径
        //  根据use字段获取文件用途,从而上传到不同文件夹下(非必选)
        FileUseEnum useEnum = FileUseEnum.getByCode(use);
            // 若文件夹不存在则创建
        String dir = useEnum.name().toLowerCase();
        File fullDir = new File(FILE_PATH + dir);
        if (!fullDir.exists()) {
            fullDir.mkdir();
        }

        String path = new StringBuffer(dir)
                .append(File.separator)
                .append(key)
                .append(".")
                .append(suffix).toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4
        String localPath = new StringBuffer(path)
                .append(".")
                .append(fileDto.getShardIndex()).toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
        String fullPath = FILE_PATH + localPath;
        // 2. 通过 transferTo 保存文件到服务器磁盘
        File dest = new File(fullPath);
        shard.transferTo(dest);
        log.info(dest.getAbsolutePath());
        // 3. 将文件分片信息保存/更新到数据库
        log.info("保存文件记录开始");
        fileDto.setPath(path);
        fileService.saveBigFile(fileDto);

        ResponseDto responseDto = new ResponseDto();
        responseDto.setContent(fileDto);

        // 4. 合并
            // 若分片均已上传,将所有分片合并成一个文件。
        if (fileDto.getShardIndex().equals(fileDto.getShardTotal())) {
            this.merge(fileDto);
        }
        // 5. 返回分片上传结果
        return responseDto;
    }

1.2.4.2 merge

文件所有分片上传完成后到合并操作,合并完成后删除文件的所有分片。

代码语言:javascript
复制
 private void merge(FileDto fileDto) {
        log.info("合并分片开始");
        String path = fileDto.getPath();
        Integer shardTotal = fileDto.getShardTotal();
        File newFile = new File(FILE_PATH + path);
        byte[] byt = new byte[10 * 1024 * 1024];
        FileInputStream inputStream = null;   // 分片文件
        int len;

        // 文件追加写入
        try (FileOutputStream outputStream = new FileOutputStream(newFile, true);
              ) {
            for (int i = 0; i < shardTotal; i++) {
                // 读取第一个分片
                inputStream = new FileInputStream(new File(FILE_PATH + path + "." + (i+1))); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
                while ((len = inputStream.read(byt))!=-1) {
                    outputStream.write(byt, 0, len);
                }
            }
        } catch (FileNotFoundException e) {
            log.info("文件寻找异常", e);
        } catch (IOException e) {
            log.info("分片合并异常", e);
        } finally {
            try {
                if(inputStream !=null) {
                    inputStream.close();
                }
                log.info("IO流关闭");
            } catch (IOException e) {
                log.error("IO流关闭", e);
            }

        }
        log.error("合并分片结束");

        System.gc();
        // 删除分片
        log.info("删除分片开始");
        for (int i = 0; i < shardTotal; i++) {
            String filePath = FILE_PATH + path + "." + (i + 1);
            File file = new File(filePath);
            boolean result = file.delete();
            log.info("删除{},{}", filePath, result ? "成功" : "失败");
        }
        log.info("删除分片结束");
    }

2. 断点续传/极速秒传

断点续传基于分片上传实现,使之前未上传完成到文件可以从上次上传完成的Part的位置继续上传。

断点续传实现了,也就间接实现了 极速秒传功能,通过 唯一key 检测文件上传进度,发现之前已经上传完成,便可返回给用户 “极速秒传” 成功的消息,而不需要将该文件再次上传一次。至于文件及其数据库信息是否需要内部拷贝,则看项目需求即可。

2.1 Vue

1.2.3.3 uploadFile 中的 _this.upload(param); 被 检测已上传分片的函数 _this.check(param); 取代。

upload 上传函数由 check 调用。

代码语言:javascript
复制
check(param) {
        let _this = this;
        _this.$ajax.get(process.env.VUE_APP_SERVER + "/file/admin/check/" + param.key).then((res)=> {
            let resp = res.data;
            if(resp.success) {
              let obj = resp.content;
              if(!obj) {
                param.shardIndex = 1;
                console.log("没有找到文件记录,从分片1开始上传");
                _this.upload(param);
              } else if (obj.shardIndex === obj.shardTotal) {
                // 已上传分片 = 分片总数,说明已全部上传完,不需要再上传
                Toast.success("文件极速秒传成功!");
                _this.afterUpload(resp);  
                $("#" + _this.inputId + "-input").val("");  
              }else {
                param.shardIndex = obj.shardIndex + 1;
                console.log("没有找到文件记录,从分片1开始上传");
                _this.upload(param);
              }
            } else {
              console.log("文件上传失败");
              $("#" + _this.inputId + "-input").val("");
            }
        });
      },
2.1 Java

Java 增加了一个检测文件分片上传情况到api。

代码语言:javascript
复制
    @GetMapping("/check/{key}")
    public ResponseDto check(@PathVariable String key) {
        log.info("检测上传分片开始:{}}", key);
        ResponseDto responseDto = new ResponseDto();
        FileDto fileDto = fileService.findByKey(key);
        responseDto.setContent(fileDto);
        return responseDto;
    }

3. 扩展

3.1 readAsDataURL

readAsDataURL 方法会读取指定的 Blob 或 File 对象。读取操作完成的时候,readyState 会变成已完成DONE,并触发 loadend 事件,同时 result 属性将包含一个data:URL格式的字符串(base64编码)以表示所读取文件的内容。

FileReader.readAsDataURL

3.2 小结

本文主要参考课程 《Spring Cloud + Vue 前后端分离 开发企业级在线视频课程系统》 中相关章节整理实现,示例本身挺基础,可供优化点很多,这里暂且不做扩展,原理了解之后,大家可自行扩展到并行上传分片、消息队列合并文件/删除分片等,应该不会太难,另外分片上传和分片下载比较类似,也可自行考虑实现。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 分片上传
    • 1.2.3 Vue
      • 1.2.4 Java
      • 2. 断点续传/极速秒传
        • 2.1 Vue
          • 2.1 Java
          • 3. 扩展
            • 3.1 readAsDataURL
              • 3.2 小结
              相关产品与服务
              对象存储
              对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档