1 文件不分块
客户端会记录当前服务器的已经上传文件的大小,等到下载网络连接成功,给服务器发生我已经上传文件的大小,并继续发生字节
服务器接收已经上传文件的大小,用这个大小来将移动文件的cursor,进行append
2 文件分块
客户端首先将需要上传的文件分块,网络恢复正常,已经上传的文件分块不在上传,上传失败的文件分块重新上传
服务器将接受到的文件分块进行合并
Web客户端实现断点上传功能
WebUploader
官网:fexteam.gz01.bdysite.com/webuploader/
http://fex.baidu.com/webuploader/
web
<template> <div><br/> 操作步骤:<br/> 1、点击“选择文件”,选择要上传的文件<br/> 2、点击“开始上传”,开始上传文件<br/> 3、如需重新上传请重复上边的步骤。<br/><br/> <div id="uploader" class="wu-example"> <div class="btns" style="float:left;padding-right: 20px"> <div id="picker">选择文件</div> </div> <div id="ctlBtn" class="webuploader-pick" @click="upload()">开始上传</div> </div> <!--用来存放文件信息--> <div id="thelist" class="uploader-list"> <div v-if="uploadFile.id" :id='uploadFile.id'><span>{{uploadFile.name}}</span> <span class='percentage'>{{percentage}}%</span> </div> </div> </div> </template> <script> import $ from '../../../../static/plugins/jquery/dist/jquery.js' import webuploader from '../../../../static/plugins/webuploader/dist/webuploader.js' import '../../../../static/css/webuploader/webuploader.css' export default { data() { return { uploader: {}, uploadFile: {}, percentage: 0, fileMd5: '' } }, methods: { //开始上传 upload() { if (this.uploadFile && this.uploadFile.id) { this.uploader.upload(this.uploadFile.id); } else { alert("请选择文件"); } } }, mounted() { // var fileMd5; // var uploadFile; WebUploader.Uploader.register({ "before-send-file": "beforeSendFile", "before-send": "beforeSend", "after-send-file": "afterSendFile" }, { 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; // alert(this.fileMd5 ) //向服务端请求注册上传文件 $.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), 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参数,上传分块时带上fileMd5 this.uploader.options.formData.fileMd5 = this.fileMd5; this.uploader.options.formData.chunk = block.chunk; return deferred.promise(); }.bind(this), afterSendFile: function (file) { // 合并分块 $.ajax( { type: "POST", url: "/api/media/upload/mergechunks", data: { fileMd5: this.fileMd5, fileName: file.name, fileSize: file.size, mimetype: file.type, fileExt: file.ext }, success: function (response) { //在这里解析合并成功结果 if (response && response.success) { alert("上传成功") } else { alert("上传失败") } } } ); }.bind(this) } ); // 创建uploader对象,配置参数 this.uploader = WebUploader.create( { swf: "/static/plugins/webuploader/dist/Uploader.swf",//上传文件的flash文件,浏览器不支持h5时启动flash server: "/api/media/upload/uploadchunk",//上传分块的服务端地址,注意跨域问题 fileVal: "file",//文件上传域的name pick: "#picker",//指定选择文件的按钮容器 auto: false,//手动触发上传 disableGlobalDnd: true,//禁掉整个页面的拖拽功能 chunked: true,// 是否分块上传 chunkSize: 1 * 1024 * 1024, // 分块大小(默认5M) threads: 3, // 开启多个线程(默认3个) prepareNextFile: true// 允许在文件传输时提前把下一个文件准备好 } ); // 将文件添加到队列 this.uploader.on("fileQueued", function (file) { this.uploadFile = file; this.percentage = 0; }.bind(this) ); //选择文件后触发 this.uploader.on("beforeFileQueued", function (file) { // this.uploader.removeFile(file) //重置uploader this.uploader.reset() this.percentage = 0; }.bind(this)); // 监控上传进度 // percentage:代表上传文件的百分比 this.uploader.on("uploadProgress", function (file, percentage) { this.percentage = Math.ceil(percentage * 100); }.bind(this)); //上传失败触发 this.uploader.on("uploadError", function (file, reason) { console.log(reason) alert("上传文件失败"); }); //上传成功触发 this.uploader.on("uploadSuccess", function (file, response) { console.log(response) // alert("上传文件成功!"); }); //每个分块上传请求后触发 this.uploader.on('uploadAccept', function (file, response) { if (!(response && response.success)) {//分块上传失败,返回false return false; } }); } } </script> <style scoped> </style>
server
package com.xuecheng.manage_media.service; import com.alibaba.fastjson.JSON; import com.xuecheng.framework.domain.media.MediaFile; import com.xuecheng.framework.domain.media.response.CheckChunkResult; import com.xuecheng.framework.domain.media.response.MediaCode; import com.xuecheng.framework.exception.ExceptionCast; import com.xuecheng.framework.model.response.CommonCode; import com.xuecheng.framework.model.response.ResponseResult; import com.xuecheng.manage_media.config.RabbitMQConfig; import com.xuecheng.manage_media.dao.MediaFileRepository; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.IOUtils; import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.util.*; /** * @author Administrator * @version 1.0 **/ @Service public class MediaUploadService { @Autowired MediaFileRepository mediaFileRepository; @Value("${xc-service-manage-media.upload-location}") String upload_location; @Value("${xc-service-manage-media.mq.routingkey-media-video}") String routingkey_media_video; @Autowired RabbitTemplate rabbitTemplate; //得到文件所属目录路径 private String getFileFolderPath(String fileMd5){ return upload_location + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/"; } //得到文件的路径 private String getFilePath(String fileMd5,String fileExt){ return upload_location + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" + fileMd5 + "." +fileExt; } //得到块文件所属目录路径 private String getChunkFileFolderPath(String fileMd5){ return upload_location + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/chunk/"; } /** * 文件上传前的注册,检查文件是否存在 * 根据文件md5得到文件路径 * 规则: * 一级目录:md5的第一个字符 * 二级目录:md5的第二个字符 * 三级目录:md5 * 文件名:md5+文件扩展名 * @param fileMd5 文件md5值 * @param fileExt 文件扩展名 * @return 文件路径 */ public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) { //1 检查文件在磁盘上是否存在 //文件所属目录的路径 String fileFolderPath = this.getFileFolderPath(fileMd5); //文件的路径 String filePath =this.getFilePath(fileMd5,fileExt); File file = new File(filePath); //文件是否存在 boolean exists = file.exists(); //2 检查文件信息在mongodb中是否存在 Optional<MediaFile > optional = mediaFileRepository.findById(fileMd5); if(exists && optional.isPresent()){ //文件存在 ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST); } //文件不存在时作一些准备工作,检查文件所在目录是否存在,如果不存在则创建 File fileFolder = new File(fileFolderPath); if(!fileFolder.exists()){ fileFolder.mkdirs(); } return new ResponseResult(CommonCode.SUCCESS); } //分块检查(前端会将文件进行分块,每一个块都会进行checkchunk()) /** * * @param fileMd5 文件md5 * @param chunk 块的下标 * @param chunkSize 块的大小 * @return */ public CheckChunkResult checkchunk(String fileMd5, Integer chunk, Integer chunkSize) { //检查分块文件是否存在 //得到分块文件的所在目录 String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5); //块文件 File chunkFile = new File(chunkFileFolderPath + chunk); if(chunkFile.exists()){ //块文件存在 return new CheckChunkResult(CommonCode.SUCCESS,true); }else{ //块文件不存在 return new CheckChunkResult(CommonCode.SUCCESS,false); } } //上传分块 public ResponseResult uploadchunk(MultipartFile file, String fileMd5, Integer chunk) { //检查分块目录,如果不存在则要自动创建 //得到分块目录 String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5); //得到分块文件路径 String chunkFilePath = chunkFileFolderPath + chunk; 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(chunkFilePath)); 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 new ResponseResult(CommonCode.SUCCESS); } //合并文件 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(); List<File> fileList = Arrays.asList(files); //创建一个合并文件 String filePath = this.getFilePath(fileMd5, fileExt); File mergeFile = new File(filePath); //执行合并 mergeFile = this.mergeFile(fileList, mergeFile); if(mergeFile == null){ //合并文件失败 ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL); } //2、校验文件的md5值是否和前端传入的md5一到 boolean checkFileMd5 = this.checkFileMd5(mergeFile, fileMd5); if(!checkFileMd5){ //校验文件失败 ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL); } //3、将文件的信息写入mongodb MediaFile mediaFile = new MediaFile(); mediaFile.setFileId(fileMd5); mediaFile.setFileOriginalName(fileName); mediaFile.setFileName(fileMd5 + "." +fileExt); //文件路径保存相对路径 String filePath1 = fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/"; mediaFile.setFilePath(filePath1); mediaFile.setFileSize(fileSize); mediaFile.setUploadTime(new Date()); mediaFile.setMimeType(mimetype); mediaFile.setFileType(fileExt); //状态为上传成功 mediaFile.setFileStatus("301002"); mediaFileRepository.save(mediaFile); //向MQ发送视频处理消息 sendProcessVideoMsg(mediaFile.getFileId()); return new ResponseResult(CommonCode.SUCCESS); } /** * 发送视频处理消息 * @param mediaId 文件id * @return */ public ResponseResult sendProcessVideoMsg(String mediaId){ //查询数据库mediaFile Optional<MediaFile> optional = mediaFileRepository.findById(mediaId); if(!optional.isPresent()){ ExceptionCast.cast(CommonCode.FAIL); } //构建消息内容 Map<String,String> map = new HashMap<>(); map.put("mediaId",mediaId); String jsonString = JSON.toJSONString(map); //向MQ发送视频处理消息 try { rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK,routingkey_media_video,jsonString); } catch (AmqpException e) { e.printStackTrace(); return new ResponseResult(CommonCode.FAIL); } return new ResponseResult(CommonCode.SUCCESS); } //校验文件 private boolean checkFileMd5(File mergeFile,String md5){ try { //创建文件输入流 FileInputStream inputStream = new FileInputStream(mergeFile); //得到文件的md5 String md5Hex = DigestUtils.md5Hex(inputStream); //和传入的md5比较 if(md5.equalsIgnoreCase(md5Hex)){ return true; } } catch (Exception e) { e.printStackTrace(); return false; } return false; } //合并文件 private File mergeFile(List<File> chunkFileList, File mergeFile) { try { //如果合并文件已存在则删除,否则创建新文件 if (mergeFile.exists()) { mergeFile.delete(); } else { //创建一个新文件 mergeFile.createNewFile(); } //对块文件进行排序 Collections.sort(chunkFileList, new Comparator<File>() { @Override public int compare(File o1, File o2) { if(Integer.parseInt(o1.getName())>Integer.parseInt(o2.getName())){ return 1; } return -1; } }); //创建一个写对象 RandomAccessFile raf_write = new RandomAccessFile(mergeFile,"rw"); byte[] b = new byte[1024]; for(File chunkFile:chunkFileList){ RandomAccessFile raf_read = new RandomAccessFile(chunkFile,"r"); int len = -1; while ((len = raf_read.read(b))!=-1){ raf_write.write(b,0,len); } raf_read.close(); } raf_write.close(); return mergeFile; } catch (IOException e) { e.printStackTrace(); return null; } } }