1. 程式人生 > >php結合webuploader斷點續傳的實現

php結合webuploader斷點續傳的實現

最近公司專案需要用到斷點續傳,所以記錄一下其中的坑 使用到的主要技術

  1. webuploader
  2. thinkphp5

斷點續傳的思路:

  1. 客戶端:

         1.獲取檔案md5(MD5是檔案唯一標識,用來判斷是否存在此檔案,並且用作分片的資料夾名)
    
         2.將檔案分片
    
         3.驗證分片是否上傳過,上傳過直接跳過當前分片
    
         3.上傳分片到md5的資料夾(儲存檔名建議按分片序號來,因為分片的順序很重要)
    
         4.最後一個分片上傳完成後傳送合併分片請求並由伺服器返回檔案資訊
    
  2. 服務端

         1.獲取md5資料夾下的檔案數量並返回用作分片驗證
    
         2.接收檔案分片並儲存到檔案md5的資料夾,檔名稱使用分片序號:如0.mp4,1.mp4
    
         3.合併分片時將md5資料夾下的所有檔案按檔名順序提取並寫入到最終的檔案內
    
         4.寫入完成獲取最終檔案hash並判斷是否存在,存在則返回已存在檔案,刪除當前檔案,不存在則寫入資料庫並返回檔案資訊
    

大體思路是這樣,實際還要加入許多驗證,比客戶端獲取到md5後馬上要驗證檔案是否存在,存在就不上傳,直接使用檔案資訊,不存在則上傳 分片上傳前也要驗證,不過分片的跳過規則需要注意,伺服器只需要返回已有的分片數量,客戶端根據已有的分片和當前分片索引即可判斷是否應該跳過,因為分片是按順序上傳的,如: 在這裡插入圖片描述 ,服務端需要注意合併的時候順序不能亂,順序亂了就無法還原原始檔,所以建議用分片索引作為檔名,獲取的時候直接按0,1,2這樣獲取就行了.

並且儲存時要注意儲存在自定義的目錄下需要確保該目錄存在,不存在的目錄需要新建後才可以儲存檔案

完整程式碼如下:

客戶端:

var uploader;
var fileMd5;
var is_upload; var totalFiles; function initWebUploader() { /***************************************************** 監聽分塊上傳過程中的三個時間點 start ***********************************************************/ WebUploader.Uploader.register({ "before-send":"beforeSend", //每個分片上傳前 }, { //時間點2:如果有分塊上傳,則每個分塊上傳之前呼叫此函式
beforeSend:function(block){ var deferred = WebUploader.Deferred(); if(is_upload)//跳過到開始上傳的哪一個分片時 { deferred.resolve(); }else if(totalFiles)//已經獲取過檔案數量則直接判斷是否跳過 { //當前分片下標小於等於目錄下的檔案數量就認為分塊存在,因為分塊上傳下標是由小到大 if (block.chunk > totalFiles - 1) { is_upload = true; deferred.resolve(); } else { //分塊存在,跳過 deferred.reject(); } } else { $.post('/api.php/upload/checkUpload', {md5: fileMd5}, function (data) { if (data.code) { totalFiles = data.data; //當前分片下標小於等於目錄下的檔案數量就認為分塊存在,因為分塊上傳下標是由小到大 if (block.chunk > data.data - 1) { is_upload = true; deferred.resolve(); } else { //分塊存在,跳過 deferred.reject(); } } else { is_upload = true; deferred.resolve(); } }); } return deferred.promise(); } }); /***************************************************** 監聽分塊上傳過程中的三個時間點 end **************************************************************/ uploader = WebUploader.create({ // swf檔案路徑 swf: '{$maccms.path}static/webupload/Uploader.swf', // 檔案接收服務端。 server: '/api.php/upload/chunkUpload', // 選擇檔案的按鈕。可選。 // 內部根據當前執行是建立,可能是input元素,也可能是flash. pick: '#name', accept: { title: 'Images', extensions: 'mp4', }, // 不壓縮image, 預設如果是jpeg,檔案上傳前會壓縮一把再上傳! resize: false, prepareNextFile:true, chunked : true, // 分片處理 chunkSize : 5 * 1024 * 1024, // 每片50M,經過測試,發現上傳1G左右的視訊大概每片50M速度比較快的,太大或者太小都對上傳效率有影響 chunkRetry : false,// 如果失敗,則不重試 threads:1, formData:{guid:WebUploader.Base.guid()} }); uploader.on('fileQueued',function (file) { uploader.md5File( file,0,10*1024*1024 ) // 及時顯示進度 .progress(function(percentage) { console.log('Percentage:', percentage); $(".readFile").text("正在讀取視訊資訊..."+(percentage * 100).toFixed(2) + '%'); }) // 完成 .then(function(val) { console.log('md5 result:', val); $(".readFile").text(''); fileMd5 = val; var formData = uploader.option('formData'); formData.md5 = val; uploader.option('formData',formData); //驗證檔案是否存在 $.post('/api.php/upload/md5check',{md5:val},function (data) { if(data.code) { //選擇後直接上傳 uploader.upload(); $("#name .webuploader-pick").text(file.name); $(".up-state .file_name").text(file.name); var size; size = Math.round(file.size/(1024*1024),2); if(size > 1024) { size = Math.round(size/1024,2) + 'G'; }else{ size += 'M'; } $(".up-state .file_size").text(size); $('.up-in').width('0'); $('.bar').find('span').html('0%'); $('.up-con').hide(); $('.up-state').show(); $('#go').click(function () { $('.up-end').hide(); $('.up-con').show(); }) }else{ var msg = '該視訊已存在!'; alert(msg); uploader.reset(); $("#name .webuploader-pick").text(file.name); $(".up-state .file_name").text(file.name); $(".file-address input\[name=vod_play_url\]").val(data.data.path); $(".file-address input\[name=url_id\]").val(data.data.id); //獲取視訊時長,配合video標籤 $("#duration").attr("src",data.data.path); } }); }); }); // 檔案上傳過程中建立進度條實時顯示。 uploader.on( 'uploadProgress', function( file, percentage ) { $('.bar').find('span').html((percentage * 100).toFixed(2) + '%'); $('.up-in').width(percentage * 100 + '%'); }); uploader.on( 'uploadSuccess', function( file,res ) { //最後一塊完成時間 //全部上傳完成傳送合併請求 $.post('/api.php/upload/videoUpload',{md5:fileMd5},function (data) { if(data.code) { $('.up-end').show(); $('.up-state').hide(); setTimeout(function () { $(".up-end").hide(); $('.up-con').show(); },2000); $(".file-address input\[name=vod_play_url\]").val(data.data.path); $(".file-address input\[name=url_id\]").val(data.data.id); $("#duration").attr("src",data.data.path); }else{ (".up-end h1").text('上傳出錯'); $('.up-end').show(); $('.up-state').hide(); setTimeout(function () { $(".up-end").hide(); $('.up-con').show(); },2000); } }); }); uploader.on( 'uploadError', function( file ) { $(".up-end h1").text('上傳出錯'); $('.up-end').show(); $('.up-state').hide(); setTimeout(function () { $(".up-end").hide(); $('.up-con').show(); },2000); }); }

伺服器:

//分片上傳
function chunkUpload($name = 'file')
{
    $md5 = request()->param()\['md5'\];
    $object_info = request()->file($name);
    //儲存檔案的順序很重要!
    $object = $object_info->rule('uniqid')->move(PATH_FILE . $md5 . '/',request()->param()\['chunk'\]);
    if($object)
    {
       return \['chunks'=>request()->param()\['chunks'\],'chunk'=>request()->param()\['chunk'\]\];
    }else{
        return false;
    }
}
//最終合併檔案
function videoUpload()
{
    $md5 = request()->param()\['md5'\];
    $dir = PATH_FILE . $md5;
    if(is_dir($dir)) {
        //獲取檔案的順序很重要!!!
        $files = \[\];
        $chunk_id = 0;
        $chunk_file = PATH_FILE . $md5 . '/';
        while (file_exists($chunk_file . $chunk_id . '.mp4')){
            $files\[\] = $chunk_file . $chunk_id . '.mp4';
            $chunk_id++;
        }
        $file_name = randomStr().'.mp4';
        $path_file = date('Ymd') . SYS_DS_PROS . $file_name;
        //日期目錄不存在則建立目錄
        if(!is_dir(PATH_FILE . date('Ymd')))
        {
            mkdir(PATH_FILE . date('Ymd'));
        }
        $count = 0;
        foreach ($files as $v)
        {
            $_file = file_get_contents($v);
            $_res = file_put_contents(PATH_FILE . $path_file,$_file,FILE_APPEND);
            if($_res)
            {
                unlink($v);
            }else{
                $count++;
            }
        }
        if($count == 0)
        {
            rmdir($dir);
            //檢查合併後的檔案hash
            $_hash = hash_file('sha1', PATH_FILE . $path_file);
            //合併後的檔案已存在則刪除已合併檔案並返回已有檔案資訊
            $file_info = $this->modelFile->getInfo(\['sha1'=>$_hash\],'id,name,path,sha1,guid,md5');
            if(!empty($file_info))
            {
                unlink(PATH_FILE . $path_file);
                return $file_info;
            }
            //合併後的檔案入庫並返回
            $_data = \['name' => $file_name, 'path' => $path_file, 'sha1' => $_hash,'guid'=>'','md5'=>request()->param()\['md5'\]\];
            $result = $this->modelFile->addInfo($_data);
            $_data\['id'\] = $result;
            return $_data;
        }else{
            $this->error = '合併檔案失敗';
            return false;
        }
    }else{
        $this->error = '分片目錄不存在!';
        return false;
    }
}
//分片驗證
function checkChunk()
{
    $md5 = request()->param()\['md5'\];
    $dir = PATH_FILE . $md5;
    if(is_dir($dir)) {
        $files = $this->getFileByPath($dir);
        return count($files);
    }else{
        return false;
    }
}

========================================================================= 最新更新: 分片驗證存在bug,當上傳分片不小心刪除了前面的分片時就導致無法合成檔案(檔案數量導致跳過了),因此,更新分片驗證 前端:

//時間點2:如果有分塊上傳,則每個分塊上傳之前呼叫此函式
beforeSend:function(block){
    var deferred = WebUpload