視頻大文件分片上傳(使用webuploader插件)


背景

  • 公司做網盤系統,一直在調用圖片服務器的接口上傳圖片,以前寫的,以為簡單改一改就可以用
  • 最初要求 php 上傳多種視頻格式,支持大文件,並可以封面截圖,時長統計

問題

  • 1、上傳到阿里雲服務器,13秒左右,連接被重置
  • 2、切換到本地服務器后 413 Request Entity Too Large / nginx
  • 第2個問題還好,一般設置一下php.ini 和 nginx.conf 文件中的上傳文件大小限制即可,但卻不是最佳選項,因為一個視頻2G算是正常大小,因此修改上傳限制到2048MB不太現實,即使修改了也會超時。
  • 第1個問題,應該是阿里策略,不允許大文件上傳,解決了第一個問題應該也會消失。

思考

  • 首先不考慮整個文件直接上傳的方案,於是搜索,發現有分片上傳這個名詞,
  • 看了下原理,大概意思是將一個大文件按照一定尺寸進行切割,然后逐個發送到后台, 后台接收到所有的分片文件后,再組裝成原文件。
  • 遂深入,發現出自百度的 webuploader ,支持分片、並發、預覽壓縮、拖拽、MD5秒傳等
  • 看了幾篇網上的使用案例以及官網的 getting-started.html, 總來來說網上的博客質量不高,有的淺嘗輒止,有的代碼缺失,有的只注重前端,有的將代碼放到csdn,需要積分下載,
  • 官方的說明還可以,就是初始化的時候,案例中未標明變量的來源,作為一個后端,搞了好久才明白。
  • 故此寫一篇文章以作備用。

本文將提供完整的代碼和注釋,js + php,因為前端本身是其他人的項目,我在這里只提供比較簡單的代碼, 既好理解也好套用。

<!DOCTYPE html>
<html>
<head>
<!--引入JQuery  插件基於JQuery-->
<script type="text/javascript" src="/resources/scripts/jquery-1.8.2.min.js"></script>

<!--引入插件的CSS文件-->
<link rel="stylesheet" type="text/css" href="/resources/webuploader/webuploader.css">

<!--引入插件的JS文件-->
<script type="text/javascript" src="/resources/webuploader/webuploader.js"></script>

<!--SWF在初始化的時候指定,在后面將展示-->


<title>視頻</title>
</head>
<body class="video-body">
<div class="index-div">
    <!--從 uploader 開始,是上傳相關代碼-->
    <div id="uploader" class="wu-display">
        <!--用來存放文件信息-->
        <div id="thelist" class="uploader-list"></div>
        <div class="btns">
            <!--<input class="style_file_content" accept="video/mp4" type="file" id="upload_file_id"/>-->
            <div id="picker">選擇文件</div>
            <button id="ctlBtn" class="btn btn-default" >開始上傳</button>
            <button id="resetBtn" class="btn btn-default" >重試</button>
        </div>
    </div>
</div>
</body>
<script>

    $(function () {
        alert("uploader");
        $list = $('#thelist'),
            $ctlBtn = $('#ctlBtn'),
            $resetBtn = $('#resetBtn'),
            state = 'pending',
            uploader;
        // 初始化WebUploader插件
        uploader = WebUploader.create({

            // swf文件路徑, 需要修改為你自己存放的路徑
            swf: '/resources/webuploader/Uploader.swf',
            // 文件接收服務端。  // 需要修改為你的后端地址
            server: 'http://self-pic.klagri.com.cn/api/file/video',
            // dnd 指定Drag And Drop拖拽的容器,如果不指定,則不啟動
            // 禁用全局拖拽,否則在沒有啟動拖拽容器的情況下,視頻拖進來后會直接在瀏覽器內播放。
            disableGlobalDnd: true,

            // 選擇文件的按鈕。可選。內部根據當前運行是創建,可能是input元素,也可能是flash.
            pick: {
                id: '#picker',                     // 對應 html 中的 picker
                innerHTML: '選擇文件',   // 按鈕上顯示的文字
                multiple: true,                  // 多文件選擇
            },

            // 允許視頻和圖片類型的文件上傳。
            accept: {
                title: 'Video',
                extensions: 'mp4,gif,jpg,jpeg,bmp,png',      // 可以多個后綴,以逗號分隔, 不要有空格
                mimeTypes: 'video/*,image/*'
            },

            // 只允許選擇圖片文件。
            //accept: {
            // title: 'Images',
            //  extensions: '',
            //  mimeTypes: ''
            //}

            // thumb配置生成縮略圖的選項, 此項交由后台完成, 所以前台未配置

            // 自動上傳暫時關閉,使用多文件隊列上傳, 如果值為true,那么在選擇完文件后,將直接開始上傳文件,因為我還要做一些其他處理,故選擇false。
            auto: false,

            //是否允許在文件傳輸時提前把下一個文件准備好。 對於一個文件的准備工作比較耗時,比如圖片壓縮,md5序列化。 如果能提前在當前文件傳輸期處理,可以節省總體耗時。
            prepareNextFile: true,

            // 可選,是否要分片處理大文件上傳
            chunked: true,
            // 如果要分片,分多大一片?這里我設置為2M, 如需更大值,可能需要需修改php.ini等配置
            chunkSize:2*1024*1024,
            // 如果某個分片由於網絡問題出錯,允許自動重傳多少次
            chunkRetry: 3,
            // 上傳並發數,允許同時上傳最大進程數,默認3
            threads:5,

            // formData {Object} [可選] [默認值:{}] 文件上傳請求的參數表,每次發送都會發送此對象中的參數。 其實就是post中的表單數據,可自定義字段。
            formData: {
                context: 1,     // 這里是我的業務數據,你可以自定義或者去掉此項都可以
                from: "pan"    // 這里是我的業務數據,你可以自定義或者去掉此項都可以
            },
            //[可選] 驗證文件總數量, 超出9個文件則不允許加入隊列。
            fileNumLimit: 9,
            // 驗證文件總大小是否超出限制(2G), 超出則不允許加入隊列。根據需要進行設置。除了前面幾個,其它都是可選項
            fileSizeLimit: 1024*1024*1024*2,
            // 驗證單個文件大小是否超出限制(2G), 超出則不允許加入隊列。
            fileSingleSizeLimit: 1024*1024*1024*2,
            // [可選] 去重, 根據文件名字、文件大小和最后修改時間來生成hash Key.
            duplicate: true,
            // 不壓縮image, 默認如果是jpeg,文件上傳前會壓縮一把再上傳!
            // resize: false,
            // 壓縮選項
            compress: {
                // 如果壓縮后比源文件大,則不壓縮,圖片有可能壓縮后比原文件還大,需設置此項
                noCompressIfLarger: true,
            },
        });

        // 以下都是監聽事件, 方法中的file 和 response 參數,需了解,並

        // 當有文件被添加進隊列的時候觸發,用於顯示加載進度條
        uploader.on( 'fileQueued', function( file ) {
            $list.append( '<div style="position: relative" id="' + file.id + '" class="item">' +
                '<h4 class="info" style="width: 150px; text-overflow: ellipsis">' + file.name + '</h4>' +
                '<p class="state" style="position: absolute; top: 0px; left: 120px; width: 120px; border: #00a2d4 solid 1px">正在加載...</p>' +
                '</div>' );
            var $li = $( '#'+file.id );
            // 生成文件的MD5值, 可以用來實現秒傳, 如不需要,可以忽略(數據庫中保存md5值,如果存在相同md5,直接在文件服務器復制一份,不需再次分片上傳以及合並,極快)
            uploader.md5File( file )
            // 及時顯示進度
                .progress(function(percentage) {
                    $percent = $li.find('.state');
                    $li.find('p.state').text('加載中 '+  Math.round(percentage * 100)  + '%');
                    console.log('Percentage:', percentage);
                })
                // 完成
                .then(function(md5) {
                    // 將md5值加入到post的表單數據formData中, 與上文中的 context 和 from字段相同
                    uploader.option("formData",{
                        ... {"md5": md5}
                    });
                    console.log('md5:', md5);
                    alert("fileQueued")

                });
        });

        // 文件上傳過程中創建進度條實時顯示。
        // 顯示進度條
        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('上傳中'+ Math.round(percentage * 100)  + '%' );
            if(Math.round(percentage * 100) == 100)
            {
                $li.find('p.state').text('即將完成');
            }

            // $percent.css( 'width', percentage * 100 + '%' );
        });

        // 監聽上傳成功
        uploader.on( 'uploadSuccess', function( file ) {
            
            $( '#'+file.id ).find('p.state').text('已上傳');
        });
        // 監聽上傳失敗
        uploader.on( 'uploadError', function( file ) {
            alert("上傳出錯")
            $( '#'+file.id ).find('p.state').text('上傳出錯');
        });
        // 監聽上傳完成,不論失敗還是成功
        uploader.on( 'uploadComplete', function( file ) {
            $( '#'+file.id ).find('.progress').fadeOut();
            console.log(uploader.getStats());
        });
        $('#ctlBtn').click(function(){
            uploader.upload(); // 手動上傳
        })
        $('#resetBtn').click(function(){
            alert(666)
            uploader.retry(); // 重新上傳
        })

    })



</script>
</html>

  • 上面的代碼大概就是這么個效果,先選擇文件,可以選擇多個,選擇后進入隊列的過程中會生成md5, 不需要的可以去掉。
  • 然后可以點擊上傳按鈕, 進行上傳了,可以看下network,分成了很多請求。
  • 上傳失敗可以點擊重試,僅上傳失敗的文件,而且從失敗的那個分片開始上傳,跟斷點續傳一樣,只不過這里是上傳而不是下載。
  • 當可以正常上傳后再看此條注釋:此時會發現一個問題,偶爾有時后台合並文件時找不到文件,因為分片上傳時並發上傳,最后一個切片上傳完成時可能第一個切片還沒有上傳完,故此找不到文件,無法合並。解決該問題很簡單,將后台php 接收分片 與 合並分片 的代碼分成兩部分,將合並分片的代碼單獨拿出來,其余代碼不變,前端監聽 'uploadSuccess' 時,代表分片已經全部上傳完畢,此時再調用合並分片的代碼即可。

PHP的后端處理, 作為測試demo, 可以將所有php代碼(2個class)放一個文件中

<?php

/**
 * 文件上傳
 * User: LiZheng  271648298@qq.com
 * Date: 2019/9/20
 */
class Upload
{
    /**
     * 視頻上傳接口, 前端的  server 填寫此接口的地址即可。
     * User: LiZheng  271648298@qq.com
     * Date: 2019/9/23
     */
    public function video(){
        //file_put_contents("d:/lizheng.log", "\n\n"."files".print_r($_FILES,true),8);
        //file_put_contents("d:/lizheng.log", "\n\n"."POST".print_r($_POST,true),8);
        //file_put_contents("d:/lizheng.log", "\n\n"."換行".print_r("123",true),8);

        /**
         * 接收參數
         */
        //根據上下文的不同執行不同的操作,存放到不同的目錄(后期可以執行不同的操作,比如生成多個尺寸的縮略圖),
        // 這里context 和from 字段是和前台對應的,以及生成存儲地址時也用了, 如果修改請注意一起修改。
        $post['context'] = $_POST['context'];//1是用戶上傳,2是設備自動上傳
        $post['from'] = $_POST['from'];      //來源
        $post['id'] = $_POST['id'];
        $post['name'] = $_POST['name'];
        $post['type'] = $_POST['type'];
        $post['lastModifiedDate'] = $_POST['lastModifiedDate'];
        $post['size'] = $_POST['size'];
        $post['md5'] = $_POST['md5'];
        $post['chunks'] = $_POST['chunks'];
        $post['chunk'] = $_POST['chunk'];

        $data = array();
        //前台 file -> input框中的 name。 $mark[0]即后台 $_FILE數組的最外層索引
        $mark = array_keys($_FILES);
        // 上傳文件的類型
        $fileType = $this->getType($post['type']);
        // 允許的上傳類型, 則繼續 //允許的上傳視頻類型
        if(!in_array($fileType, array('mp4', 'wma', 'avi', 'rm', 'rmvb', 'flv', 'mpg', 'mov', 'mkv'))) {
            echo "不支持的文件類型"; exit;  // 輸出錯誤信息,請自定義
        }

        // 根據上下文和文件MD5值,獲取一個臨時的存儲路徑,用於存放chunks, 7天清理一次
        $tmpPath = $this -> getTmpPath($post);

        // 實例化文件上傳類,並初始化, 主要用於文件切片的暫時存儲, 當所有文件切片上傳后進行合並以及刪除所有的文件切片
        $upload = new Chunk($mark[0], $tmpPath);
        // 上傳並接收分片文件
        $data['video_chunk_url'] = $upload -> uploadChunkFile($post);

        // 判斷是否每個分片都上傳成功
        if(is_array($data['video_chunk_url'])) {
            //如果是數組,則說明是錯誤, 直接返回錯誤信息
            $this -> ajax_error(ERR_NORMAL, $data['video_chunk_url']['reason']); exit;
        }else if(!$data['video_chunk_url']) {
            $this -> ajax_error(ERR_NORMAL, '文件'.$post['chunk'].'上傳失敗'); exit;
        }

        // 判斷是否最后一個分片, 如果是, 則合並分片
        if($post['chunk'] == $post['chunks']-1){
            // 根據上下文不同獲取文件真正存儲的路徑
            $path = $this ->getVideoPath($post['context'], $post['from']);
            // 合並分片文件,存儲到真正的存儲地址, 並刪除無用的分片文件。
            $data['video_url'] = $upload -> mergerChunk($post, $tmpPath, $path);
            if(!$data['video_url'])
            {
                $this -> ajax_error(ERR_NORMAL, '合並文件分片時出錯!');
            }


            // TODO  生成封面圖和縮略圖
            // 如需生成封面圖, 可使用 ffmpeg, 需要下載該軟件,建議直接使用命令調用該軟件, 雖然有 php-ffmpeg插件,但貌似不支持php7,
            // 該軟件同時支持 視頻 和 圖片 生成縮略圖,下載,配置好環境變量后,只需執行一條命令即可。


            // 進行路徑裁剪,去掉/media_space, 相對路徑(/media_space 我作為文件服務器存放文件的根目錄,所有靜態文件通過nginx指到此目錄)
            $data['video_url'] = substr($data['video_url'], strpos($data['video_url'],'media_space/')+12);
            //拼接存放圖片的服務器域名    絕對路徑
            $data['video_url_host'] = PIC_HOST.$data['video_url'];
            $this -> ajax_succ(array('data' => array(
                'video_upload' => $data['video_url'],
                'video_show' => $data['video_url_host']
            )));
        }else
        {
            $this -> ajax_succ(array('data' => array()));
        }

    }

    /**
     * 從$_FILE中的type中獲取文件類型;   比如由 image/jpeg 得到 jpeg
     * @param $type
     * @return bool|string
     * User: LiZheng  271648298@qq.com
     * Date: 2018/12/15
     */
    private function getType($type){
        return substr($type,strpos($type,'/') + 1);
    }



    /**
     * 根據不同上下文選擇不同的存儲路徑
     * @param $context     圖片上下文
     * @param $from        圖片來源,來自哪個平台應用
     * @return bool|string
     * User: LiZheng  271648298@qq.com
     * Date: 2018/12/15
     */
    private function getPath($context, $from){
        //服務器類型, WIN是本地,否則linux
        $root_dir = strtoupper(substr(PHP_OS,0,3))==='WIN'?'d:/media_space':'/media_space';
        //是否存在圖片根目錄,不存在則創建
        if(!is_dir($root_dir))
        {
            //權限是否OK
            mk_dir($root_dir, 0777, true) && chmod($root_dir, 0777);
        }

        //根據來源,分組
        if(!$from)
        {
            $path = $root_dir.'/common';  //未標明來源from,則放到公共的common文件夾下
        }else
        {
            $path = $root_dir.'/'.$from;  //根據應用的不同,進行區分
        }

        //根據圖片用途分組,並根據日期歸類
        switch ($context)
        {
            case 0:
                $path .= '/picture/'.date('Y-m-d',time());
                break;
            case 1:
                $path .= '/avatar/'.date('Y-m-d',time());
                break;
            case 2:
                $path .= '/picture/'.date('Y-m-d',time());
                break;
            case 3:
                $path .= '/license/'.date('Y-m-d',time());
                break;
            case 4:
                $path .= '/idcard/'.date('Y-m-d',time());
                break;
            default:
                $path .= 'picture/'.date('Y-m-d',time());
                break;
        }
        return $path;
    }

    /**
     * 根據不同上下文選擇不同的存儲路徑
     * @param $context
     * @param $from
     * @return string
     * User: LiZheng  271648298@qq.com
     * Date: 2019/9/25
     */
    private function getVideoPath($context, $from){
        //服務器類型, WIN是本地,否則linux
        $root_dir = strtoupper(substr(PHP_OS,0,3))==='WIN'?'d:/media_space':'/media_space';
        //是否存在圖片根目錄,不存在則創建
        if(!is_dir($root_dir))
        {
            //權限是否OK
            mk_dir($root_dir, 0777, true) && chmod($root_dir, 0777);
        }

        //根據來源,分組
        if(!$from)
        {
            $path = $root_dir.'/common';  //未標明來源from,則放到公共的common文件夾下
        }else
        {
            $path = $root_dir.'/'.$from;  //根據應用的不同,進行區分
        }

        //根據圖片用途分組,並根據日期歸類
        switch ($context)
        {
            case 0:
                $path .= '/video/'.date('Y-m-d',time());
                break;
            case 1:
                $path .= '/upload/'.date('Y-m-d',time());
                break;
            case 2:
                $path .= '/device/'.date('Y-m-d',time());
                break;
            default:
                $path .= 'video/'.date('Y-m-d',time());
                break;
        }
        return $path;
    }

    /**
     * 根據不同上下文生成臨時存放chunks的目錄
     * @param $post
     * @return string
     * User: LiZheng  271648298@qq.com
     * Date: 2019/9/25
     */
    private function getTmpPath($post){
        //服務器類型, WIN是本地,否則linux
        $root_dir = strtoupper(substr(PHP_OS,0,3))==='WIN'?'d:/media_space':'/media_space';
        //是否存在圖片根目錄,不存在則創建
        if(!is_dir($root_dir))
        {
            //權限是否OK
            mk_dir($root_dir, 0777, true) && chmod($root_dir, 0777);
        }

        //根據來源,分組
        if(!$post['from'])
        {
            $path = $root_dir.'/tmp/common';  //未標明來源from,則放到公共的common文件夾下
        }else
        {
            $path = $root_dir.'/tmp/'.$post['from'];  //根據應用的不同,進行區分
        }
        //根據圖片用途分組,並根據日期歸類
        switch ($post['context'])
        {
            case 0:
                $path .= '/video/';
                break;
            case 1:
                $path .= '/upload/';
                break;
            case 2:
                $path .= '/device/';
                break;
            default:
                $path .= 'video/';
                break;
        }
        $path .= $post['md5'];
        return $path;
    }

    public function getThumb($path,$with,$height,$_path,$size,$type){
        $jpgResize = new lib_resizeimage($path, $with, $height, false, $_path.'_'.$size.'_'.$with.'_'.$height.'.'.$type);
        return $_path.'_'.$size.'_'.$with.'_'.$height.'.'.$type;
    }

    /**
     * 自動判斷是否錯誤, 如果錯誤 -> 調用ajax錯誤輸出
     * @param $result
     * @param $return
     * @param $delete
     * @return bool  在結果是真的情況下,是否直接返回數據
     * User: LiZheng  271648298@qq.com
     * Date: 2018/12/25
     */
    public function ajax_validate($result, $return = false, $delete = 0)
    {
        if($result['result'] == 'fail')
        {
            if(0 === $delete)
            {
                $this -> ajax_error(ERR_NORMAL, $result['reason'], array()); exit;
            }else
            {
                $this -> ajax_error(ERR_NORMAL, '刪除失敗', array()); exit;
            }
        }
        if($return)
        {
            $this -> ajax_succ(array('msg' => '成功', 'data' => $result['info'])); exit;
        }
        //不會調用ajax_succ,因為返回格式差異化比較大,重用性差
        return true;
    }
    /**
     * ajax正確輸出
     * @param array $response  相應信息
     * @param array $response['data']  輸出數據
     * @param string $response['msg']  返回提示信息(非必須)
     * User: LiZheng  271648298@qq.com
     * Date: 2018/11/15
     */
    public function ajax_succ($response=array())
    {
        $result = array();
        $result['code'] = 200;
        if(!isset($response['msg']))
        {
            $result['msg'] = '';
        }
        $result = array_merge($result,$response);
        //插入日志
        //$this -> json_output($res);
        self::arrUrlEncode($result);
        $output['code'] = (int)$result['code'];
        echo urldecode ( json_encode ($result) );
        exit();
    }

    private static function arrUrlEncode(&$arr)
    {
        foreach ( $arr as $key => $value )
        {
            if(is_array($value))
            {
                self::arrUrlEncode($arr[$key]);
            }
            else
            {
                $arr[$key] = urlencode ( str_replace(
                    array("\r\n", "\r", "\n","\t", '\r\n'),
                    array('', '', '', '', '<br />'),
                    $value) );
            }
        }
    }

    /**
     * ajax錯誤輸出
     * @param int $code 錯誤狀態碼
     * @param string $msg 錯誤原因
     * @param array $detail 錯誤原因
     */
    public function ajax_error($code = ERR_NORMAL, $msg = '', $detail = array())
    {
        $result = array();
        $result['code'] = $code;
        $result['msg'] = $msg;
        $result['data'] = $detail;

        //插入日志
        //$this -> json_output($result);

        echo json_encode($result);
        exit;

    }
}


/**
 * @author	LiZheng
 * @todo 文件上傳類
 * @version	v1.0.0
 */
class Chunk
{
    protected $fileName;
    protected $maxSize;
    protected $allowMime;
    protected $allowExt;
    protected $uploadPath;
    protected $imgFlag;
    protected $videoFlag;
    protected $fileInfo;
    protected $error;
    protected $ext;
    protected $destination;
    protected $uniName;

    /**
     * lib_upload constructor.
     * @param string $fileName
     * @param string $tmpPath
     * @param bool $videoFlag
     * @param int $maxSize
     * @param array $allowExt
     * @param array $allowMime
     */
    public function __construct($fileName='file',$tmpPath='resources//chunk',$videoFlag=true,$maxSize=5242880,$allowExt=array('mp4','mpeg','webm'),$allowMime=array('application/octet-stream', 'video/mp4','video/mpeg','video/webm')){
        $this->fileName=$fileName;          // 文件數組的名稱
        $this->maxSize=$maxSize;            // 最大文件的尺寸
        $this->allowMime=$allowMime;        // 允許的mime類型
        $this->allowExt=$allowExt;          // 允許的后綴
        $this->uploadPath=$tmpPath;         // 上傳路徑(臨時)
        $this->videoFlag=$videoFlag;            // 某個標識
        $this->fileInfo=$_FILES[$this->fileName]; // 文件信息

    }
    /**
     * 檢測上傳文件是否出錯
     * @return boolean
     */
    protected function checkError(){
        if(!is_null($this->fileInfo)){
            if($this->fileInfo['error']>0){
                switch($this->fileInfo['error']){
                    case 1:
                        $this->error='超過了PHP配置文件中upload_max_filesize選項的值';
                        break;
                    case 2:
                        $this->error='超過了表單中MAX_FILE_SIZE設置的值';
                        break;
                    case 3:
                        $this->error='文件部分被上傳';
                        break;
                    case 4:
                        $this->error='沒有選擇上傳文件';
                        break;
                    case 6:
                        $this->error='沒有找到臨時目錄';
                        break;
                    case 7:
                        $this->error='文件不可寫';
                        break;
                    case 8:
                        $this->error='由於PHP的擴展程序中斷文件上傳';
                        break;

                }
                return false;
            }else{
                return true;
            }
        }else{
            $this->error='文件上傳出錯';
            return false;
        }
    }
    /**
     * 檢測上傳文件的大小
     * @return boolean
     */
    protected function checkSize(){

        if($this->fileInfo['size']>$this->maxSize){
            $this->error='上傳文件過大';
            return false;
        }
        return true;
    }
    /**
     * 檢測擴展名
     * @return boolean
     */
    protected function checkExt(){
        $this->ext=strtolower(pathinfo($this->fileInfo['name'],PATHINFO_EXTENSION));
        if(!in_array($this->ext,$this->allowExt)){
            $this->error='不允許的擴展名';
            return false;
        }
        return true;
    }
    /**
     * 檢測文件的類型
     * @return boolean
     */
    protected function checkMime(){
        if(!in_array($this->fileInfo['type'],$this->allowMime)){
            $this->error='不允許的文件類型';
            return false;
        }
        return true;
    }


    /**
     * 檢測是否是真實圖片
     * @return bool
     * User: LiZheng  271648298@qq.com
     * Date: 2019/9/25
     */
    protected function checkTrueImg(){
        if($this->imgFlag){
            if(!@getimagesize($this->fileInfo['tmp_name'])){
                $this->error='不是真實圖片';
                return false;
            }
            return true;
        }
    }
    /**
     * 檢測是否通過HTTP POST方式上傳上來的
     * @return boolean
     */
    protected function checkHTTPPost(){
        if(!is_uploaded_file($this->fileInfo['tmp_name'])){
            $this->error='文件不是通過HTTP POST方式上傳上來的';
            return false;
        }
        return true;
    }
    /**
     *顯示錯誤
     */
    protected function showError(){
        echo('<span style="color:red">'.$this->error.'</span>');
    }
    /**
     * 檢測目錄不存在則創建
     */
    protected function checkUploadPath(){
        if(!file_exists($this->uploadPath)){
            mkdir($this->uploadPath,0777,true);
        }
    }

    /**
     * 獲取chunk的序號
     * @param $post
     * @return mixed
     * User: LiZheng  271648298@qq.com
     * Date: 2019/9/25
     */
    protected function getChunkName($post)
    {
        return $post['chunk'];
    }
    /**
     * 產生唯一字符串
     * @return string
     */
    protected function getUniName(){
        return md5(uniqid(microtime(true),true));
    }

    /**
     * 上傳文件
     * @param $post
     * @return array|string
     * User: LiZheng  271648298@qq.com
     * Date: 2019/9/25
     */
    public function uploadChunkFile($post){
        if($this->checkError()&&$this->checkSize()&&$this->checkExt()&&$this->checkMime()&&$this->checkHTTPPost()){
            $this->checkUploadPath();  //檢測目錄不存在則創建
            $this->uniName=$this->getChunkName($post);  //產生唯一chunk名稱
            $this->destination=$this->uploadPath.'/'.$this->uniName.'.'.$this->ext;
            if(@move_uploaded_file($this->fileInfo['tmp_name'], $this->destination)){
                return  $this->destination;
            }else{
                $this->error='文件移動失敗';
                return array('result'=>'fail','reason'=>$this->getError());
            }
        }else{
            return array('result'=>'fail','reason'=>$this->getError());
        }
    }

    /**
     * 合並視頻分片
     * @param $post
     * @param $tmpPath
     * @param $path
     * @return string
     * User: LiZheng  271648298@qq.com
     * Date: 2019/9/25
     */
    public function mergerChunk($post, $tmpPath, $path)
    {
        $video_url = $path.'/'.$post['name'];
        if(!file_exists($path)){
            mkdir($path,0777,true);
        }
        file_put_contents($video_url, file_get_contents($tmpPath."/0".'.'.$this->ext));
        for ($i=1; $i<$post['chunks'];$i++) {
            file_put_contents($video_url, file_get_contents($tmpPath.'/'.$i.'.'.$this->ext), 8);
        }
        if($this->delDirAndFile($tmpPath)) {
            return $video_url;
        }else{
            return false;
        }
    }

    public function getError()
    {
        return $this->error;
    }


    public function getFilename()
    {
        return $this->uniName.'.'.$this->ext;
    }

    /**
     * 循環刪除目錄和文件函數
     * @param $dirName
     * @return bool
     * User: LiZheng  271648298@qq.com
     * Date: 2019/9/25
     */
    public function delDirAndFile( $dirName )
    {
        // 打開目錄句柄
        if ( $handle = opendir( "$dirName" ) ) {
            // 循環讀取目錄
            while ( false !== ( $item = readdir( $handle ) ) ) {
                if ( $item != "." && $item != ".." ) {
                    // 判斷是否是目錄
                    if ( is_dir("$dirName/$item") ) {
                        // 如果是目錄,遞歸調用本函數
                        delDirAndFile("$dirName/$item");
                    } else {
                        // 刪除文件
                        if( unlink( "$dirName/$item" ) )echo "成功刪除文件: $dirName/$item \n";
                    }
                }
            }
            // 關閉目錄句柄
            closedir($handle);
            // 刪除目錄
            if(rmdir($dirName)){
                return true;
            }
        }
        return false;
    }
}

  • 至此,就完成了,效果如下圖所示

注意

  • 1、一個是context 和 from 參數的問題,大家運行demo的時候,可以先使用這兩個參數,demo運行無誤后,再使用自己的參數,因為這兩個參數在生成存儲地址的時候用到了
  • 2、我本地是windows, 所以在地址這塊,僅進行了windows和linux系統的設置,如果是mac, 請搜索getTmpPath 函數,自行設置
  • 3、記憶中好像開啟了fileinfo擴展, windows直接在php.ini中開啟重啟即可。
  • 4、上傳文件的過程中,臨時存放分片的目錄是有文件的,當合並所有分片后,該目錄以及所有文件都會被刪除。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM