本文用于整理记录大文件分片上传、断点续传、极速秒传的Java版简单实现。
关于上传的文章
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从而选择文件并上传。
<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 的点击事件,选择文件。
/** * 点击【上传】 */ selectFile () { let _this = this; $("#" + _this.inputId + "-input").trigger("click"); },1.2.3.3 uploadFile
检测到选择好文件,input 执行该方法,完成文件上传。
/** * 上传文件 */ 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
递归上传分片的过程
/** * 递归上传分片 */ 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进行文件分片的函数。
/** * 文件分片函数 */ 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等方式优化。
@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
文件所有分片上传完成后到合并操作,合并完成后删除文件的所有分片。
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 调用。
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。
@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
3.2 小结
本文主要参考课程 《Spring Cloud + Vue 前后端分离 开发企业级在线视频课程系统》 中相关章节整理实现,示例本身挺基础,可供优化点很多,这里暂且不做扩展,原理了解之后,大家可自行扩展到并行上传分片、消息队列合并文件/删除分片等,应该不会太难,另外分片上传和分片下载比较类似,也可自行考虑实现。
4 相关代码
时不时有朋友留言要源码,之前在评论区做了回复,但今天对相关git库做了归档整理,也涉及到该项目的,故评论区的链接已失效。这里重新指引一下。
# 后端起始Controller:https://gitee.com/windcoder/qydemo-java/blob/master/qycms-2022/imoocDemo/cloudCourseDemo/file/src/main/java/com/windcoder/cloudCourseDemo/file/controller/admin/UploadController.java - 里面的 `@PostMapping("/big-upload")` 注解的即为大文件上传部分。 - `merge` 方法为最终合并逻辑。# 前端上传组件:https://gitee.com/windcoder/qydemo-java/blob/master/qycms-2022/imoocDemo/cloudCourseDemo/admin/src/components/big-file.vue本身cloudCourseDemo 为一个完整的前后端分离项目源码,其中:

评论