1. 元件簡介
webuploader:是一個以HTML5為主, Flash為輔的檔案上傳元件,採用大檔案分片/併發上傳的方式,極大地提高了檔案上傳的效率,同時相容多種瀏覽器版本;
2. 專案背景簡介
本篇文章的背景,是在上一篇文章(《無元件實現大檔案分片上傳,斷點續傳》)的專案背景下進行的一次嘗試,所以本篇文章還是基於上一篇文章的背景,但是不會介紹視訊基本資訊(視訊標題、簡介、播出時間等)的操作,主要介紹檔案的上傳。因為專案的特殊需求,這種使用插進的方式最終沒有被採用,因為一些控制元件無法做到定製化。
上一篇文章(《無元件實現大檔案分片上傳,斷點續傳》)中介紹的檔案上傳方式,在前端主要採用純JavaScript來進行檔案切分、驗證,後臺主要採用了NIO的方式進行分片的追加。而在這篇文章中,將介紹前端採用webuploader,後臺採用臨時目錄+傳統I/O方式進行分片合併的方式。
3. 技術實現
3.1 元件引入
在webuploader官網下載必要的檔案,放入專案中。在頁面中進行引入;
<!-- webuploader檔案上傳 -->
<script src="static/webuploader/webuploader.nolog.min.js"></script>
<link href="static/webuploader/webuploader.css" rel="stylesheet" type="text/css" />複製程式碼
3.2 前端頁面實現
在前端頁面中,可以不用關心css樣式,但需要注意標籤的id/nama屬性,這些將在後面的JavaScript中使用到。
<!-- Main content -->
<section class="content">
<div class="container" style="margin-top: 20px">
<div class="alert alert-info">可以一次上傳多個大檔案</div>
</div>
<div class="container" style="margin-top: 50px">
<div id="uploader" class="container">
<div class="container">
<div id="fileList" class="uploader-list"></div>
<!--存放檔案的容器-->
</div>
<div class="btns container">
<div id="picker" class="webuploader-container"
style="float: left; margin-right: 10px">
<div>
選擇檔案 <input type="file" name="file"
class="webuploader-element-invisible" multiple="multiple">
</div>
</div>
<div id="UploadBtn" class="webuploader-pick"
style="float: left; margin-right: 10px">開始上傳</div>
<div id="StopBtn" class="webuploader-pick"
style="float: left; margin-right: 10px" status="suspend">暫停上傳</div>
</div>
</div>
</div>
</section>複製程式碼
3.3 使用元件實現檔案的上傳、切分、傳送
在這部分,將使用元件完成檔案上傳、MD5驗證、刪除、切片、上傳進度條顯示、暫停、繼續上傳及上傳成功/失敗時候的回撥。
<script type="text/javascript">
$(function () {
$list = $('#fileList');
var flie_count = 0;
var uploader = WebUploader.create({
//設定選完檔案後是否自動上傳
auto: false,
//swf檔案路徑
swf: 'static/webuploader/Uploader.swf',
// 檔案接收服務端。
server: 'micro/BigFileUp',
// 選擇檔案的按鈕。可選。
// 內部根據當前執行是建立,可能是input元素,也可能是flash.
pick: '#picker',
chunked: true, //開啟分塊上傳
chunkSize: 10 * 1024 * 1024,
chunkRetry: 3,//網路問題上傳失敗後重試次數
threads: 1, //上傳併發數
//fileNumLimit :1,
fileSizeLimit: 2000 * 1024 * 1024,//最大2GB
fileSingleSizeLimit: 2000 * 1024 * 1024,
resize: false//不壓縮
//選擇檔案型別
//accept: {
// title: 'Video',
// extensions: 'mp4,avi',
// mimeTypes: 'video/*'
//}
});
// 當有檔案被新增進佇列的時候
uploader.on('fileQueued', function (file) {
$list.append('<div id="' + file.id + '" class="item">' +
'<h4 class="info">' + file.name + '<button type="button" fileId="' + file.id + '" class="btn btn-danger btn-delete"><span class="glyphicon glyphicon-trash"></span></button></h4>' +
'<p class="state">正在計算檔案MD5...請等待計算完畢後再點選上傳!</p><input type="text" id="s_WU_FILE_'+flie_count+'" />' +
'</div>');
console.info("id=file_"+flie_count);
flie_count++;
//刪除要上傳的檔案
//每次新增檔案都給btn-delete繫結刪除方法
$(".btn-delete").click(function () {
//console.log($(this).attr("fileId"));//拿到檔案id
uploader.removeFile(uploader.getFile($(this).attr("fileId"), true));
$(this).parent().parent().fadeOut();//視覺上消失了
$(this).parent().parent().remove();//DOM上刪除了
});
//uploader.options.formData.guid = WebUploader.guid();//每個檔案都附帶一個guid,以在服務端確定哪些檔案塊本來是一個
//console.info("guid= "+WebUploader.guid());
//md5計算
uploader.md5File(file)
.progress(function(percentage) {
console.log('Percentage:', percentage);
})
// 完成
.then(function (fileMd5) { // 完成
var end = +new Date();
console.log("before-send-file preupload: file.size="+file.size+" file.md5="+fileMd5);
file.wholeMd5 = fileMd5;//獲取到了md5
//uploader.options.formData.md5value = file.wholeMd5;//每個檔案都附帶一個md5,便於實現秒傳
$('#' + file.id).find('p.state').text('MD5計算完畢,可以點選上傳了');
console.info("MD5="+fileMd5);
});
});
// 檔案上傳過程中建立進度條實時顯示。
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');
}
$li.find('p.state').text('上傳中');
$percent.css('width', percentage * 100 + '%');
});
//傳送前填充資料
uploader.on( 'uploadBeforeSend', function( block, data ) {
// block為分塊資料。
// file為分塊對應的file物件。
var file = block.file;
var fileMd5 = file.wholeMd5;
// 修改data可以控制傳送哪些攜帶資料。
console.info("fileName= "+file.name+" fileMd5= "+fileMd5+" fileId= "+file.id);
console.info("input file= "+ flie_count);
// 將存在file物件中的md5資料攜帶傳送過去。
data.md5value = fileMd5;//md5
data.fileName_ = $("#s_"+file.id).val();
console.log("fileName_: "+data.fileName_);
// 刪除其他資料
// delete data.key;
if(block.chunks>1){ //檔案大於chunksize 分片上傳
data.isChunked = true;
console.info("data.isChunked= "+data.isChunked);
}else{
data.isChunked = false;
console.info("data.isChunked="+data.isChunked);
}
});
uploader.on('uploadSuccess', function (file) {
$('#' + file.id).find('p.state').text('已上傳');
$('#' + file.id).find(".progress").find(".progress-bar").attr("class", "progress-bar progress-bar-success");
$('#' + file.id).find(".info").find('.btn').fadeOut('slow');//上傳完後刪除"刪除"按鈕
$('#StopBtn').fadeOut('slow');
});
uploader.on('uploadError', function (file) {
$('#' + file.id).find('p.state').text('上傳出錯');
//上傳出錯後進度條變紅
$('#' + file.id).find(".progress").find(".progress-bar").attr("class", "progress-bar progress-bar-danger");
//新增重試按鈕
//為了防止重複新增重試按鈕,做一個判斷
//var retrybutton = $('#' + file.id).find(".btn-retry");
//$('#' + file.id)
if ($('#' + file.id).find(".btn-retry").length < 1) {
var btn = $('<button type="button" fileid="' + file.id + '" class="btn btn-success btn-retry"><span class="glyphicon glyphicon-refresh"></span></button>');
$('#' + file.id).find(".info").append(btn);//.find(".btn-danger")
}
$(".btn-retry").click(function () {
//console.log($(this).attr("fileId"));//拿到檔案id
uploader.retry(uploader.getFile($(this).attr("fileId")));
});
});
uploader.on('uploadComplete', function (file) {//上傳完成後回撥
//$('#' + file.id).find('.progress').fadeOut();//上傳完刪除進度條
//$('#' + file.id + 'btn').fadeOut('slow')//上傳完後刪除"刪除"按鈕
});
uploader.on('uploadFinished', function () {
//上傳完後的回撥方法
//alert("所有檔案上傳完畢");
//提交表單
});
$("#UploadBtn").click(function () {
uploader.upload();//上傳
});
$("#StopBtn").click(function () {
console.log($('#StopBtn').attr("status"));
var status = $('#StopBtn').attr("status");
if (status == "suspend") {
console.log("當前按鈕是暫停,即將變為繼續");
$("#StopBtn").html("繼續上傳");
$("#StopBtn").attr("status", "continuous");
console.log("當前所有檔案==="+uploader.getFiles());
console.log("=============暫停上傳==============");
uploader.stop(true);
console.log("=============所有當前暫停的檔案=============");
console.log(uploader.getFiles("interrupt"));
} else {
console.log("當前按鈕是繼續,即將變為暫停");
$("#StopBtn").html("暫停上傳");
$("#StopBtn").attr("status", "suspend");
console.log("===============所有當前暫停的檔案==============");
console.log(uploader.getFiles("interrupt"));
uploader.upload(uploader.getFiles("interrupt"));
}
});
uploader.on('uploadAccept', function (file, response) {
if (response._raw === '{"error":true}') {
return false;
}
});
});
</script>複製程式碼
以上為前端程式碼的實現
3.4 後臺分片接收
在後臺分片接收部分,主要是判斷檔案是否有分片,如果沒有,則直接存放到目的目錄;如果存在分片,則建立臨時目錄,存放分片資訊;之後判斷當前分片所屬的檔案的所有分片是否已經傳輸完畢,如果當前分片數==所屬檔案總分片數,則開始合併檔案並轉移完整檔案到目的目錄,並且刪除臨時目錄;
如下圖,是上傳檔案時所建立的臨時目錄及目錄中的臨時檔案;
Controller實現
/**
*
* @Description:
* 接受檔案分片,合併分片
* @param guid
* 可省略;每個檔案有自己唯一的guid,後續測試中發現,每個分片也有自己的guid,所以不能使用guid來確定分片屬於哪個檔案。
* @param md5value
* 檔案的MD5值
* @param chunks
* 當前所傳檔案的分片總數
* @param chunk
* 當前所傳檔案的當前分片數
* @param id
* 檔案ID,如WU_FILE_1,後面數字代表當前傳的是第幾個檔案,後續使用此ID來建立臨時目錄,將屬於該檔案ID的所有分片全部放在同一個資料夾中
* @param name
* 檔名稱,如07-中文分詞器和業務域的配置.avi
* @param type
* 檔案型別,可選,在這裡沒有用到
* @param lastModifiedDate 檔案修改日期,可選,在這裡沒有用到
* @param size 當前所傳分片大小,可選,沒有用到
* @param file 當前所傳分片
* @return
* @author: xiangdong.she
* @date: Aug 20, 2017 12:37:56 PM
*/
@ResponseBody
@RequestMapping(value = "/BigFileUp")
public String fileUpload(String guid, String md5value, String chunks, String chunk, String id, String name,
String type, String lastModifiedDate, int size, MultipartFile file) {
String fileName;
JSONObject result=new JSONObject();
try {
int index;
String uploadFolderPath = FileUtil.getRealPath(request);
String mergePath = uploadFolderPath + "\\fileDate\\" + id + "\\";
String ext = name.substring(name.lastIndexOf("."));
// 判斷檔案是否分塊
if (chunks != null && chunk != null) {
index = Integer.parseInt(chunk);
fileName = String.valueOf(index) + ext;
// 將檔案分塊儲存到臨時資料夾裡,便於之後的合併檔案
FileUtil.saveFile(mergePath, fileName, file, request);
// 驗證所有分塊是否上傳成功,成功的話進行合併
FileUtil.Uploaded(md5value, guid, chunk, chunks, mergePath, fileName, ext, request);
} else {
SimpleDateFormat year = new SimpleDateFormat("yyyy");
SimpleDateFormat m = new SimpleDateFormat("MM");
SimpleDateFormat d = new SimpleDateFormat("dd");
Date date = new Date();
String destPath = uploadFolderPath + "\\fileDate\\" + "video" + "\\" + year.format(date) + "\\"
+ m.format(date) + "\\" + d.format(date) + "\\";// 檔案路徑
String newName = System.currentTimeMillis() + ext;// 檔案新名稱
// fileName = guid + ext;
// 上傳檔案沒有分塊的話就直接儲存目標目錄
FileUtil.saveFile(destPath, newName, file, request);
}
} catch (Exception ex) {
ex.printStackTrace();
result.put("code", 0);
result.put("msg", "上傳失敗");
result.put("data", null);
return result.toString();
}
result.put("code", 1);
result.put("msg", "上傳成功");
return result.toString();
}複製程式碼
3.5檔案I/O操作實現
此部分程式碼較多,已將FileUtil上傳至Batatas專案下的Util目錄(喜歡Batatas這個專案的小夥伴,別忘了點個star喲,或者也非常歡迎加入我們),在這部分實現中,主要用到了一下幾個方法:
- saveFile()//儲存分片至臨時目錄,或者儲存未拆分檔案到目標目錄;
- mergeFile()//合併臨時目錄中的臨時檔案,並將合併後的檔案轉移至目標目錄;
- saveStreamToFile()//使用I/O流合併分片檔案
- getSavePath()//獲取檔案儲存的路徑,如果沒有該目錄,則建立,可用於臨時目錄或目標存放目錄的建立;
- isAllUploaded()//在fileUtil中,使用一個全域性的uploadInfoList去存放,已經上傳的分片資訊;在合併分片之前,首先回去這個List中檢查屬於該檔案的所有分片資訊是否已經存在,如果不存在,則不合並;如果已全部存在,則將這些資訊從list中刪除,並開始合併分片;
4. 總結
本篇文章主要介紹了使用百度Webuploader元件進行大檔案的分片上傳、斷點續傳,以及伺服器端分片合併與轉移。