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 = WebUploader.Deferred();
    if(is_upload)//跳過到開始上傳的哪一個分片時
    {
        deferred.resolve();
    }else if(totalFiles)//已經獲取過文件數量則直接判斷是否跳過
    {
        //當前分片下標小於等於目錄下的文件數量就認爲分塊存在,因爲分塊上傳下標是由小到大
        if (!totalFiles.in_array(block.chunk)) {
            deferred.resolve();
        } else {
            //分塊存在,跳過
            deferred.reject();
        }
    } else {
        $.post('/api.php/upload/checkUpload', {md5: fileMd5}, function (data) {
            if (data.code) {
                totalFiles = data.data;
                //當前分片下標小於等於目錄下的文件數量就認爲分塊存在,因爲分塊上傳下標是由小到大
                if (!data.data.in_array(block.chunk)) {
                    deferred.resolve();
                } else {
                    //分塊存在,跳過
                    deferred.reject();
                }
            } else {
                is_upload = true;
                deferred.resolve();
            }
        });
    }
    return deferred.promise();

後端:

//分片驗證
public function checkChunk()
{
    $md5 = request()->param()\['md5'\];
    $dir = PATH_FILE . $md5;
    if(is_dir($dir)) {
        $files = $this->getFileByPath($dir);
        foreach ($files as $k=>$v)
        {
            $file_name = explode('/',$v);
            $file_name = $file_name\[count($file_name) - 1\];
            $file_index = explode('.',$file_name)\[0\];
            $files\[$k\] = (int)$file_index;
        }
        return $files;
    }else{
        return false;
    }
}

利用文件名的有序性判斷當前分片索引是否存在服務器
判斷是否存在數組中的函數:

Array.prototype.in_array = function (element) {
    for (var i = 0; i < this.length; i++) {
        if (this\[i\] == element) {
            return true;
        }
    } return false;
};

本文爲勇娃子原創文章,轉載無需和我聯繫,但請註明來自勇娃子博客blog.93dd.top

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章