WebUploader+springboot 实现文件断点续传
程序员文章站
2022-05-26 16:06:08
...
在网上参照使用WebUploader上传文件断点续传,使用WebUploader和 springboot实现文件断点续传。
主要思路:文件分段上传后存储,在文件最终全部分片都上传成功后讲分片文件进行合并。
前端代码:
<h3 >视频文件上传</h3> <div style="margin: 20px 20px 20px 0;"> <div id="picker" class="form-control-focus" >选择文件</div> </div> <div id="thelist" class="uploader-list"></div> <button id="btnSync" type="button" class="btn btn-primary">开始上传</button> <button id="btnStop" type="button" class="btn btn-danger">暂停</button> <button id="btnRetry" type="button" class="btn btn-info">继续</button> <div class="progress"> <div id="progress" class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"> <span class="sr-only"></span> </div> </div> <script> _extensions = '3gp,mp4,rmvb,mov,avi,m4v,qlv,wmv'; _mimeTypes = 'video/*,audio/*,application/*'; var fileMd5;//文件唯一标识 var fileId;//文件ID var fileName;//文件名称 var count = 0;//当前正在上传的文件在数组中的下标,一次上传多个文件时使用 var filesArr = new Array();//文件数组:每当有文件被添加进队列的时候 就push到数组中 var map = {};//key存储文件id,value存储该文件上传过的进度 //监听分块上传的三种状态 WebUploader.Uploader.register({ "before-send-file": "beforeSendFile",//整个文件上传前 "before-send": "beforeSend", //每个分片上传前 "after-send-file": "afterSendFile", //分片上传完毕 }, { //所有分块进行上传之前调用此函数 beforeSendFile: function (file) { //alert('分块上传前调用的函数'); var deferred = WebUploader.Deferred(); //1、计算文件的唯一标记fileMd5,用于断点续传 如果.md5File(file)方法里只写一个file参数则计算MD5值会很慢 所以加了后面的参数:5*1024*1024 (new WebUploader.Uploader()).md5File(file, 0, 5 * 1024 * 1024).progress(function (percentage) { $('#' + file.id).find('p.state').text('正在读取文件信息...'); }) .then(function (val) { $('#' + file.id).find("p.state").text("正在上传..."); fileMd5 = val; fileId = file.id; uploader.options.formData.guid = fileMd5; //获取文件信息后进入下一步 deferred.resolve(); }); fileName = file.name; //为自定义参数文件名赋值 return deferred.promise(); }, //如果有分块上传,则每个分块上传之前调用此函数 ,检验该分片是否上传过 beforeSend: function (block) { //alert('-检验分块是否上传-'); var deferred = WebUploader.Deferred(); $.ajax({ type: "POST", url: "/common/webuploader/checkChunk", //ajax验证每一个分片 data: { fileName: fileName, fileMd5: fileMd5, //文件唯一标记 chunk: block.chunk, //当前分块下标 chunkSize: block.end - block.start,//当前分块大小 guid: uploader.options.formData.guid }, cache: false, async: false, // 与js同步 timeout: 1000, //todo 超时的话,只能认为该分片未上传过 dataType: "json", success: function (response) { if (response.existFlag) { $('#' + fileId).find("p.state").text("正在续传..."); //分块存在,跳过 deferred.reject(); } else { //分块不存在或不完整,重新发送该分块内容 deferred.resolve(); } } }); this.owner.options.formData.fileMd5 = fileMd5; deferred.resolve(); //继续执行分片上传 return deferred.promise(); }, //所有分块上传成功后调用此函数,通知后台合并所用分块 afterSendFile: function () { //alert('-所有分块上传完成后调用该函数-'); //如果分块上传成功,则通知后台合并分块 $.ajax({ type: "POST", url: "/common/webuploader/uploadSuccess", //ajax将所有片段合并成整体 data: { fileName: fileName, fileMd5: fileMd5, guid: uploader.options.formData.guid }, success: function () { count++; //每上传完成一个文件 count+1 if (count <= filesArr.length - 1) { uploader.upload(filesArr[count].id);//上传文件列表中的下一个文件 } //合并成功之后的操作 } }); } }); var uploader = WebUploader.create({ // swf文件路径 swf: 'webuploader/Uploader.swf', // 文件接收服务端。 server: '/common/webuploader/upload', // 选择文件的按钮。可选。 // 内部根据当前运行是创建,可能是input元素,也可能是flash. pick: { id: '#picker', multiple: true }, accept: { title: 'Videos', extensions: _extensions, mimeTypes: _mimeTypes }, chunked: true, //分片处理 chunkSize: 5 * 1024 * 1024, //每片5M threads: 1,//上传并发数。允许同时最大上传进程数。 // 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传! resize: false }); uploader.on("error", function (type) { if (type == "Q_TYPE_DENIED") { alert("请上传mp4、rmvb、mov、avi、m4v、wmv格式文件"); } }); // 当有文件被添加进队列的时候 uploader.on('fileQueued', function (file) { $("#thelist").append( '<div id="' + file.id + '" class="item">' + '<h4 class="info">' + file.name + '</h4>' + '<p class="file-pro"></p>' + '<p class="state">等待上传...</p>' + '</div>'); }); uploader.on('uploadSuccess', function (file) { $('#' + file.id).find('p.state').text('已上传'); }); uploader.on('uploadError', function (file) { $('#' + file.id).find('p.state').text('上传出错'); }); uploader.on('uploadComplete', function (file) { $('#' + file.id).find('.progress').fadeOut(); }); uploader.on('beforeFileQueued', function (file) { // alert(file.size); }); $("#btnSync").on('click', function () { if ($(this).hasClass('disabled')) { return false; } //显示遮罩层操作 //视频上传 uploader.upload(); }); $("#btnStop").on('click', function () { $('#' + fileId).find("p.state").text("已暂停..."); uploader.stop(true); }); $("#btnRetry").on('click', function () { $('#' + fileId).find("p.state").text("继续上传..."); uploader.upload(); }); // 文件上传过程中创建进度条实时显示 uploader.on('uploadProgress', function (file, percentage) { $("td.file-pro").text(""); var $li = $('#' + file.id).find('.file-pro'), $percent = $li.find('.file-progress .progress-bar'); // 避免重复创建 if (!$percent.length) { $percent = $('<div class="file-progress progress-striped active">' + '<div class="progress-bar" role="progressbar" style="width: 0%">' + '</div>' + '</div>' + '<br/><div class="per">0%</div>').appendTo($li).find('.progress-bar'); } $li.siblings('.file-status').text('上传中'); $li.find('.per').text((percentage * 100).toFixed(2) + '%'); $percent.css('width', percentage * 100 + '%'); }); // 所有文件上传成功后调用 uploader.on('uploadFinished', function () { //隐藏遮罩层操作 }); /*关闭上传框窗口后恢复上传框初始状态*/ $('#picker').on('click', function () { // 移除所有并将上传文件移出上传序列 for (var i = 0; i < uploader.getFiles().length; i++) { // 将文件从上传序列移除 uploader.removeFile(uploader.getFiles()[i]); $("#" + uploader.getFiles()[i].id).remove(); } // 重置uploader uploader.reset(); }) </script>
后台代码:
import com.bootdo.common.config.BootdoConfig; import com.bootdo.common.utils.R; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.util.*; /** * webuploader文件上传 */ @Controller @RequestMapping("/common/webuploader") public class WebUploaderController extends BaseController { private static final int BUFFER_SIZE = 100 * 1024; private static final Logger logger = LoggerFactory.getLogger(WebUploaderController.class); @Autowired private BootdoConfig bootdoConfig; /** * 检测已上传文件分片信息 * @param chunk * @param chunkSize * @param guid * @param fileName * @param fileMd5 * @return */ @ResponseBody @PostMapping(value="checkChunk") public R checkChunk(@RequestParam String chunk, @RequestParam String chunkSize, @RequestParam String guid, @RequestParam String fileName, @RequestParam String fileMd5) { // 获取分块文件临时存储的路径并新建该分块文件 File checkFile = new File(bootdoConfig.getUploadPath() + guid + "/" + chunk); // 检查文件是否存在,且大小是否一致 if (checkFile.exists() && checkFile.length() == Integer.parseInt(chunkSize)) { // 上传过 将结果返回给前段处理 return R.ok().put("existFlag", 1); } else { // 没有上传过 返回给前段做处理 return R.ok().put("existFlag", 0); } } /** * 上传文件 * @param file * @param chunk * @param chunks * @param guid * @param name * @param fileMd5 * @return */ @ResponseBody @PostMapping(value="upload") public R upload(@RequestParam MultipartFile file, @RequestParam Integer chunk, @RequestParam Integer chunks, @RequestParam String guid, @RequestParam String name, @RequestParam String fileMd5) { try { // 临时文件的保存路 //检查文件目录,不存在则创建 String relativePath = bootdoConfig.getUploadPath() + guid; File folder = new File(relativePath); if (!folder.exists()) { folder.mkdirs(); } // 处理获取到的上传文件的文件名的路径部分,只保留文件名部分 String fileName = name.substring(name.lastIndexOf("\\") + 1); // 以chunks分块文件的下标作为上传文件的名字 File tmpFile = new File(folder, String.valueOf(chunk)); //文件已存在删除旧文件(上传了同名的文件) if (chunk == 0 && tmpFile.exists()) { tmpFile.delete(); tmpFile = new File(folder, String.valueOf(chunk)); } //生成临时文件 createFile(file.getInputStream(), tmpFile); if (chunk == chunks - 1) { logger.info("上传完成"); }else { logger.info("还剩["+(chunks-1-chunk)+"]个块文件"); } } catch (IOException e) { logger.error(e.getMessage()); } return R.ok(); } /** * 上传文件成功后合并已上传文件块 * @param guid * @param fileName * @param fileMd5 */ @ResponseBody @PostMapping(value="uploadSuccess") public void uploadSuccess(@RequestParam String guid, @RequestParam String fileName, @RequestParam String fileMd5) { logger.info("开始合并。。。文件唯一表示符=" + guid + ";文件名字=" + fileName); // 进行文件合并 File newFile = new File(bootdoConfig.getUploadPath() + fileName); FileInputStream temp = null; FileOutputStream outputStream = null; File dir = new File(bootdoConfig.getUploadPath() + guid); File[] childs = dir.listFiles(); List<File> files = Arrays.asList(childs); Collections.sort(files, new Comparator<File>() { @Override public int compare(File o1, File o2) { int o1Num = Integer.parseInt(o1.getName()); int o2Num = Integer.parseInt(o2.getName()); return o1Num - o2Num; } }); try { int len; byte[] byt = new byte[5 * 1024 * 1024]; outputStream = new FileOutputStream(newFile, true);// 文件追加写入 for (int i = 0; i < files.size(); i++) { temp = new FileInputStream(files.get(i)); while ((len = temp.read(byt)) != -1) { outputStream.write(byt, 0, len); } temp.close(); temp = null; } } catch (IOException e) { throw new RuntimeException("合并文件失败"); } finally { if (temp != null) { try { temp.close(); } catch (IOException e) { e.printStackTrace(); } } try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } for (int i = 0; i < files.size(); i++) { //删除临时文件 files.get(i).delete(); } //删除临时文件夹 dir.delete(); } /** * 生成目标文件 * @param in * @param destFile */ private void createFile(InputStream in, File destFile) { OutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(destFile), BUFFER_SIZE); in = new BufferedInputStream(in, BUFFER_SIZE); int len = 0; byte[] buffer = new byte[BUFFER_SIZE]; while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } } catch (Exception e) { logger.error(e.getMessage()); } finally { try { if (null != in) { in.close(); } if(null != out){ out.close(); } } catch (IOException e) { logger.error(e.getMessage()); } } } }