zoukankan      html  css  js  c++  java
  • 视频大文件分片上传(使用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=" 150px; text-overflow: ellipsis">' + file.name + '</h4>' +
                    '<p class="state" style="position: absolute; top: 0px; left: 120px;  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=" 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", "
    
    "."files".print_r($_FILES,true),8);
            //file_put_contents("d:/lizheng.log", "
    
    "."POST".print_r($_POST,true),8);
            //file_put_contents("d:/lizheng.log", "
    
    "."换行".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("
    ", "
    ", "
    ","	", '
    '),
                        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 
    ";
                        }
                    }
                }
                // 关闭目录句柄
                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、上传文件的过程中,临时存放分片的目录是有文件的,当合并所有分片后,该目录以及所有文件都会被删除。
  • 相关阅读:
    时间复杂度和空间复杂度
    七、vue计算属性
    六、vue侦听属性
    四、vue派发更新
    五、vue nextTick
    三、vue依赖收集
    二、vue响应式对象
    递归
    链表
    TypeScript类型定义文件(*.d.ts)生成工具
  • 原文地址:https://www.cnblogs.com/lz0925/p/11587236.html
Copyright © 2011-2022 走看看