文件上传插件WebUploader的使用
插件描述:
WebUploader是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件。在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览器,沿用原来的FLASH运行时,兼容IE6+,iOS 6+, android 4+。两套运行时,同样的调用方式,可供用户任意选用。 采用大文件分片并发上传,极大的提高了文件上传效率。
插件特点:
分片、并发
分片与并发结合,将一个大文件分割成多块,并发上传,极大地提高大文件的上传速度。
当网络问题导致传输错误时,只需要重传出错分片,而不是整个文件。另外分片传输能够更加实时的跟踪上传进度。
预览、压缩
支持常用图片格式jpg,jpeg,gif,bmp,png预览与压缩,节省网络数据传输。
解析jpeg中的meta信息,对于各种orientation做了正确的处理,同时压缩后上传保留图片的所有原始meta数据。
多途径添加文件
支持文件多选,类型过滤,拖拽(文件&文件夹),图片粘贴功能。
粘贴功能主要体现在当有图片数据在剪切板中时(截屏工具如QQ(Ctrl + ALT + A), 网页中右击图片点击复制),Ctrl + V便可添加此图片文件。
HTML5 & FLASH
兼容主流浏览器,接口一致,实现了两套运行时支持,用户无需关心内部用了什么内核。
同时Flash部分没有做任何UI相关的工作,方便不关心flash的用户扩展和自定义业务需求。
MD5秒传
当文件体积大、量比较多时,支持上传前做文件md5值验证,一致则可直接跳过。
如果服务端与前端统一修改算法,取段md5,可大大提升验证性能,耗时在20ms左右。
易扩展、可拆分
采用可拆分机制, 将各个功能独立成了小组件,可自由搭配。
采用AMD规范组织代码,清晰明了,方便高级玩家扩展。
资源介绍:
// SWF文件,当使用Flash运行时需要引入。
Uploader.swf
// 完全版本。
webuploader.js
webuploader.min.js
// 只有Flash实现的版本。
webuploader.flashonly.js
webuploader.flashonly.min.js
// 只有Html5实现的版本。
webuploader.html5only.js
webuploader.html5only.min.js
// 去除图片处理的版本,包括HTML5和FLASH.
webuploader.withoutimage.js
webuploader.withoutimage.min.js
使用方法:
接下来以图片上传实例,讲解如何使用WebUploader。
我们首先将css和相关js文件加载。
1 <link rel="stylesheet" type="text/css" href="css/webuploader.css"> 2 <script src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script> 3 <script type="text/javascript" src="js/webuploader.min.js"></script>
然后我们需要准备一个按钮#imgPicker,和一个用来存放添加的文件信息列表的容器#fileList,在body中加入如下代码:
1 <div id="uploadimg"> 2 <div id="fileList" class="uploader-list"></div> 3 <div id="imgPicker">选择图片</div> 4 </div>
首先创建Web Uploader实例:
1 var uploader = WebUploader.create({ 2 auto: true, // 选完文件后,是否自动上传 3 swf: 'js/Uploader.swf', // swf文件路径 4 server: '/uploadServlet?methodName=fileupload', // 文件接收服务端 5 pick: '#imgPicker', // 选择文件的按钮。可选 6 // 只允许选择图片文件。 7 accept: { 8 title: 'Images', 9 extensions: 'gif,jpg,jpeg,bmp,png', 10 mimeTypes: 'image/*' 11 } 12 });
接着监听fileQueued事件,即当有文件添加进来的时候,通过uploader.makeThumb来创建图片预览图。
1 uploader.on( 'fileQueued', function( file ) { 2 var $list = $("#fileList"), //获取文件列表 3 $li = $( 4 '<div id="' + file.id + '" class="file-item thumbnail">' + 5 '<img>' + 6 '<div class="info">' + file.name + '</div>' + 7 '</div>' 8 ), 9 $img = $li.find('img'); 10 $list.append( $li ); // $list为容器jQuery实例 11 // 创建缩略图 12 uploader.makeThumb( file, function( error, src ) { 13 if ( error ) { 14 $img.replaceWith('<span>不能预览</span>'); 15 return; 16 } 17 $img.attr( 'src', src );//设置预览图 18 }, 100, 100 ); //100x100为缩略图尺寸 19 });
最后是上传状态提示了:
文件上传过程中对应uploadProgress事件。
1 // 文件上传过程中创建进度条实时显示。 2 uploader.on( 'uploadProgress', function( file, percentage ) { 3 var $li = $( '#'+file.id ), 4 $percent = $li.find('.progress span'); 5 //避免重复创建 6 if ( !$percent.length ) { 7 $percent = $('<p class="progress"><span></span></p>').appendTo( $li ).find('span'); 8 } 9 $percent.css( 'width', percentage * 100 + '%' ); 10 });
文件上传成功对应uploadSuccess事件。
1 // 文件上传成功,给item添加成功class, 用样式标记上传成功。 2 uploader.on( 'uploadSuccess', function( file, res ) { 3 console.log(res.filePath);//这里可以得到上传后的文件路径 4 $( '#'+file.id ).addClass('upload-state-done'); 5 });
文件上传失败对应uploadError事件。
1 // 文件上传失败,显示上传出错。 2 uploader.on( 'uploadError', function( file ) { 3 var $li = $( '#'+file.id ), 4 $error = $li.find('div.error'); 5 // 避免重复创建 6 if ( !$error.length ) { 7 $error = $('<div class="error"></div>').appendTo( $li ); 8 } 9 $error.text('上传失败'); 10 });
文件上传完成对应uploadComplete事件。
1 // 完成上传,成功或者失败,先删除进度条。 2 uploader.on( 'uploadComplete', function( file ) { 3 $( '#'+file.id ).find('.progress').remove(); 4 });
到这里,我们就实现了一个简单的图片上传实例,点击“选择图片”会弹出文件选择对话框,当选择图片后,即进入上传图片流程,会将图片对应的缩略图现实在列表里。
=====================================================================================
接下来贴上文件分片上传的完整代码:
先引入相关js文件:
<script type="text/javascript" src="http://www.qiyinwang.com.cn/js/newperson/jquery-1.11.3.min.js"></script>
<script type="text/javascript" src="http://www.qiyinwang.com.cn/js/webuploader/webuploader.nolog.js"></script>
页面引入相关控件:
<div id="uploadBtn" class="btn btn-upload">上传文件</div>
<div id="list" class="upload-box clearfix"></div>
前端js代码:
1 //验证数组方法 2 Array.prototype.contains = function (obj) { 3 var i = this.length; 4 while (i--) { 5 if (this[i] === obj) { 6 return true; 7 } 8 } 9 return false; 10 } 11 12 var arr_md5 = [];//加载页面时,将已上传成功的分片数组化 13 var chunks = 0;//分片总数量,用来上传成功以后校验分片文件总数 14 var repart_chunks = 0; 15 var upload_success = false; 16 var times = 0; 17 var interval = 0; 18 19 //注册组件 20 WebUploader.Uploader.register({ 21 'before-send': 'preupload' 22 }, { 23 preupload: function( block ) { 24 var me = this, 25 owner = this.owner, 26 deferred = WebUploader.Deferred(); 27 chunks = block.chunks; 28 owner.md5File(block.blob) 29 //及时显示进度 30 .progress(function(percentage) { 31 console.log('Percentage:', percentage); 32 }) 33 //如果读取出错了,则通过reject告诉webuploader文件上传出错。 34 .fail(function() { 35 deferred.reject(); 36 console.log("==1=="); 37 }) 38 //md5值计算完成 39 .then(function( md5 ) { 40 if(arr_md5.contains(md5)){//数组中包含+(block.chunk+1) 41 deferred.reject(); 42 console.log("分片已上传"+block.file.id); 43 $("#speed_"+block.file.id).text("正在断点续传中..."); 44 if(block.end == block.total){ 45 $("#speed_"+block.file.id).text("上传完毕"); 46 $("#pro_"+block.file.id).css( 'width', '100%' ); 47 }else{ 48 $("#pro_"+block.file.id).css( 'width',(block.end/block.total) * 100 + '%' ); 49 } 50 }else{ 51 deferred.resolve(); 52 console.log("开始上传分片:"+md5); 53 } 54 }); 55 return deferred.promise(); 56 } 57 }); 58 59 //初始化WebUploader 60 var uploader = WebUploader.create({ 61 //swf文件路径 62 //swf: 'http://www.ssss.com.cn/js/webuploader/Uploader.swf', 63 //文件接收服务端。 64 server: '/upload/UploadPhotoSlt?methodName=fileupload&tokenid='+$("#tokenid").val(), 65 //选择文件的按钮,可选。内部根据当前运行是创建,可能是input元素,也可能是flash. 66 pick: '#uploadBtn', 67 //不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传! 68 resize: false, 69 auto:true, 70 //是否分片 71 chunked :true, 72 //分片大小 73 chunkSize :1024*1024*2, 74 chunkRetry :3, 75 threads :3,//最大并发 76 fileNumLimit :1, 77 fileSizeLimit :1024*1024*1024*1024, 78 fileSingleSizeLimit: 1024*1024*1024, 79 duplicate :true, 80 accept: { 81 title: 'file', 82 extensions: 'jpg,png,ai,zip,rar,psd,pdf,cdr,psd,tif', 83 mimeTypes: '*/*' 84 } 85 }); 86 87 //当文件被加入队列之前触发,此事件的handler返回值为false,则此文件不会被添加进入队列。 88 uploader.on('beforeFileQueued',function(file){ 89 if(",jpg,png,ai,zip,rar,psd,pdf,cdr,psd,tif,".indexOf(","+file.ext+",")<0){ 90 alert("不支持的文件格式"); 91 } 92 }); 93 94 //当有文件添加进来的时候 95 uploader.on( 'fileQueued', function( file ) { 96 times = 1; 97 var src = "http://www.wodexiangce.cn/images/type/JPG.jpg"; 98 if(file.ext == 'jpg'){ 99 src = "http://www.wodexiangce.cn/images/type/JPG.jpg"; 100 }else if(file.ext == 'png'){ 101 src = "http://www.wodexiangce.cn/images/type/PNG.jpg"; 102 }else if(file.ext == 'ai'){ 103 src = "http://www.wodexiangce.cn/images/type/AI.jpg"; 104 }else if(file.ext == 'zip'){ 105 src = "http://www.wodexiangce.cn/images/type/ZIP.jpg"; 106 }else if(file.ext == 'rar'){ 107 src = "http://www.wodexiangce.cn/images/type/RAR.jpg"; 108 }else if(file.ext == 'psd'){ 109 src = "http://www.wodexiangce.cn/images/type/PSD.jpg"; 110 }else if(file.ext == 'pdf'){ 111 src = "http://www.wodexiangce.cn/images/type/PDF.jpg"; 112 }else if(file.ext == 'cdr'){ 113 src = "http://www.wodexiangce.cn/images/type/CDR.jpg"; 114 }else if(file.ext == 'tif'){ 115 src = "http://www.wodexiangce.cn/images/type/TIF.jpg"; 116 } 117 upload_success = false; 118 $("#list").html( 119 '<div class="clearfix"><img src='+src+' width="50px" class="icon-file" ></img>'+ 120 '<div class="fl" style="margin-left: 70px;">'+ 121 '<p class="speed font-12" id="speed_'+file.id+'">校验文件...</p>'+ 122 '<div class="progress">'+ 123 '<span id="pro_'+file.id+'" class="progressing"></span>'+ 124 '</div>'+ 125 '<span class="file-size">'+(file.size/1024/1024).toFixed(2)+'MB</span>'+ 126 '<a href="javascript:void(0);" id="stopOretry_'+file.id+'" onclick="stop(\''+file.id+'\');" class="a-pause">暂停</a>'+ 127 '</div></div><span class="file-name">'+file.name+'</span><br/>' ); 128 //文件开始上传后开始计时,计算实时上传速度 129 interval = setInterval(function(){times++;},1000); 130 131 }); 132 133 //上传过程中触发,携带上传进度。文件上传过程中创建进度条实时显示。 134 uploader.on( 'uploadProgress', function( file, percentage ) { 135 var status_pre = file.size*percentage-arr_md5.length*2*1024*1024; 136 if(status_pre<=0){ 137 return; 138 } 139 $("#pro_"+file.id).css( 'width', percentage * 100 + '%' ); 140 var speed = ((status_pre/1024)/times).toFixed(0); 141 $("#speed_"+file.id).text(speed +"KB/S"); 142 if(percentage == 1){ 143 $("#speed_"+file.id).text("上传完毕"); 144 } 145 }); 146 147 //文件上传成功时触发 148 uploader.on( 'uploadSuccess', function( file,response) { 149 console.log("成功上传"+file.name+" res="+response); 150 $.ajax({ 151 type:'get', 152 url:'/upload/UploadPhotoSlt', 153 dataType: 'json', 154 data: { 155 methodName:'composeFile', 156 name:file.name, 157 chunks:chunks, 158 tokenid:$("#tokenid").val() 159 }, 160 async:false, 161 success: function(data) { 162 console.log("==compose=="+data.status); 163 if(data.status == "success"){ 164 upload_success = true; 165 $("#url").val(data.url); 166 console.log(data.url); 167 }else{ 168 upload_success = false; 169 alert(data.errstr); 170 } 171 } 172 }); 173 }); 174 175 //文件上传异常失败触发 176 uploader.on( 'uploadError', function( file,reason ) { 177 console.log("失败"+reason ); 178 }); 179 180 //某个文件开始上传前触发,一个文件只会触发一次。 181 uploader.on( 'uploadStart', function(file) { 182 console.info("file="+file.name); 183 //分片文件上传之前 184 $.ajax({ 185 type:'get', 186 url:'/upload/UploadPhotoSlt', 187 dataType: 'json', 188 data: { 189 methodName:'md5Validation', 190 name:file.name, 191 tokenid:$("#tokenid").val() 192 }, 193 async:false, 194 success: function(data) { 195 if(data.md5_arr != ""){ 196 arr_md5 = data.md5_arr.split(",") 197 }else{ 198 arr_md5 = new Array(); 199 } 200 } 201 }); 202 }); 203 204 //当所有文件上传结束时触发。 205 uploader.on( 'uploadFinished', function() { 206 207 }); 208 209 function stop(id){ 210 uploader.stop(true); 211 $("#stopOretry_"+id).attr("onclick","retry('"+id+"')"); 212 $("#stopOretry_"+id).text("恢复"); 213 clearInterval(interval); 214 } 215 function retry(id){ 216 uploader.retry(); 217 $("#stopOretry_"+id).attr("onclick","stop('"+id+"')"); 218 $("#stopOretry_"+id).text("暂停"); 219 interval = setInterval(function(){times++;},1000); 220 }
后台Java代码:使用到commons-fileupload-1.1.1.jar
servlet:HttpServletBasic
1 package com.wodexiangce.web.servlet; 2 3 import java.lang.reflect.InvocationTargetException; 4 import java.lang.reflect.Method; 5 6 import javax.servlet.ServletContext; 7 import javax.servlet.ServletException; 8 import javax.servlet.http.HttpServlet; 9 import javax.servlet.http.HttpServletRequest; 10 import javax.servlet.http.HttpServletResponse; 11 12 import org.springframework.web.context.WebApplicationContext; 13 import org.springframework.web.context.support.WebApplicationContextUtils; 14 15 public class HttpServletBasic extends HttpServlet{ 16 private static final long serialVersionUID = 1L; 17 18 protected WebApplicationContext wac = null; 19 20 public void init() throws ServletException { 21 super.init(); 22 ServletContext sc = this.getServletContext(); 23 wac = WebApplicationContextUtils.getRequiredWebApplicationContext(sc); 24 } 25 26 /** 27 * 方法转发器,通过方法名调用相应的方法 。 28 * 可以看出,不能分出方法同名而参数不同的方法 。 29 */ 30 protected void service(HttpServletRequest request,HttpServletResponse response){ 31 32 String methodName = request.getParameter("methodName") ; 33 34 //当前类所有的方法名称 35 Method[] methods = this.getClass().getDeclaredMethods() ; 36 for(Method m:methods){ 37 if(m.getName().equals(methodName)){ 38 try { 39 m.invoke(this, new Object[]{request,response}) ; 40 } catch (IllegalArgumentException e) { 41 e.printStackTrace(); 42 } catch (IllegalAccessException e) { 43 e.printStackTrace(); 44 } catch (InvocationTargetException e) { 45 e.printStackTrace(); 46 } 47 break ; 48 } 49 } 50 } 51 52 public void destroy() { 53 wac = null; 54 } 55 56 }
servlet:UploadPhotoSlt
1 package com.wodexiangce.web.servlet; 2 3 import java.io.File; 4 import java.io.IOException; 5 import java.util.Date; 6 import java.util.HashMap; 7 import java.util.List; 8 import java.util.Map; 9 10 import javax.servlet.http.HttpServletRequest; 11 import javax.servlet.http.HttpServletResponse; 12 13 import net.sf.json.JSONObject; 14 15 import org.apache.commons.fileupload.FileItem; 16 import org.apache.commons.fileupload.FileItemFactory; 17 import org.apache.commons.fileupload.disk.DiskFileItemFactory; 18 import org.apache.commons.fileupload.servlet.ServletFileUpload; 19 import org.apache.commons.lang.math.NumberUtils; 20 21 import com.wodexiangce.persistence.model.WdxcQywFiles; 22 import com.wodexiangce.services.QywFileService; 23 import com.wodexiangce.util.FileUtils; 24 25 26 public class UploadPhotoSlt extends HttpServletBasic{ 27 28 private static final long serialVersionUID = 1L; 29 30 /** 31 * 文件上传 32 * @param request 33 * @param response 34 * @throws IOException 35 */ 36 @SuppressWarnings({ "unchecked", "deprecation" }) 37 public void fileupload(HttpServletRequest request, HttpServletResponse response)throws IOException{ 38 try { 39 System.out.println("=================fileupload==================="); 40 response.addHeader("Access-Control-Allow-Origin", "*"); 41 String useridStr = request.getParameter("tokenid"); 42 if(useridStr==null||"".equals(useridStr)||useridStr.length()<3){ 43 response.getWriter().write(JSONObject.fromObject("{\"status\":\"error\",'errstr':'token校验错误'}").toString()); 44 return; 45 } 46 long userid = Long.parseLong(useridStr); 47 boolean isMultipart = ServletFileUpload.isMultipartContent(request); 48 if (isMultipart) { 49 FileItemFactory factory = new DiskFileItemFactory(); 50 ServletFileUpload upload = new ServletFileUpload(factory); 51 52 // 得到所有的表单域,它们目前都被当作FileItem 53 List<FileItem> fileItems = upload.parseRequest(request); 54 55 String id = ""; 56 String fileName = ""; 57 // 如果大于1说明是分片处理 58 int chunks = 1; 59 int chunk = 0; 60 FileItem tempFileItem = null; 61 for (FileItem fileItem : fileItems) { 62 if (fileItem.getFieldName().equals("id")) { 63 id = fileItem.getString(); 64 } else if (fileItem.getFieldName().equals("name")) { 65 fileName = new String(fileItem.getString().getBytes("ISO-8859-1"), "UTF-8"); 66 } else if (fileItem.getFieldName().equals("chunks")) { 67 chunks = NumberUtils.toInt(fileItem.getString()); 68 } else if (fileItem.getFieldName().equals("chunk")) { 69 chunk = NumberUtils.toInt(fileItem.getString()); 70 } else if (fileItem.getFieldName().equals("file")) { 71 tempFileItem = fileItem; 72 } 73 } 74 System.out.println("id="+id+" filename="+fileName+" chunks="+chunks+" chunk="+chunk+" size="+tempFileItem.getSize()); 75 //临时目录用来存放所有分片文件 76 String tempFileDir = getTempFilePath()+ File.separator + userid; 77 File parentFileDir = new File(tempFileDir); 78 if (!parentFileDir.exists()) { 79 parentFileDir.mkdirs(); 80 } 81 //分片处理时,前台会多次调用上传接口,每次都会上传文件的一部分到后台 82 File tempPartFile = new File(parentFileDir, fileName + "_" + chunk+ ".part"); 83 84 String MD5 = FileUtils.copyInputStreamToFile(tempFileItem.getInputStream(),tempPartFile); 85 int count = 0; 86 while(MD5==null&&count<3){ 87 MD5 = FileUtils.copyInputStreamToFile(tempFileItem.getInputStream(),tempPartFile); 88 count++; 89 } 90 if(MD5==null){ 91 throw new Exception("分片文件:"+chunk+"上传失败"); 92 } 93 //分片文件上传成功以后,重命名分片文件,规则:原名之前拼接MD5码 94 tempPartFile.renameTo(new File(parentFileDir, fileName + "_" + chunk+"_"+MD5+ ".part")); 95 System.out.println("MD5="+MD5); 96 response.getWriter().write(JSONObject.fromObject("{\"md5\":\""+MD5+"\"}").toString()); 97 } 98 } catch (Exception e) { 99 e.printStackTrace(); 100 } 101 } 102 103 /** 104 * 重复验证 105 * @param request 106 * @param response 107 * @throws IOException 108 */ 109 public void md5Validation(HttpServletRequest request, HttpServletResponse response)throws IOException{ 110 try { 111 System.out.println("=================md5Validation==================="); 112 response.addHeader("Access-Control-Allow-Origin", "*"); 113 response.setCharacterEncoding("utf-8"); 114 String useridStr = request.getParameter("tokenid"); 115 if(useridStr==null||"".equals(useridStr)||useridStr.length()<3){ 116 response.getWriter().write(JSONObject.fromObject("{\"status\":\"error\",'errstr':'token校验错误'}").toString()); 117 return; 118 } 119 long userid = Long.parseLong(useridStr); 120 String tempFileDir = getTempFilePath()+ File.separator + userid; 121 File parentFileDir = new File(tempFileDir); 122 if (!parentFileDir.exists()) { 123 response.getWriter().write(JSONObject.fromObject("{\"md5_arr\":\"\"}").toString()); 124 return; 125 } 126 String fileName = request.getParameter("name"); 127 fileName = new String(fileName.getBytes("ISO-8859-1"),"UTF-8"); 128 System.out.println("fileName="+fileName); 129 130 StringBuffer sb = new StringBuffer(); 131 for(File file:parentFileDir.listFiles()){ 132 String name = file.getName(); 133 if(name.endsWith(".part") && name.startsWith(fileName)){ 134 //此文件有上传记录,解析MD5 135 name = name.substring(name.lastIndexOf("_")+1,name.lastIndexOf(".part")); 136 if(name.length()==32){ 137 if(sb.length()>0){ 138 sb.append(","); 139 } 140 sb.append(name); 141 } 142 } 143 } 144 response.getWriter().write(JSONObject.fromObject("{\"md5_arr\":\""+sb.toString()+"\"}").toString()); 145 } catch (Exception e) { 146 e.printStackTrace(); 147 } 148 } 149 150 /** 151 * 文件所有分片上传完毕之后,保存文件数据到数据库 152 * @param request 153 * @param response 154 * @throws IOException 155 */ 156 public void composeFile(HttpServletRequest request, HttpServletResponse response)throws IOException{ 157 try { 158 System.out.println("=================composeFile==================="); 159 response.addHeader("Access-Control-Allow-Origin", "*"); 160 response.setCharacterEncoding("utf-8"); 161 String useridStr = request.getParameter("tokenid"); 162 if(useridStr==null||"".equals(useridStr)||useridStr.length()<3){ 163 response.getWriter().write(JSONObject.fromObject("{\"status\":\"error\",'errstr':'token校验错误'}").toString()); 164 return; 165 } 166 long userid = Long.parseLong(useridStr); 167 String tempFileDir = getTempFilePath()+ File.separator + userid; 168 File parentFileDir = new File(tempFileDir); 169 if (!parentFileDir.exists()) { 170 response.getWriter().write(JSONObject.fromObject("{\"status\":\"error\",'errstr':'目录不存在'}").toString()); 171 return; 172 } 173 String fileName = request.getParameter("name"); 174 fileName = new String(fileName.getBytes("ISO-8859-1"),"UTF-8"); 175 String chunks = request.getParameter("chunks"); 176 System.out.println("fileName="+fileName); 177 Map<String,File> chunkFileMap = new HashMap<String, File>(); 178 179 String md5 = null; 180 for(File file:parentFileDir.listFiles()){ 181 String name = file.getName(); 182 if(name.endsWith(".part") && name.startsWith(fileName)){ 183 md5 = name.substring(name.lastIndexOf("_")+1,name.lastIndexOf(".part")); 184 System.out.println("md5="+md5); 185 if(md5.length()==32){//完整的分片文件 186 String index = name.replace(fileName, "").replace("_"+md5+".part", "").replace("_", ""); 187 chunkFileMap.put(index, file); 188 } 189 } 190 } 191 //分片总数和分片文件数一致,则证明分片文件已完整上传,可以持久化数据 192 if(chunkFileMap.size() == Integer.parseInt(chunks.trim())){ 193 System.out.println("===========开始合并文件分片=========="); 194 WdxcQywFiles QywFile = new WdxcQywFiles(); 195 QywFile.setFilename(fileName); 196 QywFile.setCreatetime(new Date()); 197 QywFile.setFilepath("/site/photos/file/"+userid+"/"+fileName); 198 QywFile.setFiledownurl("http://www.sssss.cn/file/"+userid+"/"+fileName); 199 200 201 //AI、PDF、EPS、CDR、PSD、JPG、TIFF 202 if(fileName.toLowerCase().endsWith(".tif")){ 203 QywFile.setFilepreview("http://www.wodexiangce.cn/images/type/TIF.jpg"); 204 }else if(fileName.toLowerCase().endsWith(".jpg")){ 205 QywFile.setFilepreview("http://www.wodexiangce.cn/images/type/JPG.jpg"); 206 }else if(fileName.toLowerCase().endsWith(".psd")){ 207 QywFile.setFilepreview("http://www.wodexiangce.cn/images/type/PSD.jpg"); 208 }else if(fileName.toLowerCase().endsWith(".ai")){ 209 QywFile.setFilepreview("http://www.wodexiangce.cn/images/type/AI.jpg"); 210 }else if(fileName.toLowerCase().endsWith(".cdr")){ 211 QywFile.setFilepreview("http://www.wodexiangce.cn/images/type/CDR.jpg"); 212 }else if(fileName.toLowerCase().endsWith(".zip")){ 213 QywFile.setFilepreview("http://www.wodexiangce.cn/images/type/ZIP.jpg"); 214 }else if(fileName.toLowerCase().endsWith(".pdf")){ 215 QywFile.setFilepreview("http://www.wodexiangce.cn/images/type/PDF.jpg"); 216 }else if(fileName.toLowerCase().endsWith(".png")){ 217 QywFile.setFilepreview("http://www.wodexiangce.cn/images/type/PNG.jpg"); 218 }else if(fileName.toLowerCase().endsWith(".rar")){ 219 QywFile.setFilepreview("http://www.wodexiangce.cn/images/type/RAR.jpg"); 220 } 221 QywFile.setUserid(userid); 222 FileUtils.fileCompose(QywFile, chunkFileMap);//合并文件 223 File file = new File(QywFile.getFilepath()); 224 if(!file.exists()){ 225 System.out.println("文件合并失败:"+QywFile.getFilepath()); 226 response.getWriter().write(JSONObject.fromObject("{\"status\":\"error\",'errstr':'文件合并失败'}").toString()); 227 return; 228 } 229 230 //把文件路径保存到数据库 231 QywFileService fileService = (QywFileService)wac.getBean("qywFileService"); 232 Long fileid = (Long)fileService.saveQywFile(QywFile); 233 System.out.println("文件保存成功:"+fileid); 234 235 response.getWriter().write(JSONObject.fromObject("{\"status\":\"success\",'url':'"+QywFile.getFiledownurl()+"'}").toString()); 236 }else{ 237 System.out.println("分片数量不正确,实际分片数量:"+chunkFileMap.size()+" 总分片数量:"+chunks); 238 response.getWriter().write(JSONObject.fromObject("{\"status\":\"error\",'errstr':'分片数量不正确'}").toString()); 239 } 240 } catch (Exception e) { 241 e.printStackTrace(); 242 System.err.println("文件合并失败:"+e.getMessage()); 243 response.getWriter().write(JSONObject.fromObject("{\"status\":\"error\",'errstr':'文件合并失败'}").toString()); 244 } 245 } 246 247 /** 248 * 指定临时目录 249 * @return 250 */ 251 private String getTempFilePath(){ 252 return "/site/xxxxxx/temp"; 253 } 254 255 }
补充:涉及到的实体类和工具类
1 package com.wodexiangce.persistence.model; 2 3 import java.io.Serializable; 4 import java.math.BigDecimal; 5 import java.util.Date; 6 7 8 public class WdxcQywFiles implements Serializable{ 9 10 private static final long serialVersionUID = 1L; 11 12 private long id; 13 14 private long userid; 15 16 private String filename; 17 18 private String filepreview; 19 20 private String filepath; 21 22 private int deleted; 23 24 private Date deletedtime; 25 26 private Date createtime; 27 28 private String filedownurl; 29 30 private BigDecimal filesize; 31 32 public long getId() { 33 return id; 34 } 35 public void setId(long id) { 36 this.id = id; 37 } 38 public long getUserid() { 39 return userid; 40 } 41 public void setUserid(long userid) { 42 this.userid = userid; 43 } 44 public String getFilename() { 45 return filename; 46 } 47 public void setFilename(String filename) { 48 this.filename = filename; 49 } 50 public String getFilepreview() { 51 return filepreview; 52 } 53 public void setFilepreview(String filepreview) { 54 this.filepreview = filepreview; 55 } 56 public String getFilepath() { 57 return filepath; 58 } 59 public void setFilepath(String filepath) { 60 this.filepath = filepath; 61 } 62 /** 63 * 删除状态 0:正常 1:已删除 64 * @return 65 */ 66 public int getDeleted() { 67 return deleted; 68 } 69 public void setDeleted(int deleted) { 70 this.deleted = deleted; 71 } 72 public Date getDeletedtime() { 73 return deletedtime; 74 } 75 public void setDeletedtime(Date deletedtime) { 76 this.deletedtime = deletedtime; 77 } 78 public Date getCreatetime() { 79 return createtime; 80 } 81 public void setCreatetime(Date createtime) { 82 this.createtime = createtime; 83 } 84 /** 85 * 文件下载URL前缀(需拼写分片名称) 86 * @return 87 */ 88 public String getFiledownurl() { 89 return filedownurl; 90 } 91 public void setFiledownurl(String filedownurl) { 92 this.filedownurl = filedownurl; 93 } 94 /** 95 * 文件大小 96 * @return 97 */ 98 public BigDecimal getFilesize() { 99 return filesize; 100 } 101 public void setFilesize(BigDecimal bigDecimal) { 102 this.filesize = bigDecimal; 103 } 104 }
1 package com.wodexiangce.util; 2 3 import java.io.File; 4 import java.io.FileInputStream; 5 import java.io.FileOutputStream; 6 import java.io.InputStream; 7 import java.lang.reflect.Method; 8 import java.math.BigDecimal; 9 import java.nio.MappedByteBuffer; 10 import java.nio.channels.FileChannel; 11 import java.security.AccessController; 12 import java.security.MessageDigest; 13 import java.security.PrivilegedAction; 14 import java.util.Map; 15 16 import com.wodexiangce.persistence.model.WdxcQywFiles; 17 18 /** 19 * 文件处理工具类 20 */ 21 public class FileUtils { 22 /** 23 * 保存文件流到文件,同时返回文件MD5 24 * @param inputStream 25 * @param file 26 */ 27 public static String copyInputStreamToFile(InputStream inputStream,File file){ 28 MessageDigest md = null; 29 try { 30 if(inputStream == null || inputStream == null) { 31 return null; 32 } 33 md = MessageDigest.getInstance("MD5"); 34 byte[] b = new byte[102400];//set b 100Kb byte. 35 int n = inputStream.read(b); 36 int totalBytes = n; 37 FileOutputStream fos = new FileOutputStream(file); 38 while (n > -1) { 39 fos.write(b, 0, n); 40 fos.flush(); 41 n = inputStream.read(b); 42 totalBytes += n; 43 } 44 fos.close(); 45 inputStream.close(); 46 System.out.println("文件总大小:"+totalBytes); 47 //生成文件MD5值 48 FileInputStream in = new FileInputStream(file); 49 //文件内存映射,提高读写超大文件可能和速度,但会造成文件锁定不可操作。 50 MappedByteBuffer byteBuffer = in.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); 51 md.update(byteBuffer); 52 clean(byteBuffer); 53 54 byte[] encrypt = md.digest(); 55 StringBuffer sb = new StringBuffer(); 56 for (int i = 0; i < encrypt.length; i++) { 57 String hex = Integer.toHexString(0xff & encrypt[i]); 58 if (hex.length() == 1) 59 sb.append('0'); 60 sb.append(hex); 61 } 62 in.close(); 63 return sb.toString(); 64 } catch (Exception e) { 65 e.printStackTrace(); 66 return null; 67 } 68 } 69 70 /** 71 * 文件合并 72 * @param QywFile 73 * @param chunkFileMap 74 */ 75 public static void fileCompose(WdxcQywFiles QywFile,Map<String,File> chunkFileMap){ 76 String path = QywFile.getFilepath(); 77 File mainFile = new File(path); 78 if(!mainFile.getParentFile().exists()){ 79 mainFile.getParentFile().mkdirs(); 80 } 81 try { 82 FileChannel out = new FileOutputStream(mainFile).getChannel(); 83 FileChannel in = null; 84 long start = System.currentTimeMillis(); 85 for(int i=0;i<chunkFileMap.size();i++){ 86 File file = chunkFileMap.get(String.valueOf(i)); 87 System.out.println("file="+file.getName()); 88 in = new FileInputStream(file).getChannel(); 89 MappedByteBuffer buf = in.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); 90 out.write(buf); 91 in.close(); 92 FileUtils.clean(buf); 93 } 94 System.out.println("文件大小:"+mainFile.length()/1024/1024+" M"); 95 QywFile.setFilesize(new BigDecimal(mainFile.length())); 96 long end = System.currentTimeMillis(); 97 System.out.println("常规方法合并耗时:"+(end-start)/1000+" 秒"); 98 }catch (Exception e) { 99 e.printStackTrace(); 100 } 101 } 102 103 104 @SuppressWarnings("unchecked") 105 public static void clean(final Object buffer) throws Exception { 106 AccessController.doPrivileged(new PrivilegedAction() { 107 public Object run() { 108 try { 109 Method getCleanerMethod = buffer.getClass().getMethod("cleaner",new Class[0]); 110 getCleanerMethod.setAccessible(true); 111 sun.misc.Cleaner cleaner =(sun.misc.Cleaner)getCleanerMethod.invoke(buffer,new Object[0]); 112 cleaner.clean(); 113 } catch(Exception e) { 114 e.printStackTrace(); 115 } 116 return null;} 117 }); 118 } 119 }