依星源码资源网,依星资源网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

限时开通VIP永久会员,可免费下载所有附件此广告位出租
查看: 10|回复: 0

Spring Boot:基于WebUploader实现超大文件上传和断点续传(二)

[复制链接] 主动推送

9442

主题

9494

帖子

1万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
10275
发表于 3 天前 | 显示全部楼层 |阅读模式
Spring Boot:基于WebUploader实现超大文件上传和断点续传(二)
前言

书接前文,欢迎回来!Spring Boot:基于WebUploader实现超大文件上传和断点续传(一),主要讲述了大文件上传的实现思路、实现原理、以及主要的前端代码实现的。这篇文章将重点分析后面几个问题:

5、前端、后端如何校验分片是否已经上传?

6、后端如何处理分片上传请求?

7、webuploader组件中,合并文件分片的请求在哪里触发?

8、后端如何合并分片请求?

9、分片上传失败后,如何在断点处继续上传?

10、上传的进度条是怎么实现的?


代码实现

5、前端、后端如何校验分片是否已经上传?

在webuploader内部的一个command(before-send)已经完成了分片文件的md5计算以及请求后台接口来校验当前分片文件是否已经上传(参见第3个问题),如果已上传,那么会直接跳过当前分片上传接口的调用,uploadBeforeSend事件也不会再触发(当某个分片文件在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次);

Spring Boot:基于WebUploader实现超大文件上传和断点续传(二)

Spring Boot:基于WebUploader实现超大文件上传和断点续传(二)

如果未上传,则uploadBeforeSend事件会触发,携带一些分片的参数信息发起分片上传请求;

Spring Boot:基于WebUploader实现超大文件上传和断点续传(二)

Spring Boot:基于WebUploader实现超大文件上传和断点续传(二)

那么后端是如何检验分片是否上传呢?如下:

1、在分片文件上传接口中,分片上传成功后,会保存分片的相关信息,如:分片文件md5、文件md5、文件大小、分片存储位置、分片数据块的起始结束位置、总共分片数量等,这里使用redis缓存了这些分片信息,redis用到了hash数据结构,其中key为文件的md5,hashkey是“chunk_md5_”+分片索引,value就是分片文件的md5值;(当然也可以使用数据库或其他存储介质,)

2、接口被调用的时候,根据前端传过来的当前分片的索引位置取出分片的md5与前端传过来的分片文件md5进行比较,如果相同,则说明当前分片已经上传成功;如果不相同,则说明未上传过;


  1. @PostMapping("/check")
  2. public boolean check(String fileMd5,String chunk,String chunkMd5) {
  3.     Object o = redisTemplate.opsForHash().get(fileMd5, "chunk_md5_"+chunk);
  4.     if (chunkMd5.equals(o)) {
  5.         return true;
  6.     }
  7.     return false;
  8. }
复制代码
6、后端如何处理分片上传请求?

后端在处理分片上传时主要做了两件事:

第一,把分片文件保存在磁盘上或其他的网络存储介质上,这里需要注意一下分片文件的命名规则,尽量有规律一些,方便后面合并分片;这里分片文件的命名规则是:分片md5值+分片索引位置;

第二、保存分片相关的信息,在实际业务开发中可以考虑保存在缓存或数据库里,这里只是作了缓存;缓存的数据结构是hash,key是文件整体的md5值,hashKey与hashValue对应关系如下:

hashKey
hashValue
描述
“chunk_location_”+分片索引位置
分片文件的存储绝对路径
分片文件的存储位置
“chunk_start_end_”+分片索引位置
“起始位置”+“-”+“结束位置”
分片文件的在整体文件中字节的起始结束位置
"chunk_md5_"+分片索引位置
分片文件的md5值
分片文件的md5值
file_size
文件整体的字节数大小
文件整体的字节数大小
file_chunks
文件整体被分了多少片
文件整体被分了多少片
  1. /**
  2.      * 分片上传接口
  3.      *
  4.      * @param request
  5.      * @param multipartFile
  6.      * @return
  7.      * @throws IOException
  8.      */
  9.     @PostMapping("/upload")
  10.     public String upload(HttpServletRequest request, MultipartFile multipartFile) {
  11.         log.info("分片上传....");
  12.         Map<String, String> requestParam = this.doRequestParam(request);
  13.         String md5Value = requestParam.get("md5Value");//整体文件的md5值
  14.         String chunkIndex = requestParam.get("chunk");//分片在所有分片文件中索引位置
  15.         String start = requestParam.get("start");//当前分片在整个数据文件中的开始位置
  16.         String end = requestParam.get("end");//当前分片在整个数据文件中的结束位置
  17.         String chunks = requestParam.get("chunks");//整体文件总共被分了多少片
  18.         String fileSize = requestParam.get("size");//整体文件大小
  19.         String chunkMd5 = requestParam.get("chunkMd5");//分片文件的md5值
  20.         String userDir = System.getProperty("user.dir");
  21.         String chunkFilePath = userDir + File.separator + chunkMd5 + "_" + chunkIndex;
  22.         File file = new File(chunkFilePath);
  23.         try {
  24.             multipartFile.transferTo(file);
  25.             Map<String, String> map = new HashMap<>();
  26.             map.put("chunk_location_" + chunkIndex, chunkFilePath);//分片存储路径
  27.             map.put("chunk_start_end_" + chunkIndex, start + "_" + end);
  28.             map.put("file_size", fileSize);
  29.             map.put("file_chunks", chunks);
  30.             map.put("chunk_md5_" + chunkIndex, chunkMd5);
  31.             redisTemplate.opsForHash().putAll(md5Value, map);
  32.         } catch (IOException e) {
  33.             e.printStackTrace();
  34.         } catch (IllegalStateException e) {
  35.             e.printStackTrace();
  36.         }
  37.         return "success";
  38.     }
复制代码
7、webuploader组件中,合并文件分片的请求在哪里触发?

webuploader组件中,有一对事件分别是uploadSuccess和uploadError,当文件上传成功时,uploadSuccess触发;当文件上传失败时,uploadError触发;因此uploadSuccess事件刚好可以用来,向后台发起合并分片文件的请求;
  1. //当文件上传成功时触发
  2. uploader.on('uploadSuccess', function (file) {
  3.     //大文件的所有分片上传成功后,请求后端对分片进行合并
  4.     $.ajax({
  5.         url: 'http://localhost:8080/file/merge',
  6.         method: 'post',
  7.         data: {'md5Value': file.wholeMd5, 'originalFilename': file.name},
  8.         success: function (res) {
  9.             alert('大文件上传成功!')
  10.         }
  11.     })
  12.     $('#' + file.id).find('p.state').append('文件上传成功<br/>');
  13. });
复制代码
8、后端如何合并分片请求?

当所有的分片文件上传成功时会触发webuploader的uploadSuccess事件触发时机,然后调用后台的合并分片文件接口,合并分片文件接口的主要业务逻辑:

1、检验一下所有的分片是否全部上传完成(当分片上传成功时,会把分片md5值和文件整体总共分了多少片存储在redis里,存储时的hashKey是"chunk_md5_"+分片索引位置和file_chunks,如果存储的分片md5的数量与文件整体分片的数量一致,则表示所有的分片均已上传);


  1. /**
  2. * 合并分片前检验文件整体的所有分片是否全部上传
  3. *
  4. * @param key
  5. * @return
  6. */
  7. private boolean checkBeforeMerge(String key) {
  8.     Map map = redisTemplate.opsForHash().entries(key);
  9.     Object file_chunks = map.get("file_chunks");
  10.     int i = 0;
  11.     for (Object hashKey : map.keySet()) {
  12.         if (hashKey.toString().startsWith("chunk_md5_")) {
  13.             ++i;
  14.         }
  15.     }
  16.     if (Integer.valueOf(file_chunks.toString())==(i)) {
  17.         return true;
  18.     }
  19.     return false;
  20. }
复制代码
2、如果当前文件文件已经上传过,只是名字不同,那么md5值是相同的,直接拿出已经上传的文件按现在名字再复制一份;

3、在开始合并分片文件前,要先从redis中取出分片文件的存储位置,这里要特别注意一下,分片合并的顺序一定与索引位置的升序一致,否则合并的文件是无法打开或运行的;因为分片上传的过程是并发执行的,到达后端的顺序可能每次都不一样,但是各分片的索引位置不会变;因此可以在[0,文件分片总数量-1]之间遍历,从redis中依次取出分片文件的存储路径,并依次写入到一个新的文件里;

4、各个分片文件依次写入完成后,关闭输入流、输出流,并删除分片文件(分片合并成完整文件的时候,分片文件就没有用了,另外缓存的分片其他相关信息也没有用了,也可以删除了,当然在实际业务开发中,可根据具体的需求酌情保留);


  1. /**
  2. * 合并分片文件接口
  3. *
  4. * @param request
  5. * @return
  6. * @throws IOException
  7. */
  8. @PostMapping("/merge")
  9. public String merge(HttpServletRequest request) throws IOException {
  10.     log.info("合并分片...");
  11.     Map<String, String> requestParam = this.doRequestParam(request);
  12.     String md5Value = requestParam.get("md5Value");
  13.     String originalFilename = requestParam.get("originalFilename");
  14.     //校验切片是否己经上传完毕
  15.     boolean flag = this.checkBeforeMerge(md5Value);
  16.     if (!flag) {
  17.         return "切片未完全上传";
  18.     }
  19.     //检查是否已经有相同md5值的文件上传;主要是对名字不同,而实际文件相同的文件,直接对原文件进行复制;
  20.     Object file_location = redisTemplate.opsForHash().get(md5Value, "file_location");
  21.     if (file_location != null) {
  22.         String source = file_location.toString();
  23.         File file = new File(source);
  24.         if (!file.getName().equals(originalFilename)) {
  25.             File target = new File(System.getProperty("user.dir") + File.separator + originalFilename);
  26.             Files.copy(file.toPath(), target.toPath());
  27.             return "success";
  28.         }

  29.     }
  30.     //这里要特别注意,合并分片的时候一定要按照分片的索引顺序进行合并,否则文件无法使用;
  31.     Integer file_chunks = Integer.valueOf(redisTemplate.opsForHash().get(md5Value, "file_chunks").toString());
  32.     String userDir = System.getProperty("user.dir");
  33.     File writeFile = new File(userDir + File.separator + originalFilename);
  34.     OutputStream outputStream = new FileOutputStream(writeFile);
  35.     InputStream inputStream = null;
  36.     for (int i = 0; i < file_chunks; i++) {
  37.         String tmpPath = redisTemplate.opsForHash().get(md5Value,"chunk_location_" + i).toString();
  38.         File readFile = new File(tmpPath);
  39.         inputStream = new FileInputStream(readFile);
  40.         byte[] bytes = new byte[1024 * 1024];
  41.         while ((inputStream.read(bytes) != -1)) {
  42.             outputStream.write(bytes);
  43.         }
  44.         if (inputStream != null) {
  45.             inputStream.close();
  46.         }
  47.     }
  48.     if (outputStream != null) {
  49.         outputStream.close();
  50.     }
  51.     redisTemplate.opsForHash().put(md5Value, "file_location", userDir + File.separator + originalFilename);
  52.     this.delTmpFile(md5Value);
  53.     return "success";
  54. }
  55. private void delTmpFile(String md5Value) throws JsonProcessingException {
  56.     Map map = redisTemplate.opsForHash().entries(md5Value);
  57.     List<String> list = new ArrayList<>();
  58.     for (Object hashKey : map.keySet()) {
  59.         if (hashKey.toString().startsWith("chunk_location")) {
  60.             String filePath = map.get(hashKey).toString();
  61.             File file = new File(filePath);
  62.             boolean flag = file.delete();
  63.             list.add(hashKey.toString());
  64.             log.info("delete:" + filePath + ",:" + flag);
  65.         }
  66.         if (hashKey.toString().startsWith("chunk_start_end_")) {
  67.             list.add(hashKey.toString());
  68.         }
  69.         if (hashKey.toString().startsWith("chunk_md5_")) {
  70.             list.add(hashKey.toString());
  71.         }
  72.     }
  73.     list.add("file_chunks");
  74.     list.add("file_size");
  75.     redisTemplate.opsForHash().delete(md5Value, list.toArray());
  76. }
复制代码
9、分片上传失败后,如何在断点处继续上传?

在第3个问题、第5个问题中,已经解决了这个问题,webuploader内部一个command(before-send)会触发,这时计算分片文件的md5值,并携带分片文件的md5值调用后台的校验接口;如果已上传,那么会直接跳过当前分片上传接口的调用;如果未上传,则会只上传未上传的的那个分片文件;

10、上传的进度条是怎么实现的?

webuploader的uploadProgress事件在上传过程中触发,会携带上传进度参数;

  1. // 文件上传过程中创建进度条实时显示
  2. uploader.on('uploadProgress', function (file, percentage) {
  3.     var $li = $('#' + file.id),
  4.         $percent = $li.find('.progress .progress-bar');
  5.     if (!$percent.length) {
  6.         $percent = $('<div class="progress progress-striped active">' +
  7.             '<div class="progress-bar" role="progressbar" style="width: 0%">' +
  8.             '</div>' +
  9.             '</div>').appendTo($li).find('.progress-bar');
  10.     }
  11.     $percent.css('width', percentage * 100 + '%');
  12. });
复制代码
总结

想要完整说清楚一件事,确实不容易,不知道我究间说明白了没,我感觉我是说明白了,评论区告诉我吧。

1、对于后端来说,大部分时候写的程序都是同步顺序执行的,但前端的异步执行很常见,通过这篇文章又重新学习了promise、deferred的使用;

2、不要以为看懂了一篇文章,就真的懂了,纸上得来终觉浅,绝知此事须躬行,还是得上手自己验证一翻,别人说的未必是对的,或者说在作者当时的场景下是对的,如何确定你的场景和他的是否相同?所以小编这里希望,大家多提问题,共同讨论,共同进步。


附件中为所有完整的的示例文件以及WebUploader组件源码


游客,本帖隐藏的内容需要积分高于 2 才可浏览,您当前积分为 0

提取码下载:
文件名称:提取码下载.txt 
下载次数:0  文件大小:13 Bytes  售价:6金钱 [记录]
下载权限: 不限 [购买VIP]   [充值]   [在线充值]   【VIP会员5折;永久VIP免费】
安全检测,请放心下载





相关帖子

扫码关注微信公众号,及时获取最新资源信息!下载附件优惠VIP会员5折;永久VIP免费
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

免责声明:
1、本站提供的所有资源仅供参考学习使用,版权归原著所有,禁止下载本站资源参与商业和非法行为,请在24小时之内自行删除!
2、本站所有内容均由互联网收集整理、网友上传,并且以计算机技术研究交流为目的,仅供大家参考、学习,请勿任何商业目的与商业用途。
3、若您需要商业运营或用于其他商业活动,请您购买正版授权并合法使用。
4、论坛的所有内容都不保证其准确性,完整性,有效性,由于源码具有复制性,一经售出,概不退换。阅读本站内容因误导等因素而造成的损失本站不承担连带责任。
5、用户使用本网站必须遵守适用的法律法规,对于用户违法使用本站非法运营而引起的一切责任,由用户自行承担
6、本站所有资源来自互联网转载,版权归原著所有,用户访问和使用本站的条件是必须接受本站“免责声明”,如果不遵守,请勿访问或使用本网站
7、本站使用者因为违反本声明的规定而触犯中华人民共和国法律的,一切后果自己负责,本站不承担任何责任。
8、凡以任何方式登陆本网站或直接、间接使用本网站资料者,视为自愿接受本网站声明的约束。
9、本站以《2013 中华人民共和国计算机软件保护条例》第二章 “软件著作权” 第十七条为原则:为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。若有学员需要商用本站资源,请务必联系版权方购买正版授权!
10、本网站如无意中侵犯了某个企业或个人的知识产权,请来信【站长信箱312337667@qq.com】告之,本站将立即删除。
郑重声明:
本站所有资源仅供用户本地电脑学习源代码的内含设计思想和原理,禁止任何其他用途!
本站所有资源、教程来自互联网转载,仅供学习交流,不得商业运营资源,不确保资源完整性,图片和资源仅供参考,不提供任何技术服务。
本站资源仅供本地编辑研究学习参考,禁止未经资源商正版授权参与任何商业行为,违法行为!如需商业请购买各资源商正版授权
本站仅收集资源,提供用户自学研究使用,本站不存在私自接受协助用户架设游戏或资源,非法运营资源行为。
 
在线客服
点击这里给我发消息 点击这里给我发消息 点击这里给我发消息
售前咨询热线
312337667

微信扫一扫,私享最新原创实用干货

QQ|免责声明|小黑屋|依星资源网 ( 鲁ICP备2021043233号-3 )|网站地图

GMT+8, 2024-9-1 08:19

Powered by Net188.com X3.4

邮箱:312337667@qq.com 客服QQ:312337667(工作时间:9:00~21:00)

快速回复 返回顶部 返回列表