webUploader大檔案斷點續傳學習心得
阿新 • • 發佈:2019-02-01
一、準備材料:Uploader.swf、webuploader.css、webuploader.js,其中Uploader.swf只在初始化webUploader時用到,其餘兩個檔案在頁面引用即可。下載地址:https://github.com/fex-team/webuploader/releases
二、Jsp程式碼:
三、js程式碼
注:webUploader斷點上傳多個大檔案時是按佇列順序上傳的,即佇列中的檔案一個一個上傳,前一個上傳完成才會開始上傳下一個,不能實現同時上傳。
二、Jsp程式碼:
<!-- 斷點續傳 start--> <!-- 隱藏域 實時儲存上傳進度 --> <input id="jindutiao" type="hidden"/> <div id="uploader" class="wu-example"> <label class="text-right" style="font-weight:100;float:left;margin-left:15px;width:144px;margin-right:15px;">大檔案:</label> <div class="btns"> <div id="picker" class="webuploader-container"> <div class="webuploader-pick">選擇檔案</div> <div id="rt_rt_1bchdejhrarjdvd11h41eoh1nt1" style="position: absolute; top: 0px; left: 0px; width: 88px; height: 35px; overflow: hidden; bottom: auto; right: auto;"> <input id="file_bp" name="file" class="webuploader-element-invisible" type="file" /> <label style="opacity: 0; width: 100%; height: 100%; display: block; cursor: pointer; background: rgb(255, 255, 255) none repeat scroll 0% 0%;"></label> </div> </div> <!-- 檔案列表:選擇檔案後在該div顯示 --> <div id="thelist" class="uploader-list list-group-item clearfix ng-hide" style="margin-left:160px;"></div> <label class="text-right" style="font-weight:100;float:left;margin-left:15px;width:144px;margin-right:15px;"></label> <button class="btn m-b-xs btn-sm btn-info btn-addon" id="startOrStopBtn" style="padding:7px 50px;margin-top:20px;">開始上傳</button> </div> </div> <!-- 斷點續傳 end-->
三、js程式碼
<script type="text/javascript"> jQuery(function() { /*******************初始化引數*********************************/ var $list = $('#thelist'),//檔案列表 $btn = $('#startOrStopBtn'),//開始上傳按鈕 state = 'pending',//初始按鈕狀態 uploader; //uploader物件 var fileMd5; //檔案唯一標識 /******************下面的引數是自定義的*************************/ var fileName;//檔名稱 var oldJindu;//如果該檔案之前上傳過 已經上傳的進度是多少 var count=0;//當前正在上傳的檔案在陣列中的下標,一次上傳多個檔案時使用 var filesArr=new Array();//檔案陣列:每當有檔案被新增進佇列的時候 就push到陣列中 var map={};//key儲存檔案id,value儲存該檔案上傳過的進度 /***************************************************** 監聽分塊上傳過程中的三個時間點 start ***********************************************************/ WebUploader.Uploader.register({ "before-send-file":"beforeSendFile",//整個檔案上傳前 "before-send":"beforeSend", //每個分片上傳前 "after-send-file":"afterSendFile", //分片上傳完畢 }, { //時間點1:所有分塊進行上傳之前呼叫此函式 beforeSendFile:function(file){ var deferred = WebUploader.Deferred(); //1、計算檔案的唯一標記fileMd5,用於斷點續傳 如果.md5File(file)方法裡只寫一個file引數則計算MD5值會很慢 所以加了後面的引數:10*1024*1024 (new WebUploader.Uploader()).md5File(file,0,10*1024*1024).progress(function(percentage){ $('#'+file.id ).find('p.state').text('正在讀取檔案資訊...'); }) .then(function(val){ $('#'+file.id ).find("p.state").text("成功獲取檔案資訊..."); fileMd5=val; //獲取檔案資訊後進入下一步 deferred.resolve(); }); fileName=file.name; //為自定義引數檔名賦值 return deferred.promise(); }, //時間點2:如果有分塊上傳,則每個分塊上傳之前呼叫此函式 beforeSend:function(block){ var deferred = WebUploader.Deferred(); $.ajax({ type:"POST", url:"${ctx}/testController/mergeOrCheckChunks.do?param=checkChunk", //ajax驗證每一個分片 data:{ fileName : fileName, jindutiao:$("#jindutiao").val(), fileMd5:fileMd5, //檔案唯一標記 chunk:block.chunk, //當前分塊下標 chunkSize:block.end-block.start//當前分塊大小 }, cache: false, async: false, // 與js同步 timeout: 1000, //todo 超時的話,只能認為該分片未上傳過 dataType:"json", success:function(response){ if(response.ifExist){ //分塊存在,跳過 deferred.reject(); }else{ //分塊不存在或不完整,重新發送該分塊內容 deferred.resolve(); } } }); this.owner.options.formData.fileMd5 = fileMd5; deferred.resolve(); return deferred.promise(); }, //時間點3:所有分塊上傳成功後呼叫此函式 afterSendFile:function(){ //如果分塊上傳成功,則通知後臺合併分塊 $.ajax({ type:"POST", url:"${ctx}/testController/mergeOrCheckChunks.do?param=mergeChunks", //ajax將所有片段合併成整體 data:{ fileName : fileName, fileMd5:fileMd5, }, success:function(data){ count++; //每上傳完成一個檔案 count+1 if(count<=filesArr.length-1){ uploader.upload(filesArr[count].id);//上傳檔案列表中的下一個檔案 } //合併成功之後的操作 } }); } }); /***************************************************** 監聽分塊上傳過程中的三個時間點 end **************************************************************/ /************************************************************ 初始化WebUploader start ******************************************************************/ uploader = WebUploader.create({ auto:false,//選擇檔案後是否自動上傳 chunked: true,//開啟分片上傳 chunkSize:10*1024*1024,// 如果要分片,分多大一片?預設大小為5M chunkRetry: 3,//如果某個分片由於網路問題出錯,允許自動重傳多少次 threads: 3,//上傳併發數。允許同時最大上傳程序數[預設值:3] duplicate : false,//是否重複上傳(同時選擇多個一樣的檔案),true可以重複上傳 prepareNextFile: true,//上傳當前分片時預處理下一分片 swf: '${ctx}/resource/webuploader/Uploader.swf',// swf檔案路徑 server: '${ctx}/testController/fileSave.do',// 檔案接收服務端 fileSizeLimit:6*1024*1024*1024,//6G 驗證檔案總大小是否超出限制, 超出則不允許加入佇列 fileSingleSizeLimit:3*1024*1024*1024, //3G 驗證單個檔案大小是否超出限制, 超出則不允許加入佇列 pick: { id: '#picker', //這個id是你要點選上傳檔案按鈕的外層div的id multiple : false //是否可以批量上傳,true可以同時選擇多個檔案 }, resize: false, //不壓縮image, 預設如果是jpeg,檔案上傳前會先壓縮再上傳! accept: { //允許上傳的檔案字尾,不帶點,多個用逗號分割 extensions: "txt,jpg,jpeg,bmp,png,zip,rar,war,pdf,cebx,doc,docx,ppt,pptx,xls,xlsx", mimeTypes: '.txt,.jpg,.jpeg,.bmp,.png,.zip,.rar,.war,.pdf,.cebx,.doc,.docx,.ppt,.pptx,.xls,.xlsx', } }); /************************************************************ 初始化WebUploader end ********************************************************************/ //當有檔案被新增進佇列的時候(點選上傳檔案按鈕,彈出檔案選擇框,選擇完檔案點選確定後觸發的事件) uploader.on('fileQueued', function(file) { //限制單個檔案的大小 超出了提示 if(file.size>3*1024*1024*1024){ alert("單個檔案大小不能超過3G"); return false; } /*************如果一次只能選擇一個檔案,再次選擇替換前一個,就增加如下程式碼*******************************/ //清空檔案佇列 $list.html(""); //清空檔案陣列 filesArr=[]; /*************如果一次只能選擇一個檔案,再次選擇替換前一個,就增加以上程式碼*******************************/ //將選擇的檔案新增進檔案陣列 filesArr.push(file); $.ajax({ type:"POST", url:"${ctx}/testController/selectProgressByFileName.do", //先檢查該檔案是否上傳過,如果上傳過,上傳進度是多少 data:{ fileName : file.name //檔名 }, cache: false, async: false, // 同步 dataType:"json", success:function(data){ //上傳過 if(data>0){ //上傳過的進度的百分比 oldJindu=data/100; //如果上傳過 上傳了多少 var jindutiaoStyle="width:"+data+"%"; $list.append( '<div id="' + file.id + '" class="item">' + '<h4 class="info">' + file.name + '</h4>' + '<p class="state">已上傳'+data+'%</p>' + '<a href="javascript:void(0);" class="btn btn-primary file_btn btnRemoveFile">刪除</a>' + '<div class="progress progress-striped active">' + '<div class="progress-bar" role="progressbar" style="'+jindutiaoStyle+'">' + '</div>' + '</div>'+ '</div>' ); //將上傳過的進度存入map集合 map[file.id]=oldJindu; }else{//沒有上傳過 $list.append( '<div id="' + file.id + '" class="item">' + '<h4 class="info">' + file.name + '</h4>' + '<p class="state">等待上傳...</p>' + '<a href="javascript:void(0);" class="btn btn-primary file_btn btnRemoveFile">刪除</a>' + '</div>' ); } } }); uploader.stop(true); //刪除佇列中的檔案 $(".btnRemoveFile").bind("click", function() { var fileItem = $(this).parent(); uploader.removeFile($(fileItem).attr("id"), true); $(fileItem).fadeOut(function() { $(fileItem).remove(); }); //陣列中的檔案也要刪除 for(var i=0;i<filesArr.length;i++){ if(filesArr[i].id==$(fileItem).attr("id")){ filesArr.splice(i,1);//i是要刪除的元素在陣列中的下標,1代表從下標位置開始連續刪除一個元素 } } }); }); //檔案上傳過程中建立進度條實時顯示 uploader.on('uploadProgress', function(file, percentage) { var $li = $( '#'+file.id ), $percent = $li.find('.progress .progress-bar'); //避免重複建立 if (!$percent.length){ $percent = $('<div class="progress progress-striped active">' + '<div class="progress-bar" role="progressbar" style="width: 0%">' + '</div>' + '</div>').appendTo( $li ).find('.progress-bar'); } //將實時進度存入隱藏域 $("#jindutiao").val(Math.round(percentage * 100)); //根據fielId獲得當前要上傳的檔案的進度 var oldJinduValue = map[file.id]; if(percentage<oldJinduValue && oldJinduValue!=1){ $li.find('p.state').text('上傳中'+Math.round(oldJinduValue * 100) + '%'); $percent.css('width', oldJinduValue * 100 + '%'); }else{ $li.find('p.state').text('上傳中'+Math.round(percentage * 100) + '%'); $percent.css('width', percentage * 100 + '%'); } }); //上傳成功後執行的方法 uploader.on('uploadSuccess', function( file ) { //上傳成功去掉進度條 $('#'+file.id).find('.progress').fadeOut(); //隱藏刪除按鈕 $(".btnRemoveFile").hide(); //隱藏上傳按鈕 $("#startOrStopBtn").hide(); $('#'+file.id).find('p.state').text('檔案已上傳成功,系統後臺正在處理,請稍後...'); }); //上傳出錯後執行的方法 uploader.on('uploadError', function( file ) { errorUpload=true; $btn.text('開始上傳'); uploader.stop(true); $('#'+file.id).find('p.state').text('上傳出錯,請檢查網路連線'); }); //檔案上傳成功失敗都會走這個方法 uploader.on('uploadComplete', function( file ) { }); uploader.on('all', function(type){ if (type === 'startUpload'){ state = 'uploading'; }else if(type === 'stopUpload'){ state = 'paused'; }else if(type === 'uploadFinished'){ state = 'done'; } if (state === 'uploading'){ $btn.text('暫停上傳'); } else { $btn.text('開始上傳'); } }); //上傳按鈕的onclick時間 $btn.on('click', function(){ if (state === 'uploading'){ uploader.stop(true); } else { //當前上傳檔案的檔名 var currentFileName; //當前上傳檔案的檔案id var currentFileId; //count=0 說明沒開始傳 預設從檔案列表的第一個開始傳 if(count==0){ currentFileName=filesArr[0].name; currentFileId=filesArr[0].id; }else{ if(count<=filesArr.length-1){ currentFileName=filesArr[count].name; currentFileId=filesArr[count].id; } } //先查詢該檔案是否上傳過 如果上傳過已經上傳的進度是多少 $.ajax({ type:"POST", url:"${ctx}/testController/selectProgressByFileName.do", data:{ fileName : currentFileName//檔名 }, cache: false, async: false, // 同步 dataType:"json", success:function(data){ //如果上傳過 將進度存入map if(data>0){ map[currentFileId]=data/100; } //執行上傳 uploader.upload(currentFileId); } }); } }); }); </script>
四、Java程式碼
//合併、驗證分片方法 public void mergeOrCheckChunks(HttpServletRequest request, HttpServletResponse response) { String param = request.getParameter("param"); String fileName = request.getParameter("fileName"); //當前登入使用者資訊 SysUser sysUser = (SysUser)request.getSession().getAttribute("user"); String newFilePath = sysUser.getUserId()+"_"+fileName; String savePath = request.getRealPath("/"); //檔案上傳的臨時檔案儲存在專案的temp資料夾下 定時刪除 savePath = new File(savePath) + "/upload/"; if(param.equals("mergeChunks")){ //合併檔案 Jedis jedis =null; try { jedis =jedisPool.getResource(); //讀取目錄裡的所有檔案 File f = new File(savePath+"/"+jedis.get("fileName_"+fileName)); File[] fileArray = f.listFiles(new FileFilter(){ //排除目錄只要檔案 @Override public boolean accept(File pathname) { if(pathname.isDirectory()){ return false; } return true; } }); //轉成集合,便於排序 List<File> fileList = new ArrayList<File>(Arrays.asList(fileArray)); Collections.sort(fileList,new Comparator<File>() { @Override public int compare(File o1, File o2) { if(Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())){ return -1; } return 1; } }); //擷取檔名的字尾名 //最後一個"."的位置 int pointIndex=fileName.lastIndexOf("."); //字尾名 String suffix=fileName.substring(pointIndex); //合併後的檔案 File outputFile = new File(savePath+"/"+jedis.get("fileName_"+fileName)+suffix); //建立檔案 try { outputFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } //輸出流 FileChannel outChnnel = new FileOutputStream(outputFile).getChannel(); //合併 FileChannel inChannel; for(File file : fileList){ inChannel = new FileInputStream(file).getChannel(); try { inChannel.transferTo(0, inChannel.size(), outChnnel); } catch (IOException e) { e.printStackTrace(); } try { inChannel.close(); } catch (IOException e) { e.printStackTrace(); } //刪除分片 file.delete(); } try { outChnnel.close(); } catch (IOException e) { e.printStackTrace(); } //清除資料夾 File tempFile = new File(savePath+"/"+jedis.get("fileName_"+fileName)); if(tempFile.isDirectory() && tempFile.exists()){ tempFile.delete(); } Map<String, String> resultMap=new HashMap<>(); //將檔案的最後上傳時間和生成的檔名返回 resultMap.put("lastUploadTime", jedis.get("lastUploadTime_"+newFilePath)); resultMap.put("pathFileName", jedis.get("fileName_"+fileName)+suffix); /****************清除redis中的相關資訊**********************/ //合併成功後刪除redis中的進度資訊 jedis.del("jindutiao_"+newFilePath); //合併成功後刪除redis中的最後上傳時間,只存沒上傳完成的 jedis.del("lastUploadTime_"+newFilePath); //合併成功後刪除檔名稱與該檔案上傳時生成的儲存分片的臨時資料夾的名稱在redis中的資訊 key:上傳檔案的真實名稱 value:儲存分片的臨時資料夾名稱(由上傳檔案的MD5值+時間戳組成) //如果下次再上傳同名檔案 redis中將儲存新的臨時資料夾名稱 沒有上傳完成的還要保留在redis中 直到定時任務生效 jedis.del("fileName_"+fileName); Gson gson=new Gson(); String json=gson.toJson(resultMap); PrintWriterJsonUtils.printWriter(response, json); } catch (Exception e) { e.printStackTrace(); }finally{ jedisPool.returnResource(jedis); } }else if(param.equals("checkChunk")){ /*************************檢查當前分塊是否上傳成功**********************************/ String fileMd5 = request.getParameter("fileMd5"); String chunk = request.getParameter("chunk"); String chunkSize = request.getParameter("chunkSize"); String jindutiao=request.getParameter("jindutiao");//檔案上傳的實時進度 Jedis jedis =null; try { jedis =jedisPool.getResource(); //將當前進度存入redis jedis.set("jindutiao_"+newFilePath, jindutiao); //將系統當前時間轉換為字串 Date date=new Date(); SimpleDateFormat formatter=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String lastUploadTime=formatter.format(date); //將最後上傳時間以字串形式存入redis jedis.set("lastUploadTime_"+newFilePath, lastUploadTime); //自定義檔名: 時間戳(13位) String tempFileName= String.valueOf(System.currentTimeMillis()); if(jedis.get("fileName_"+fileName)==null || "".equals(jedis.get("fileName_"+fileName))){ //將檔名與該檔案上傳時生成的儲存分片的臨時資料夾的名稱存入redis //檔案上傳時生成的儲存分片的臨時資料夾的名稱由MD5和時間戳組成 jedis.set("fileName_"+fileName, fileMd5+tempFileName); } File checkFile = new File(savePath+"/"+jedis.get("fileName_"+fileName)+"/"+chunk); response.setContentType("text/html;charset=utf-8"); //檢查檔案是否存在,且大小是否一致 if(checkFile.exists() && checkFile.length()==Integer.parseInt(chunkSize)){ //上傳過 try { response.getWriter().write("{\"ifExist\":1}"); } catch (IOException e) { e.printStackTrace(); } }else{ //沒有上傳過 try { response.getWriter().write("{\"ifExist\":0}"); } catch (IOException e) { e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); }finally{ jedisPool.returnResource(jedis); } } } //儲存上傳分片 public void fileSave(HttpServletRequest request, HttpServletResponse response) { DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload sfu = new ServletFileUpload(factory); sfu.setHeaderEncoding("utf-8"); String savePath = request.getRealPath("/"); savePath = new File(savePath) + "/upload/"; String fileMd5 = null; String chunk = null; String fileName=null; try { List<FileItem> items = sfu.parseRequest(request); for(FileItem item:items){ //上傳檔案的真實名稱 fileName=item.getName(); if(item.isFormField()){ String fieldName = item.getFieldName(); if(fieldName.equals("fileMd5")){ try { fileMd5 = item.getString("utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } if(fieldName.equals("chunk")){ try { chunk = item.getString("utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } }else{ Jedis jedis =null; try { jedis =jedisPool.getResource(); File file = new File(savePath+"/"+jedis.get("fileName_"+fileName)); if(!file.exists()){ file.mkdir(); } File chunkFile = new File(savePath+"/"+jedis.get("fileName_"+fileName)+"/"+chunk); FileUtils.copyInputStreamToFile(item.getInputStream(), chunkFile); } catch (Exception e) { e.printStackTrace(); }finally{ jedisPool.returnResource(jedis); } } } } catch (FileUploadException e) { e.printStackTrace(); } } //當有檔案新增進佇列時 通過檔名檢視該檔案是否上傳過 上傳進度是多少 public String selectProgressByFileName(String fileName) { String jindutiao=""; Jedis jedis =null; try { jedis =jedisPool.getResource(); if(null!=fileName && !"".equals(fileName)){ jindutiao=jedis.get("jindutiao_"+fileName); } }catch(Exception e){ e.printStackTrace(); }finally{ jedisPool.returnResource(jedis); } return jindutiao; }
注:webUploader斷點上傳多個大檔案時是按佇列順序上傳的,即佇列中的檔案一個一個上傳,前一個上傳完成才會開始上傳下一個,不能實現同時上傳。