WebUploader实现分块上传(断点续传)着重后端Java代码的实现

2023-12-31 07:10

本文主要是介绍WebUploader实现分块上传(断点续传)着重后端Java代码的实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

断点续传:
在下载或上传时,将文件分段,每段采用一个线程进行上传或下载。
每次上传分块前校验分块,如果已存在分块则不再上传。
因此若碰到网络故障中断,可继续上传下载(检测是否在服务器存在,或是否下载)。
最后将每段合并。


流程图

在这里插入图片描述


上传前准备工作

WebUploader.js中有给文件生成MD5的方法,即在文件上传前,把内容读取出来,算出MD5值,通过Ajax与服务端合并完成后的文件MD5值进行比对验证。

上传前还需要检查该上传的文件是否存在。

前端

         beforeSendFile:function(file) {// 创建一个deffered,用于通知是否完成操作var deferred = WebUploader.Deferred();// 计算文件的唯一标识,用于断点续传(new WebUploader.Uploader()).md5File(file, 0, 100*1024*1024).then(function(val) {this.fileMd5 = val;this.uploadFile = file;//向服务端请求注册上传文件$.ajax({type:"POST",url:"/api/media/upload/register",data:{// 文件唯一表示fileMd5:this.fileMd5,fileName: file.name,fileSize:file.size,mimetype:file.type,fileExt:file.ext},dataType:"json",success:function(response) {if(response.success) {//alert('上传文件注册成功开始上传');deferred.resolve();} else {alert(response.message);deferred.reject();}}});}.bind(this));return deferred.promise();}.bind(this),

后端

    /*** 得到文件所属目录路径* 一级目录:MD5第一个字符* 二级目录:MD5第二个字符* 三级目录:MD5* 文件名:MD5+文件扩展名* @param fileMd5* @return*/private String getFileFolderPath(String fileMd5) {return uploadLocation + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";}private String getFilePath(String fileMd5, String fileExt) {return this.getFileFolderPath(fileMd5) + fileMd5 + "." + fileExt;}private String getChunkFileFolderPath(String fileMd5) {return this.getFileFolderPath(fileMd5) + "chunk/";}// 文件上传前的准备工作public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {// 检查文件是否存在于磁盘String fileFolderPath = this.getFileFolderPath(fileMd5);String filePath = this.getFilePath(fileMd5, fileExt);File file = new File(filePath);boolean exists = file.exists();// 检查文件是否存在于MongoDB (主键为 fileMd5)boolean present = mediaFileRepository.findById(fileMd5).isPresent();if (exists && present) {// 既存在于磁盘又存在于数据库说明该文件存在ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);}// 文件不存在// 检查文件所在目录是否存在File fileFolder = new File(fileFolderPath);if (!fileFolder.exists()) {// 不存在创建该目录 (目录就是根据前端传来的MD5值创建的)fileFolder.mkdirs();}return ResponseResult.SUCCESS();}

检查分块是否存在

分块名是从0开始递增的(相当于索引),通过分块所在路径+分块的索引定位到具体分块来判断是否存在。
在这里插入图片描述
前端

        beforeSend:function(block) {var deferred = WebUploader.Deferred();// 每次上传分块前校验分块,如果已存在分块则不再上传,达到断点续传的目的$.ajax({type:"POST",url:"/api/media/upload/checkchunk",data:{// 文件唯一表示fileMd5:this.fileMd5,// 当前分块下标chunk:block.chunk,// 当前分块大小chunkSize:block.end-block.start},dataType:"json",success:function(response) {if(response.fileExist) {// 分块存在,跳过该分块deferred.reject();} else {// 分块不存在或不完整,重新发送deferred.resolve();}}});//构建fileMd5参数,上传分块时带上fileMd5this.uploader.options.formData.fileMd5 = this.fileMd5;this.uploader.options.formData.chunk = block.chunk;return deferred.promise();}.bind(this),

后端

    // 检查分块是否存在public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {// 检查分块文件是否存在// 得到分块所在路径String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);// 分块所在路径+分块的索引可定位具体分块if (new File(chunkFileFolderPath + chunk).exists()) {return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK, true);}return new CheckChunkResult(CommonCode.SUCCESS, false);}

上传文件

我们选择分块上传文件,chunked设置为true
在这里插入图片描述

上传文件使用的是IOUtils.copy(in, out);方法

    // 上传分块public ResponseResult uploadChunk(MultipartFile file, String fileMd5, Integer chunk) {// 检查分块目录是否存在String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);File chunkFileFolder = new File(chunkFileFolderPath);if (!chunkFileFolder.exists()) {chunkFileFolder.mkdirs();}// 上传文件输入流InputStream inputStream = null;FileOutputStream outputStream = null;try {inputStream = file.getInputStream();outputStream = new FileOutputStream(new File(chunkFileFolderPath + chunk));IOUtils.copy(inputStream, outputStream);} catch (IOException e) {e.printStackTrace();} finally {try {inputStream.close();} catch (IOException e) {e.printStackTrace();}try {outputStream.close();} catch (IOException e) {e.printStackTrace();}}return ResponseResult.SUCCESS();}

分块的合并与校验

全部分块上传后,我们就要进行合并分块的工作、校验、与入库。

合并分块:使用RandomAccessFile类进行读写操作,将排序好的分块列表遍历写入到合并文件。

校验:就是将前端WebUploader所生成的MD5与后端合并好的文件所生成的MD5进行比对。

    // 合并分块、校验MD5、数据入库public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {// 1. 合并分块String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);File chunkFileFolder = new File(chunkFileFolderPath);File[] files = chunkFileFolder.listFiles();String filePath = this.getFilePath(fileMd5, fileExt);File mergeFile = new File(filePath);List<File> fileList = Arrays.asList(files);// 合并mergeFile = this.mergeFile(fileList, mergeFile);if (mergeFile == null) {ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);}// 2. 校验文件MD5是否与前端传入一致boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);// 校验失败if (!checkResult) {ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);}// 3. 文件信息写入MongoDBMediaFile mediaFile = new MediaFile();mediaFile.setFileId(fileMd5);mediaFile.setFileOriginalName(fileName);mediaFile.setFileName(fileMd5 + "." + fileExt);// 文件相对路径String relativeFilePath = fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2)+ "/" + fileMd5 + "/";mediaFile.setFilePath(relativeFilePath);mediaFile.setFileSize(fileSize);mediaFile.setUploadTime(new Date());mediaFile.setMimeType(mimetype);mediaFile.setFileType(fileExt);// 上传成功状态码mediaFile.setFileStatus(UPLOADED);mediaFileRepository.save(mediaFile);return ResponseResult.SUCCESS();}/*** 合并文件* @param chunkFileList* @param mergeFile* @return*/private File mergeFile(List<File> chunkFileList, File mergeFile) {try {// 有删 无创建if (mergeFile.exists()) {mergeFile.delete();} else {mergeFile.createNewFile();}// 排序Collections.sort(chunkFileList, new Comparator<File>() {@Overridepublic int compare(File o1, File o2) {if (Integer.parseInt(o1.getName()) > Integer.parseInt(o2.getName())) {return 1;}return -1;}});byte[] b = new byte[1024];RandomAccessFile writeFile = new RandomAccessFile(mergeFile, "rw");for (File chunkFile : chunkFileList) {RandomAccessFile readFile = new RandomAccessFile(chunkFile, "r");int len = -1;while ((len = readFile.read(b)) != -1) {writeFile.write(b, 0, len);}readFile.close();}writeFile.close();return mergeFile;} catch (IOException e) {e.printStackTrace();return null;}}/*** 校验文件MD5* @param mergeFile* @param md5* @return*/private boolean checkFileMd5(File mergeFile, String md5) {try {// 得到文件MD5FileInputStream inputStream = new FileInputStream(mergeFile);String md5Hex = DigestUtils.md5Hex(inputStream);if (StringUtils.equalsIgnoreCase(md5, md5Hex)) {return true;}} catch (Exception e) {e.printStackTrace();}return false;}

效果测试

在上传过程中,关闭上传页面(中断上传)再次上传,在checkchunk请求时发现分块都已存在,继续checkchunk直到分块不存在再进行uploadchunk。
在这里插入图片描述
分块全部上传完会请求合并,成功后提示上传成功。
在这里插入图片描述
磁盘
在这里插入图片描述
数据库

主键为文件的MD5值
在这里插入图片描述
而当上传相同文件的时候,因为在register的准备工作中做过了判断,因为会上传不成功。
在这里插入图片描述


BUG

有一点比较迷的是,其他文件都能上传成功,而我的英雄学院这集动画上传后在磁盘中分块和合并视频都存在,而且也能正常播放,但是后端生成的md5码就是与前端的不同,导致返回上传不成功的状态码。
在这里插入图片描述
然后我找了个在线生成文件MD5码的网站:
在这里插入图片描述
和后端生成的一样,那说明问题出在前端(没错本人前端cv工程师)。

然后我再看了下前端的生成md5的方法:
md5File(file, 0, 100*1024*1024)

好吧这里做了文件大小的限制,而动漫视频么,挺大的,把它设置大点后,生成的md5就与后端相同了,视频上传成功。

md5File( file[, start[, end]] ) ⇒ promise

           md5File: function( file, start, end ) {var md5 = new Md5(),deferred = Base.Deferred(),blob = (file instanceof Blob) ? file :this.request( 'get-file', file ).source;md5.on( 'progress load', function( e ) {e = e || {};deferred.notify( e.total ? e.loaded / e.total : 1 );});md5.on( 'complete', function() {deferred.resolve( md5.getResult() );});md5.on( 'error', function( reason ) {deferred.reject( reason );});if ( arguments.length > 1 ) {start = start || 0;end = end || 0;start < 0 && (start = blob.size + start);end < 0 && (end = blob.size + end);end = Math.min( end, blob.size );blob = blob.slice( start, end );}md5.loadFromBlob( blob );return deferred.promise();}

这篇关于WebUploader实现分块上传(断点续传)着重后端Java代码的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/555330

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

java图像识别工具类(ImageRecognitionUtils)使用实例详解

《java图像识别工具类(ImageRecognitionUtils)使用实例详解》:本文主要介绍如何在Java中使用OpenCV进行图像识别,包括图像加载、预处理、分类、人脸检测和特征提取等步骤... 目录前言1. 图像识别的背景与作用2. 设计目标3. 项目依赖4. 设计与实现 ImageRecogni

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

Java访问修饰符public、private、protected及默认访问权限详解

《Java访问修饰符public、private、protected及默认访问权限详解》:本文主要介绍Java访问修饰符public、private、protected及默认访问权限的相关资料,每... 目录前言1. public 访问修饰符特点:示例:适用场景:2. private 访问修饰符特点:示例:

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

使用Python实现在Word中添加或删除超链接

《使用Python实现在Word中添加或删除超链接》在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能,本文将为大家介绍一下Python如何实现在Word中添加或... 在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能。通过添加超

windos server2022里的DFS配置的实现

《windosserver2022里的DFS配置的实现》DFS是WindowsServer操作系统提供的一种功能,用于在多台服务器上集中管理共享文件夹和文件的分布式存储解决方案,本文就来介绍一下wi... 目录什么是DFS?优势:应用场景:DFS配置步骤什么是DFS?DFS指的是分布式文件系统(Distr

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服