plupload 大文件分片上傳與PHP分片合並探索
最近老大分給我了做一個電影cms系統,其中涉及到一個功能,使用七牛雲的文件上傳功能。七牛javascript skd,使用起來很方便,屏蔽了許多的技術細節。如果只滿足與調用sdk,那麽可能工作中也就沒有什麽收獲了。其中對七牛雲的服務很佩服的一點是,無論我上傳多大的文件,當我文件最後一片上傳完成的時候,就立刻返回到文件鏈接,這個問題我想了好久,也不知道七牛是如何做到的。
七牛雲的sdk分為 JavaScript 與 PHP端。 JavaScript端的作用是提供文件上傳功能,客戶端(瀏覽器)無需關註當前的環境,七牛雲的sdk會自動檢測瀏覽器的版本,並提供統一的接口調用。PHP端主要作用是提供鑒權的,每次七牛雲上傳圖片到七牛雲的空間,需要到自己的應用服務器,拿到一個授權的key,你可以理解為密鑰。
關於七牛雲 JavaScript 與 PHP端的 SDK 使用,本篇就不贅言了,官網的文檔描述的很清楚,可以參考 七牛雲javascript sdk 文檔
七牛雲的JavaScript SDK 繼承了 plupload 的所有方法。本篇的主要目的是展示plupload的使用場景,以及文件分片上傳後,後端服務器如何處理,提供一個思路。
如果你有以下的業務場景,可以嘗試使用plupload 插件
1、用戶上傳圖片,需要實時預覽,並且兼容主流瀏覽器 2、上傳的圖片需要在本地進行質量壓縮,或者文件類型校驗 3、上傳圖片的時候,需要提示上傳的百分比 4、客戶端可能需要上傳較大的文件,但是服務端的配置並不允許開啟大文件上傳 5、需要斷點續傳功能,即文件上傳了一半,下次上傳時可以接著上傳。
前端的代碼和後端PHP的代碼放在文後,代碼並不重要,思路最重要。
前端將文件分成片後,瀏覽器開啟多線程上傳服務。例如將一個文件分成100片,由於異步的原因,可能第1片 和 第10片 先到,因而分片的上傳你可以理解為亂序的。另外php上傳文件是上傳到臨時文件夾,當腳本執行結束後,就會自動刪除文件。所以如果不對分片上傳的數據進行保存,那麽就會竹籃打水一場空,等所有分片都上傳完了,結果卻無法合並。因此我們後端需要解決幾個問題:
1、將分片保存起來
2、自己維護分片的順序
3、需要將分片合並到最終的目標文件中
4、當分片合並後,需要刪除無用的分片
維護分片的這部分解決,我使用了Reids 的Zset 數據結構,該結構能幫我解決分片的順序。
分片上傳的時候,會標註共有多少分片, 這是第幾個分片.
後端PHP代碼:
<?php
/**
* 這是個上傳測試文件,作為研究分片上傳原理使用
*
*/
if (!is_array($_FILES) || empty($_FILES)) {
die(); //輸出報錯的話術
}
$destication = "/usr/local/var/www/uploads/"; //上傳文件的最終文件夾
$destication_frag_path = "/usr/local/var/www/uploads_tmp/"; //分片上傳的臨時文件夾
if (!is_dir($destication) || !is_dir($destication_frag_path)) {
@mkdir($destication, 0755);
@mkdir($destication_frag_path, 0755);
}
if ($_REQUEST['chunks'] == 1) {
//文件很小,無需分片上傳。
$tmp_file_path = $_FILES['file']['tmp_name']; //上傳的臨時文件
$save_file_name = $destication.$_REQUEST['name'];
move_uploaded_file($tmp_file_path, $save_file_name);
} else {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis_key = $_REQUEST['name'];
$file_name = explode('.', $_REQUEST['name']);
$save_tmp_name = $destication_frag_path.$file_name[0]."_".$_REQUEST['chunk']; //文件名拼接成第幾塊
$tmp_file_path = $_FILES['file']['tmp_name']; //上傳的臨時文件
move_uploaded_file($tmp_file_path, $save_tmp_name);
$redis->setTimeout($redis_key, 3600); //一個小時後過期
$redis->zAdd($redis_key, $_REQUEST['chunk'], $save_tmp_name);
$uploaded_count = $redis->zCard($redis_key);
//分片資源上傳完畢後,開始分片合並工作
if ($uploaded_count == $_REQUEST['chunks']) {
//獲取經過排序後的分片資源
$all_files_fen_pian = $redis->zRange($redis_key, 0, -1);
if ($all_files_fen_pian && is_array($all_files_fen_pian)) {
//創建要合並的最終文件資源
$final_file = $destication.$_REQUEST['name'];
$final_file_handler = fopen($final_file, 'wb');
//開始合並文件分片
foreach ($all_files_fen_pian as $fragmentation_file) {
$frag_file_handler = fopen($fragmentation_file, 'rb');
$frag_file_content = fread($frag_file_handler, filesize($fragmentation_file));
fwrite($final_file_handler, $frag_file_content);
unset($frag_file_content);
fclose($frag_file_handler); //銷毀分片文件資源
unlink($fragmentation_file); //刪除已經合並的分片文件
usleep(10000);
}
}
}
}
前端Javascript 代碼:
var uploader = new plupload.Uploader({
browse_button : 'browse', //觸發文件選擇對話框的按鈕,為那個元素id
url : 'upload.php', //服務器端的上傳頁面地址
flash_swf_url: './public/js/plupload/js//Moxie.swf', //flash文件地址
max_file_size: '1000mb',//限制為2MB
chunk_size:'1mb',
unique_names:true, //為每個文件生成一個臨時名稱
max_retries:3,
multipart_params:{
},//擴展參數
//filters: [{title: "image ",extensions: "jpg,gif,png"}], //圖片限制
//filters: [{title: "movie ",extensions: "mp4"}], //電影限制
silverlight_xap_url : 'js/Moxie.xap' //silverlight文件,當需要使用silverlight方式進行上傳時需要配置該參數
});
uploader.init(); //初始化uploader
uploader.start(); //開始上傳
uploader.stop(); //暫停上傳
$("#start_upload").click(function(){
uploader.start();
});
//文件添加的時候
uploader.bind('FilesAdded', function (uploader, files) {
/**
* 因此在這一步可以判斷文件的上傳個數,和文件的格式,以及上傳文件的大小,以及進行圖片預覽的相關東東
*/
console.log('-----當文件添加的時候打印 開始----');
for (i=0; i<files.length; i++ ) {
var tmp_file_size = files[i]['size'] / 1024;
var tmp_msg = '添加文件的索引:'+ files[i]['id']+ "\t\t原始文件名:"+files[i]['name'] + "\t文件後綴:"+files[i]['type']+"\t文件的大小:"+tmp_file_size+"kb";
console.log(tmp_msg);
}
// 如果是圖片的時候可以啟用這部分
// mOxie.each(files, function(files) {
// var image = new mOxie.Image();
// image.onload = function() {
// var dataUrl = image.getAsDataURL();
// $("#preview_img").attr('src', dataUrl);
// };
//
// image.load(files.getSource());
// });
console.log('-----當文件添加的時候打印 結束----');
});
//當上傳隊列中某一個文件開始上傳後觸發。
uploader.bind('BeforeUpload', function(uploader, file){
console.log('文件開始上傳了');
//console.dir(file);
});
//當使用文件小片上傳功能時,每一個小片上傳完成後觸發
uploader.bind('ChunkUploaded', function(uploader,file,responseObject){
console.log('-----當文件上傳分片的時候打印 開始----');
//console.dir(JSON.parse(responseObject['response']));
console.log('-----當文件上傳分片的時候打印 結束---');
});
//會在文件上傳過程中不斷觸發,可以用此事件來顯示上傳進度
uploader.bind('UploadProgress', function(uploader,file) {
//console.log(uploader);
//console.log(file);
});
//當隊列中的某一個文件上傳完成後觸發
uploader.bind('FileUploaded',function(uploader,files,data){
console.log('-----當文件上傳完成的時候打印 開始----');
//console.dir(files);
console.dir(data);
console.log('-----當文件上傳完成的時候打印 結束----');
});
//當上傳隊列中所有文件都上傳完成後觸發
uploader.bind('UploadComplete', function(uploader,files) {
console.log('所有的文件都已經上傳完畢了');
});
//當上傳發聲錯誤時觸發
uploader.bind('Error', function(uploader,errObj) {
console.log('-----當文件上傳錯誤的時候打印 開始----');
console.dir(errObj);
console.log('-----當文件上傳錯誤的時候打印 結束----');
});
</script>
註意事項: 本代碼僅作為研究分片上傳的原理使用,未應用於生產環境
本文代碼github 地址: roverliang 的github
plupload 學習的相關地址:
- pluplod github 地址
- pluplod 官網(可能要FQ)
- pluplod 中文文檔
plupload 大文件分片上傳與PHP分片合並探索