1、添加js和CSS文件
<link rel="stylesheet" type="text/css" href="/resources/css/WebUploader/webuploader.css" /> <script type="text/javascript" src="/resources/js/jquery-1.7.min.js"></script> <script type="text/javascript" src="/resources/js/WebUploader/webuploader.js"></script> <script type="text/javascript" src="/resources/js/WebUploader/md5.js"></script> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <style type="text/css"> .itemDel, .itemStop, .itemUpload{ margin-left: 15px; color: blue; cursor: pointer; } #theList{ width: 80%; min-height: 100px; border: 1px solid red; } #theList .itemStop{ display: none; } </style>
2、html代码
<div id="uploader"> <ul id="theList"></ul> <div id="picker">选择文件</div> </div>3、js代码
<script type="text/javascript"> var userInfo = {userId:"kazaff", md5:""}; //用户会话信息 var chunkSize = 5 *1024 * 1024; //分块大小 var uniqueFileName = null; //文件唯一标识符 var md5Mark = null; function getServer(type){ //测试用,根据不同类型的后端返回对应的请求地址 return "<%=basePath%>fileUpload"; } var backEndUrl = getServer("java"); WebUploader.Uploader.register({ "before-send-file": "beforeSendFile" , "before-send": "beforeSend" , "after-send-file": "afterSendFile" }, { beforeSendFile: function(file){ //秒传验证 var task = new $.Deferred(); var start = new Date().getTime(); (new WebUploader.Uploader()).md5File(file, 0, 10*1024*1024).progress(function(percentage){ console.log(percentage); }).then(function(val){ console.log("总耗时: "+((new Date().getTime()) - start)/1000); md5Mark = val; userInfo.md5 = val; $.ajax({ type: "POST" , url: backEndUrl , data: { status: "md5Check" , md5: val } , cache: false , timeout: 1000 //todo 超时的话,只能认为该文件不曾上传过 , dataType: "json" }).then(function(data, textStatus, jqXHR){ //console.log(data); if(data.ifExist){ //若存在,这返回失败给WebUploader,表明该文件不需要上传 task.reject(); uploader.skipFile(file); file.path = data.path; UploadComlate(file); }else{ task.resolve(); //拿到上传文件的唯一名称,用于断点续传 uniqueFileName = md5(''+userInfo.userId+file.name+file.type+file.lastModifiedDate+file.size); } }, function(jqXHR, textStatus, errorThrown){ //任何形式的验证失败,都触发重新上传 task.resolve(); //拿到上传文件的唯一名称,用于断点续传 uniqueFileName = md5(''+userInfo.userId+file.name+file.type+file.lastModifiedDate+file.size); }); }); return $.when(task); } , beforeSend: function(block){ //分片验证是否已传过,用于断点续传 var task = new $.Deferred(); $.ajax({ type: "POST" , url: backEndUrl , data: { status: "chunkCheck" , name: uniqueFileName , chunkIndex: block.chunk , size: block.end - block.start } , cache: false , timeout: 1000 //todo 超时的话,只能认为该分片未上传过 , dataType: "json" }).then(function(data, textStatus, jqXHR){ if(data.ifExist){ //若存在,返回失败给WebUploader,表明该分块不需要上传 task.reject(); }else{ task.resolve(); } }, function(jqXHR, textStatus, errorThrown){ //任何形式的验证失败,都触发重新上传 task.resolve(); }); return $.when(task); } , afterSendFile: function(file){ var chunksTotal = 0; if((chunksTotal = Math.ceil(file.size/chunkSize)) > 1){ //合并请求 var task = new $.Deferred(); $.ajax({ type: "POST" , url: backEndUrl , data: { status: "chunksMerge" , name: uniqueFileName , chunks: chunksTotal , ext: file.ext , md5: md5Mark } , cache: false , dataType: "json" }).then(function(data, textStatus, jqXHR){ //todo 检查响应是否正常 task.resolve(); file.path = data.path; UploadComlate(file); }, function(jqXHR, textStatus, errorThrown){ task.reject(); }); return $.when(task); }else{ UploadComlate(file); } } }); var uploader = WebUploader.create({ auto:true //允许自动上传 , swf: "/js/WebUploaderUploader.swf" //引用swf文件 , server: backEndUrl //后台响应服务器路径 , pick:{ id:"##picker" //点击触发文件上传的按钮id , multiple:false //是否可选择多个文件(默认为true) } , resize: true //不压缩文件 , paste: document.body //指定监听paste事件的容器 , disableGlobalDnd: true //是否禁止拖拽 , thumb: { //配置生成缩略图的选项 width: 200 , height: 200 , quality: 70 , allowMagnify: true , crop: true //, type: "image/jpeg" } , compress: { //配置上传前压缩的图片的选项 quality: 100 //图片压缩后比例(只允许jpeg格式) , allowMagnify: false //是否允许放大(设为false,保证不失真) , crop: false //是否允许裁剪 , preserveHeaders: true //是否保留头部meta信息 , noCompressIfLarger: true //如果压缩后图片比原图要大,是否选择原图 ,compressSize: 1024 //小于此值不进行压缩(单位:字节) } , accept: { title: 'Images', extensions: 'gif,jpg,jpeg,bmp,png', mimeTypes: 'image/jpg,image/jpeg,image/png,image/bmp' } , duplicate: true //支持重复上传 , prepareNextFile: true //是否允许在文件传输时提前把下一个文件准备好 , chunked: true //是否要分片处理 , chunkSize: chunkSize //分片数量,默认为2 , threads: threads //上传并发数,默认为3 , formData: function(){return $.extend(true, {}, userInfo);} //文件上传请求的参数表 , fileNumLimit: 10 //验证文件总数量, 超出则不允许加入队列 , fileSingleSizeLimit: 1000 * 1024 * 1024 //验证文件总大小是否超出限制, 超出则不允许加入队列 }); uploader.on("fileQueued", function(file){ $("#theList").append( '<li id="'+file.id+'">' + '<img /><span>'+file.name+'</span><span class="itemUpload">上传</span><span class="itemStop">暂停</span><span class="itemDel">删除</span>' + '<div class="percentage"></div>' + '</li>'); var $img = $("#" + file.id).find("img"); uploader.makeThumb(file, function(error, src){ if(error){ $img.replaceWith("<span>不能预览</span>"); } $img.attr("src", src); }); }); $("#theList").on("click", ".itemUpload", function(){ uploader.upload(); //"上传"-->"暂停" $(this).hide(); $(".itemStop").show(); }); $("#theList").on("click", ".itemStop", function(){ uploader.stop(true); //"暂停"-->"上传" $(this).hide(); $(".itemUpload").show(); }); //todo 如果要删除的文件正在上传(包括暂停),则需要发送给后端一个请求用来清除服务器端的缓存文件 $("#theList").on("click", ".itemDel", function(){ uploader.removeFile($(this).parent().attr("id")); //从上传文件列表中删除 $(this).parent().remove(); //从上传列表dom中删除 }); uploader.on("uploadProgress", function(file, percentage){ $("#" + file.id + " .percentage").text(percentage * 100 + "%"); }); function UploadComlate(file){ console.log(file); $("#" + file.id + " .percentage").text("上传完毕"); $(".itemStop").hide(); $(".itemUpload").hide(); $(".itemDel").hide(); } </script>
4、后台代码
(1)、bean层
package com.bean; public class FileInfo { private String md5; private int chunkIndex; private String size; private String name; private String userId; private String id; private int chunks; private int chunk; private String lastModifiedDate; private String type; private String ext; public FileInfo(){} public FileInfo(String name, String size, int chunkIndex){ this.name = name; this.size = size; this.chunkIndex = chunkIndex; } public FileInfo(String userId, String id){ this.userId = userId; this.id = id; } public FileInfo(String md5){ this.md5 = md5; } public FileInfo(int chunks, int chunk, String userId, String id, String name, String size, String lastModifiedDate, String type){ this.userId = userId; this.id = id; this.name = name; this.size = size; this.chunks = chunks; this.chunk = chunk; this.lastModifiedDate = lastModifiedDate; this.type = type; } public FileInfo(String name, int chunks, String ext, String md5){ this.name = name; this.chunks = chunks; this.ext = ext; this.md5 = md5; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getLastModifiedDate() { return lastModifiedDate; } public void setLastModifiedDate(String lastModifiedDate) { this.lastModifiedDate = lastModifiedDate; } public int getChunks() { return chunks; } public void setChunks(int chunks) { this.chunks = chunks; } public int getChunk() { return chunk; } public void setChunk(int chunk) { this.chunk = chunk; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 = md5; } public int getChunkIndex() { return chunkIndex; } public void setChunkIndex(int chunkIndex) { this.chunkIndex = chunkIndex; } public String getSize() { return size; } public void setSize(String size) { this.size = size; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getExt() { return ext; } public void setExt(String ext) { this.ext = ext; } }
(2)、Controller层
package com.controller; import com.bean.FileInfo; import com.service.webUploader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.io.*; @Controller @RequestMapping("/") public class FileUploadController { private final static Logger log = LoggerFactory.getLogger(FileUploadController.class); @Autowired private webUploader wu; @RequestMapping(method = RequestMethod.GET) public String printWelcome(ModelMap model) { model.addAttribute("message", "Hello world!"); return "hello"; } //大文件上传 @RequestMapping(value = "fileUpload", method = RequestMethod.POST) @ResponseBody public String fileUpload(String status, FileInfo info, @RequestParam(value = "file", required = false) MultipartFile file){ String uploadFolder="D:\tupian\"; if(status == null){ //文件上传 if(file != null && !file.isEmpty()){ //验证请求不会包含数据上传,所以避免NullPoint这里要检查一下file变量是否为null try { File target = wu.getReadySpace(info, this.uploadFolder); //为上传的文件准备好对应的位置 if(target == null){ return "{\"status\": 0, \"message\": \"" + wu.getErrorMsg() + "\"}"; } file.transferTo(target); //保存上传文件 //将MD5签名和合并后的文件path存入持久层,注意这里这个需求导致需要修改webuploader.js源码3170行 //因为原始webuploader.js不支持为formData设置函数类型参数,这将导致不能在控件初始化后修改该参数 if(info.getChunks() <= 0){ if(!wu.saveMd52FileMap(info.getMd5(), target.getName())){ log.error("文件[" + info.getMd5() + "=>" + target.getName() + "]保存关系到持久成失败,但并不影响文件上传,只会导致日后该文件可能被重复上传而已"); } } return "{\"status\": 1, \"path\": \"" + target.getName() + "\"}"; }catch(IOException ex){ log.error("数据上传失败", ex); return "{\"status\": 0, \"message\": \"数据上传失败\"}"; } } }else{ if(status.equals("md5Check")){ //秒传验证 String path = wu.md5Check(info.getMd5()); if(path == null){ return "{\"ifExist\": 0}"; }else{ return "{\"ifExist\": 1, \"path\": \"" + path + "\"}"; } }else if(status.equals("chunkCheck")){ //分块验证 //检查目标分片是否存在且完整 if(wu.chunkCheck(this.uploadFolder + "/" + info.getName() + "/" + info.getChunkIndex(), Long.valueOf(info.getSize()))){ return "{\"ifExist\": 1}"; }else{ return "{\"ifExist\": 0}"; } }else if(status.equals("chunksMerge")){ //分块合并 String path = wu.chunksMerge(info.getName(), info.getExt(), info.getChunks(), info.getMd5(), this.uploadFolder); if(path == null){ return "{\"status\": 0, \"message\": \"" + wu.getErrorMsg() + "\"}"; } return "{\"status\": 1, \"path\": \"" + path + "\", \"message\": \"中文测试\"}"; } } log.error("请求参数不完整"); return "{\"status\": 0, \"message\": \"请求参数不完整\"}"; } }(3)、Service层
package com.service; import com.bean.FileInfo; import com.util.fileLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; import java.io.*; import java.nio.channels.FileChannel; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.locks.Lock; /** * Created by kazaff on 2014/12/2. */ @Service @Scope("prototype") public class webUploader { private final static Logger log = LoggerFactory.getLogger(webUploader.class); /** * 错误详情 */ private String msg; /** * 秒传验证 * 根据文件的MD5签名判断该文件是否已经存在 * * @param key 文件的md5签名 * @return 若存在则返回该文件的路径,不存在则返回null */ public String md5Check(String key){ //todo 模拟去数据库查找 Map<String, String> data = new HashMap<String, String>(); data.put("b0201e4d41b2eeefc7d3d355a44c6f5a", "kazaff2.jpg"); if(data.containsKey(key)){ return data.get(key); }else{ return null; } } /** * 分片验证 * 验证对应分片文件是否存在,大小是否吻合 * @param file 分片文件的路径 * @param size 分片文件的大小 * @return */ public boolean chunkCheck(String file, Long size){ //检查目标分片是否存在且完整 File target = new File(file); if(target.isFile() && size == target.length()){ return true; }else{ return false; } } /** * 分片合并操作 * 要点: * > 合并: NIO * > 并发锁: 避免多线程同时触发合并操作 * > 清理: 合并清理不再需要的分片文件、文件夹、tmp文件 * @param folder 分片文件所在的文件夹名称 * @param ext 合并后的文件后缀名 * @param chunks 分片总数 * @param md5 文件签名 * @param path 合并后的文件所存储的位置 * @return */ public String chunksMerge(String folder, String ext, int chunks, String md5, String path){ //合并后的目标文件 String target; //检查是否满足合并条件:分片数量是否足够 if(chunks == this.getChunksNum(path + "/" + folder)){ //同步指定合并的对象 Lock lock = fileLock.getLock(folder); lock.lock(); try{ //检查是否满足合并条件:分片数量是否足够 //File[] files = this.getChunks(path + "/" +folder); List<File> files = new ArrayList<File>(Arrays.asList(this.getChunks(path + "/" +folder))); if(chunks == files.size()){ //按照名称排序文件,这里分片都是按照数字命名的 Collections.sort(files, new Comparator<File>() { @Override public int compare(File o1, File o2) { if(Integer.valueOf(o1.getName()) < Integer.valueOf(o2.getName())){ return -1; } return 1; } }); //创建合并后的文件 File outputFile = new File(path + "/" + this.randomFileName(ext)); if(outputFile.exists()){ log.error("文件[" + folder + "]随机命名冲突"); this.setErrorMsg("文件随机命名冲突"); return null; } outputFile.createNewFile(); FileChannel outChannel = new FileOutputStream(outputFile).getChannel(); //合并 FileChannel inChannel; for(File file : files){ inChannel = new FileInputStream(file).getChannel(); inChannel.transferTo(0, inChannel.size(), outChannel); inChannel.close(); //删除分片 if(!file.delete()){ log.error("分片[" + folder + "=>" + file.getName() + "]删除失败"); } } outChannel.close(); files = null; //将MD5签名和合并后的文件path存入持久层 if(this.saveMd52FileMap(md5, outputFile.getName())){ log.error("文件[" + md5 + "=>" + outputFile.getName() + "]保存关系到持久成失败,但并不影响文件上传,只会导致日后该文件可能被重复上传而已"); } //清理:文件夹,tmp文件 this.cleanSpace(folder, path); return outputFile.getName(); } }catch(Exception ex){ log.error("数据分片合并失败", ex); this.setErrorMsg("数据分片合并失败"); return null; }finally { //解锁 lock.unlock(); //清理锁对象 fileLock.removeLock(folder); } } //去持久层查找对应md5签名,直接返回对应path target = this.md5Check(md5); if(target == null){ log.error("文件[签名:" + md5 + "]数据不完整,可能该文件正在合并中"); this.setErrorMsg("数据不完整,可能该文件正在合并中"); return null; } return target; } /** * 将MD5签名和目标文件path的映射关系存入持久层 * @param key md5签名 * @param file 文件路径 * @return */ public boolean saveMd52FileMap(String key, String file){ //todo return true; } /** * 为上传的文件创建对应的保存位置 * 若上传的是分片,则会创建对应的文件夹结构和tmp文件 * @param info 上传文件的相关信息 * @param path 文件保存根路径 * @return */ public File getReadySpace(FileInfo info, String path){ //创建上传文件所需的文件夹 if(!this.createFileFolder(path, false)){ return null; } String newFileName; //上传文件的新名称 //如果是分片上传,则需要为分片创建文件夹 if (info.getChunks() > 0) { newFileName = String.valueOf(info.getChunk()); String fileFolder = this.md5(info.getUserId() + info.getName() + info.getType() + info.getLastModifiedDate() + info.getSize()); if(fileFolder == null){ return null; } path += "/" + fileFolder; //文件上传路径更新为指定文件信息签名后的临时文件夹,用于后期合并 if(!this.createFileFolder(path, true)){ return null; } } else { //生成随机文件名 newFileName = this.randomFileName(info.getName()); } return new File(path, newFileName); } /** * 清理分片上传的相关数据 * 文件夹,tmp文件 * @param folder 文件夹名称 * @param path 上传文件根路径 * @return */ private boolean cleanSpace(String folder, String path){ //删除分片文件夹 File garbage = new File(path + "/" + folder); if(!garbage.delete()){ return false; } //删除tmp文件 garbage = new File(path + "/" + folder + ".tmp"); if(!garbage.delete()){ return false; } return true; } /** * 获取指定文件的所有分片 * @param folder 文件夹路径 * @return */ private File[] getChunks(String folder){ File targetFolder = new File(folder); return targetFolder.listFiles(new FileFilter() { @Override public boolean accept(File file) { if (file.isDirectory()) { return false; } return true; } }); } /** * 获取指定文件的分片数量 * @param folder 文件夹路径 * @return */ private int getChunksNum(String folder){ File[] filesList = this.getChunks(folder); return filesList.length; } /** * 创建存放上传的文件的文件夹 * @param file 文件夹路径 * @return */ private boolean createFileFolder(String file, boolean hasTmp){ //创建存放分片文件的临时文件夹 File tmpFile = new File(file); if(!tmpFile.exists()){ try { tmpFile.mkdir(); }catch(SecurityException ex){ log.error("无法创建文件夹", ex); this.setErrorMsg("无法创建文件夹"); return false; } } if(hasTmp){ //创建一个对应的文件,用来记录上传分片文件的修改时间,用于清理长期未完成的垃圾分片 tmpFile = new File(file + ".tmp"); if(tmpFile.exists()){ tmpFile.setLastModified(System.currentTimeMillis()); }else{ try{ tmpFile.createNewFile(); }catch(IOException ex){ log.error("无法创建tmp文件", ex); this.setErrorMsg("无法创建tmp文件"); return false; } } } return true; } /** * 为上传的文件生成随机名称 * @param originalName 文件的原始名称,主要用来获取文件的后缀名 * @return */ private String randomFileName(String originalName){ String ext[] = originalName.split("\\."); return UUID.randomUUID().toString() + "." + ext[ext.length-1]; } /** * MD5签名 * @param content 要签名的内容 * @return */ private String md5(String content){ StringBuffer sb = new StringBuffer(); try{ MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(content.getBytes("UTF-8")); byte[] tmpFolder = md5.digest(); for(int i = 0; i < tmpFolder.length; i++){ sb.append(Integer.toString((tmpFolder[i] & 0xff) + 0x100, 16).substring(1)); } return sb.toString(); }catch(NoSuchAlgorithmException ex){ log.error("无法生成文件的MD5签名", ex); this.setErrorMsg("无法生成文件的MD5签名"); return null; }catch(UnsupportedEncodingException ex){ log.error("无法生成文件的MD5签名", ex); this.setErrorMsg("无法生成文件的MD5签名"); return null; } } /** * 记录异常错误信息 * @param msg 错误详细 */ private void setErrorMsg(String msg){ this.msg = msg; } /** * 获取错误详细 * @return */ public String getErrorMsg(){ return this.msg; } }(4)、util层
package com.util; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Component public class fileLock { private static Map<String, Lock> LOCKS = new HashMap<String, Lock>(); public static synchronized Lock getLock(String key){ if(LOCKS.containsKey(key)){ return LOCKS.get(key); }else{ Lock one = new ReentrantLock(); LOCKS.put(key, one); return one; } } public static synchronized void removeLock(String key){ LOCKS.remove(key); } }