我的流程是前台选择上传大文件,通过webuploader插件切片文件,创建上传辅助类(MultipartFileParam)封装上传需要的name以及分片信息,md5等。上传到了我本地的d:mnt/data/下,根据redis中的文件对应的md5值去校验文件,达到断点上传的过程,文件merge(合并)完,删除临时文件目录及分片文件,然后记录文件的uuid以及路径以及文件length到数据库。断点下载参考的网上的教程,需要注意的是发送get请求的方式非Ajax请求,此处我使用了window.location.href.如有不足请多指教。
1.使用的插件
页面引入css以js(依赖jq)
2.页面代码
var file_md5 = null; // 用于MD5校验文件 var chunk_md5 = null; var secSend = false; var uploader; var file_chunks; WebUploader.Uploader.register({ //文件发送之前执行 "before-send-file":"beforeSendFile", //在文件分片(如果没有启用分片,整个文件被当成一个分片)后,上传之前执行 "before-send":"beforeSend", //在文件所有分片都上传完后,且服务端没有错误返回后执行 "after-send-file":"afterSendFile" },{ beforeSendFile:function(file){ var me = this, owner = this.owner, deferred = WebUploader.Deferred(); log("正在计算MD5值..."); log("<div id='md5"+file.id+"'></div>"); uploader.md5File(file,0,1*1024*1024) //新版本jquery不会调用 .progress(function(percentage) { $('#md5'+file.id).html((percentage*100).toFixed()+'%'); }) .then(function (fileMd5) { file_md5 = fileMd5; log("MD5计算完成。"); // 检查是否有已经上传成功的分片文件 $.post('courseImage/check', JSON.stringify({md5: file_md5}), function (data) { console.log(data); log(data.data); if (data.data) { uploader.skipFile(file); secSend = true; $.post('courseImage/merge', JSON.stringify({ md5: file_md5, name: file.name }), function (data) { log(data.msg); deferred.resolve(); parent.location.reload(); }); } deferred.resolve(); }) }); return deferred.promise(); }, beforeSend:function (block) { var me = this, owner = this.owner, blob = block.blob, deferred = WebUploader.Deferred(); file_chunks = block.chunks; if(block.chunks>1){ uploader.md5File(block.blob).then(function (value) { chunk_md5 = value; deferred.resolve(); }) }else { deferred.resolve(); } return deferred.promise(); }, afterSendFile:function (file) { var deferred = WebUploader.Deferred(); console.log(file_chunks>1 && !secSend); //判断是否要分片 if(file_chunks>1 && !secSend){ $.post('courseImage/merge', JSON.stringify({ md5: file_md5, name: file.name }), function (data) { log(data.msg); deferred.resolve(); parent.location.reload(); }); }else { deferred.resolve(); } return deferred.promise(); } }) // 创建上传 uploader = WebUploader.create({ swf: '../../js/Uploader.swf', server: '/courseImage/upload', // 服务端地址 pick: '#picker', // 指定选择文件的按钮容器 resize: false, chunked: true, //开启分片上传 chunkSize: 1024 * 1024 * 1, //每一片的大小 chunkRetry: 3, // 如果遇到网络错误,重新上传次数 threads: 3, // [默认值:3] 上传并发数。允许同时最大上传进程数。 auto: true, // 选完文件后,是否自动上传 // fileNumLimit:1, // fileSizeLimit:1024*1024*1024*1.5, }); // 上传提交 $("#ctlBtn").click(function () { log('准备上传...'); uploader.upload(); }); // 当有文件被添加进队列的时候-md5序列化 uploader.on('fileQueued', function (file) { $("#filelist").append( '<div id="' + file.id + '" class="item">' + '<h4 class="info">' + file.name + '</h4>' + '<p class="state">等待上传...</p>' + '</div>' ); }); // 发送前在body中添加一些信息,如分块数目chunks uploader.on('uploadBeforeSend', function( block, data ) { //事件里不支持这种用法 // var deferred = WebUploader.Deferred(); data.chunks = block.chunks; data.md5 = file_md5; console.log(file_md5+"-------------------"+block.chunks) }); // 上传完成后触发 uploader.on('uploadSuccess', function (file,response) { if(secSend){ $("#percentage_a").css("width","100%"); $("#percentage").html("100%"); } log("上传完成"); }); // 文件上传过程中创建进度条实时显示。 uploader.on('uploadProgress', function (file, percentage) { $("#percentage_a").css("width",parseInt(percentage * 100)+"%"); $("#percentage").html(parseInt(percentage * 100) +"%"); }); // 上传出错处理 uploader.on('uploadError', function (file) { uploader.retry(); }); // 暂停处理 $("#stop").click(function(e){ log("暂停上传..."); uploader.stop(true); }) // 从暂停文件继续 $("#start").click(function(e){ log("恢复上传..."); uploader.upload(); }); function log(html) { $("#log").append("<div>"+html+"</div>"); }
3.控制层
@PostMapping("/check") @RequiresPermissions("sys:image:add") public ResultModel checkFileMd5(@RequestBody MultipartFileParam multipartFileParam) { String md5 = multipartFileParam.getMd5(); String md5InRedis = (String) stringRedisTemplate.opsForValue().get("file_md5"); if (!StringUtils.isEmpty(md5InRedis) && md5InRedis.equals(md5)) { return ResultUtil.ok(true);//文件存在 } return ResultUtil.ok(false); } @PostMapping("/upload") @RequiresPermissions("sys:image:add") public ResultModel importImage(MultipartFileParam mParam) { boolean flag = BreakPointUploadUtils.uploadImage(mParam); if (flag) { stringRedisTemplate.opsForValue().set("file_md5", mParam.getMd5()); //上传记录md5----记录到redis } return ResultUtil.ok(); } @PostMapping("/merge") @RequiresPermissions("sys:image:add") public ResultModel mergeChunk(@RequestBody MultipartFileParam mParam) { Map<String, String> map = BreakPointUploadUtils.mergeImage(mParam); //清redis缓存 stringRedisTemplate.delete("file_md5"); if (null != map) { EduCourseImagesModel eduCourseImagesModel = new EduCourseImagesModel(); eduCourseImagesModel.setCreateBy(WebUtil.getCurrentUserId()); eduCourseImagesService.addImage(eduCourseImagesModel, map); } return ResultUtil.ok(); } @GetMapping("/download/{id}") @RequiresPermissions("sys:image:add") public ResultModel downloadFile(@PathVariable Long id) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); HttpServletResponse response = requestAttributes.getResponse(); EduCourseImagesModel eduCourseImagesModel = eduCourseImagesService.selectById(id); return ResultUtil.ok(BreakPointUploadUtils.downloadImage(eduCourseImagesModel, request, response)); }
4.BreakPointUploadUtils工具类
@Component(value = "breakPointUploadUtils") public class BreakPointUploadUtils { private static final Logger logger = Logger.getLogger(UploadUtils.class); // 默认大小 50M(52428800) 改为 200m public static long defaultMaxSize; public static String defaultBaseDir; // 默认的文件名最大长度 public static int DEFAULT_FILE_NAME_LENGTH = 200; public static final String OTHERF_PATH = "ean-upload/files/"; public static final String BIG_FILE_PATH = "ean-upload/image/"; public static final String MEDIA_PATH = "ean-upload/media/"; public static final String[] DEFAULT_ALLOWED_EXTENSION = { // 图片 "bmp", "gif", "jpg", "jpeg", "png", // word excel powerpoint "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", // media "mp4", //zip "zip", // pdf "pdf", "gz", "tar" }; @Value("${ean.upload.default-base-dir}") public void setDefaultBaseDir(String defaultBaseDir) { BreakPointUploadUtils.defaultBaseDir = defaultBaseDir; } @Value("${ean.upload.default-max-size}") public void setDefaultMaxSize(long defaultMaxSize) { BreakPointUploadUtils.defaultMaxSize = defaultMaxSize; } private static final String datePath() { Date now = new Date(); return DateFormatUtils.format(now, "yyyyMMdd"); } public static final void assertAllowed(MultipartFile file, long maxSize) throws BusinessException { //判断文件是否存在 if (file.isEmpty()) { throw new BusinessException(Constants.ResultCodeEnum.BAD_REQUEST); } //判断文件名是否超出限制大小 int fileNamelength = file.getOriginalFilename().length(); if (fileNamelength > UploadUtils.DEFAULT_FILE_NAME_LENGTH) { throw new BusinessException("超过文件名最大长度"); } String extension = Files.getFileExtension(file.getOriginalFilename()); //判断文件后缀名是否合法 if (!Arrays.asList(DEFAULT_ALLOWED_EXTENSION).contains(extension)) { throw new BusinessException(); } //判断文件是否超出限制大小 long size = file.getSize(); if (maxSize != -1 && size > maxSize) { throw new BusinessException("超出文件大小限制"); } } public static boolean uploadImage(MultipartFileParam mParam) { assertAllowed(mParam.getFile(), mParam.getSize()); boolean flag = false; int chunks = mParam.getChunks(); String folder = defaultBaseDir + BIG_FILE_PATH + datePath(); System.out.println(folder); if (chunks == 1 || chunks == 0) {//统一放在图片下 File file = new File(defaultBaseDir + OTHERF_PATH + datePath() + "/" + mParam.getName()); if (!file.isDirectory()) { file.mkdirs();//创建文件夹 } try { byte[] bytes = mParam.getFile().getBytes(); Path path = Paths.get(defaultBaseDir + OTHERF_PATH + datePath() + "/" + mParam.getName()); java.nio.file.Files.write(path, bytes); } catch (IOException e) { e.printStackTrace(); } } else { File dir = new File(folder + "/" + mParam.getName() + ".tmp"); if (!dir.exists()) {//如果文件夹不存在 dir.mkdirs();//创建文件夹 } try { byte[] bytes = mParam.getFile().getBytes(); Path path = Paths.get(folder + "/" + mParam.getName() + ".tmp/" + mParam.getChunk()); java.nio.file.Files.write(path, bytes); flag = true; } catch (IOException e) { e.printStackTrace(); } } return flag; } /** * 断点上传 * * @param mParam * @return */ public static Map<String, String> mergeImage(MultipartFileParam mParam) { Map<String, String> map = new HashMap<>(); String newFilename = UUID.randomUUID().toString().replaceAll("-", "") + "." + Files.getFileExtension(mParam.getName()); File dir = new File(defaultBaseDir + BIG_FILE_PATH + datePath() + "/" + mParam.getName() + ".tmp"); File file2 = null; if (dir.exists()) { File[] files = dir.listFiles(); List<File> fileList = Arrays.asList(files); //按时间排序 Collections.sort(fileList, new Comparator<File>() { @Override public int compare(File o1, File o2) { if (o1.isDirectory() && o2.isFile()) return -1; if (o1.isFile() && o2.isDirectory()) return 1; return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName()); } }); for (File file : fileList) { System.out.println(file.getAbsolutePath()); try { //读取小文件的输入流 InputStream in = new FileInputStream(file); //写入大文件的输出流 file2 = new File(defaultBaseDir + BIG_FILE_PATH + datePath() + "/" + newFilename); OutputStream out = new FileOutputStream(file2, true); int len = -1; byte[] bytes = new byte[1 * 1024 * 1024]; while ((len = in.read(bytes)) != -1) { out.write(bytes, 0, len); } out.close(); in.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } file.delete(); } dir.delete(); } map.put("imagePath", file2.getAbsolutePath().substring(12)); map.put("imageUUID", newFilename.substring(0, newFilename.lastIndexOf("."))); map.put("fileSize", String.valueOf(file2.length())); return map; } /** * 断点下载镜像文件 * * @param eduCourseImagesModel * @param request * @param response * @return */ public static boolean downloadImage(EduCourseImagesModel eduCourseImagesModel, HttpServletRequest request, HttpServletResponse response) { boolean flag = false; String fullPath = eduCourseImagesModel.getImagePath(); File downloadFile = new File(defaultBaseDir + "/" + fullPath); ServletContext context = request.getServletContext(); String mimeType = context.getMimeType(fullPath); if (mimeType == null) { // set to binary type if MIME mapping not found mimeType = "application/octet-stream"; } // set content attributes for the response response.setContentType(mimeType); // response.setContentLength((int) downloadFile.length()); // set headers for the response String headerKey = "Content-Disposition"; String headerValue = String.format("attachment; filename=\"%s\"", downloadFile.getName()); response.setHeader(headerKey, headerValue); // 解析断点续传相关信息 response.setHeader("Accept-Ranges", "bytes"); long downloadSize = downloadFile.length(); long fromPos = 0, toPos = 0; if (request.getHeader("Range") == null) { response.setHeader("Content-Length", downloadSize + ""); } else { // 若客户端传来Range,说明之前下载了一部分,设置206状态(SC_PARTIAL_CONTENT) response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); String range = request.getHeader("Range"); String bytes = range.replaceAll("bytes=", ""); String[] ary = bytes.split("-"); fromPos = Long.parseLong(ary[0]); if (ary.length == 2) { toPos = Long.parseLong(ary[1]); } int size; if (toPos > fromPos) { size = (int) (toPos - fromPos); } else { size = (int) (downloadSize - fromPos); } response.setHeader("Content-Length", size + ""); downloadSize = size; } // Copy the stream to the response's output stream. RandomAccessFile in = null; OutputStream out = null; try { in = new RandomAccessFile(downloadFile, "rw"); // 设置下载起始位置 if (fromPos > 0) { in.seek(fromPos); } // 缓冲区大小 int bufLen = (int) (downloadSize < 2048 ? downloadSize : 2048); byte[] buffer = new byte[bufLen]; int num; int count = 0; // 当前写到客户端的大小 out = response.getOutputStream(); while ((num = in.read(buffer)) != -1) { out.write(buffer, 0, num); count += num; //处理最后一段,计算不满缓冲区的大小 if (downloadSize - count < bufLen) { bufLen = (int) (downloadSize - count); if (bufLen == 0) { break; } buffer = new byte[bufLen]; } } response.flushBuffer(); flag = true; } catch (IOException e) { e.printStackTrace(); } finally { if (null != out) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != in) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } return flag; } }
5.MultipartFileParam
public class MultipartFileParam { //任务ID private String id; //总分片数量 private int chunks; //当前为第几块分片 private int chunk; //当前分片大小 private long size = 0L; //文件名 private String name; //分片对象 private MultipartFile file; // MD5 private String md5;
get set 略
}
6.yml大文件上传配置
注 enabled:true需要去掉,改false测试也是不可以
7.补html代码(我使用了bootstrap.min.css)
<link rel="stylesheet" href="../../css/bootstrap.min.css" media="all"/> <link rel="stylesheet" href="../../css/webuploader.css" media="all"/>
<div class="container"> <div class="row"> <div class="col-md-4"> <div class="panel panel-default"> <div class="panel-heading">上传文件</div> <div class="panel-body"> <div id="picker">选择文件</div> <div id="filelist"></div> </div> </div> </div> <div class="col-md-4"> <div class="panel panel-default"> <div class="panel-heading">上传进度及日志</div> <div class="panel-body"> <div> <div class="progress"> <div class="progress-bar progress-bar-warning progress-bar-striped" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 0%" id="percentage_a"> <span id="percentage">0%</span> </div> </div> </div> <div id="log" style="max-height: 600px;overflow: auto;"> </div> </div> </div> </div> <div class="col-md-3"> <div class="panel panel-default"> <div class="panel-heading">上传控制</div> <div class="panel-body"> <div id="ctlBtn" class="btn-group"> <input class="btn btn-primary" type="button" value="开始上传"> </div> <div class="btn-group"> <input class="btn btn-danger" type="button" value="暂停" id="stop"> </div> <div class="btn-group"> <input class="btn btn-success" type="button" value="继续" id="start"> </div> </div> </div> </div> </div> </div> <script type="text/javascript" src="jquery1.11.1.min.js"></script> <script type="text/javascript" src="webuploader.js"></script>