1. 程式人生 > >plupload 大文件分片上傳與PHP分片合並探索

plupload 大文件分片上傳與PHP分片合並探索

fun func ram 完成後 隊列 explode 應用服務器 完成 所有

最近老大分給我了做一個電影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 學習的相關地址:

  1. pluplod github 地址
  2. pluplod 官網(可能要FQ)
  3. pluplod 中文文檔

plupload 大文件分片上傳與PHP分片合並探索