PHP大文件分片上传、断点续传、上传进度条

目录

【前言】

分片上传原理

断点续传原理

进度条显示

用到的工具及引入的附件

html代码

js代码

PHP代码


【前言】

因公司有大文件上传后处理的需求,但是nginx和php都有做文件大小的限制,所以要用到分片上传;因为文件比较大,可能会遇到 网络断开、误关闭页面、服务端暂不可用等问题,所以要实现断点续传的功能。

分片上传原理

前端将大文件按照固定大小切分,每次上传单个分片二进制流,最后一片上传完毕后后端对文件进行分片合并。

断点续传原理

服务端记录某个文件的上传进度; 

好多人都是每次上传分片都检测下上传进度,没必要;

我的例子中是首次上传时先从后端获取分片位置【position】,然后从上次上传position开始继续上传,中间有任何异常,都会重置是否获取 position的变量,再重新获取上传进度,接着上传;

这样会减少很多没必要的请求,不会有任何问题。

进度条显示

很简单, 已上传的分片进度 / 总分片数 * 100 

用到的工具及引入的附件

redis 、jquery

jquery-1.8.0.min.js  和 md5.js  都是公用的,网上一查就有,我没找到上传附件的地方...

html代码

<html>
    <head>
        <script src="html/js/jquery-1.8.0.min.js"></script>
        <script src="html/js/md5.js"></script>
    </head>
    <body>
    <input type="file" id="zipfile-inputEl">
    <button id="upload">点击上传</button>
    <br>
    <p style="display: inline">
        上传进度:
        <p id="progressBar" style="display: inline">0</p>
        %
    </p>
    </body>
</html>

js代码

/**
     * 分片上传相关变量
     */
    var	nowIndex = 0;          // 计算分片上传文件切片设置,默认从0开始
    var getPosition = true;    // 获取分片上传位置,默认获取(断点续传)

    /**
     * 文件分片上传
     * file    文件对象
     * filemd5 整个文件的md5值
     */
    function shardToUpload(file, fileMd5)
    {
        var fileName   = file.name;        //文件名
        var fileSize   = file.size;        //总大小
        var shardSize  = 2 * 1024 * 1024;                 // 每个分片的大小(2M)
        var shardTotal = Math.ceil(fileSize / shardSize); // 总分片数
        var fileSuffix = fileName.substr(fileName.lastIndexOf('.')+1); //文件后缀(例:zip)

        // 进度条展示
        $('#progressBar').html(parseInt(nowIndex / shardTotal * 100));

        //计算每一片的起始与结束位置
        var start = nowIndex * shardSize,
            end = Math.min(fileSize, start + shardSize);

        //构造一个表单
        var form = new FormData();

        form.append('file_md5', fileMd5);
        form.append('file_name', fileName);
        form.append('file_suffix', fileSuffix);    // 文件后缀
        form.append('shard_total', shardTotal);    // 总分片数
        form.append('shard_index', nowIndex);      // 当前是第几片(0~shardCount-1)

        // 获取分片上传位置
        if (getPosition) {
            form.append('get_position', '1');
        } else {
            // 按大小切割文件段
            var tmp_blob = file.slice(start, end);
            form.append('tmp_file', tmp_blob);
        }


        $.ajax({
            url: '/logistics/basicfile/php/good.php?do=shardToUpload',
            type: 'POST',
            data: form,
            dataType: 'json',
            processData: false,  //很重要,告诉jquery不要对form进行处理
            contentType: false,  //很重要,指定为false才能形成正确的Content-Type
            success: function(data){
                console.log(data);
                if (data.success === true) {
                    if (getPosition === true) {
                        // 获取之前文件分片上传的位置,断点续传
                        nowIndex = data.position;
                        getPosition = false;
                    } else {
                        nowIndex++;
                    }

                    // 分片上传完毕
                    if (nowIndex >= shardTotal) {
                        alert('最后一片已经上传完毕');
                        // 重置分片上传变量
                        nowIndex = 0;
                        getPosition = true;
                        $('#progressBar').html('100');
                        return true;
                    }
                    shardToUpload(file, fileMd5);
                } else {
                        // 重置分片上传变量
                        nowIndex = 0;
                        getPosition = true;
                        alert('上传失败,可点击继续上传,支持断点续传~');
                        return false;
                }
            },error: function() {
                alert("服务器出错!");
            }
        });

        // 获取分片md5方式
        // var r = new FileReader();
        // r.readAsBinaryString(data);
        // $(r).load(function(e) {
            // form.append('shard_md5', hex_md5(e.target.result));
        // })
    }

    $('#upload').click(function() {
        // 文件对象
        var file = $("#zipfile-inputEl")[0].files[0];

        // 验证文件大小
        var limitFileSizeByM = 1000; // 限制上传大小为1G
        var nowFileSizeByM = Math.ceil(file.size / 1024 / 1024); // 当前上传文件大小
        if (file.size > (limitFileSizeByM * 1024 * 1024)) {
            alert('当前上传文件约' + nowFileSizeByM + 'M, 请上传小于'+ limitFileSizeByM + 'M的图片文件!');
            return false;
        }

        var r = new FileReader();
        r.readAsBinaryString(file);
        $(r).load(function(e) {
            // 上传文件操作
            var bolb = e.target.result;
            var fileMd5 = hex_md5(bolb);  // 整个文件的md5值

            // 首次调用,先检测分片上传位置
            getPosition = true;
            shardToUpload(file, fileMd5);
        });
    });

PHP代码

/**
 * 大文件分片上传,支持断点上传
 * 文件上传成功后,可根据 {$fileMd5}_path key 去redis中获取到文件路径
 * @param int $_POST['get_position'] 是否获取分片上传(true:获取上次上传分片位置;false:上传文件分片)
 * @param int $_POST['shard_total']  总分片数
 * @param int $_POST['shard_index']  分片偏移量(0 ~ $shardTotal-1)
 * @param int $_POST['file_md5']     整个文件内容的md5值
 * @param int $_POST['file_suffix']  文件后缀(因为客户端每次上传的是分片 所以服务端无法知道整个文件的后缀 最后合并分片时需要)
 * @param int $_FILES['tmp_file']    二进制分片文件
 */
function shardToUploadAction()
{
    $redis       = AWRedis::getInstance();
    $getPosition = isset($_POST['get_position']) ? $_POST['get_position'] : 0;// 是否获取分片上传位置
    $shardTotal  = $_POST['shard_total'];    // 总分片数
    $shardIndex  = $_POST['shard_index'];    // 分片偏移量(0 ~ $shardTotal-1)
    $fileMd5     = $_POST['file_md5'];       // 整个文件md5值
    $fileSuffix  = $_POST['file_suffix'];    // 文件后缀(例:zip)
    $filePositionKey = $fileMd5.'_position'; // 文件分片上传位置key

    $returnData = [
        'success'  => true,
        'msg'      => '',
        'position' => $shardIndex,
        'file_path'=> ''
    ];

    try {
        // 首先判断文件是否之前已经生成【{$fileMd5}_path => 文件路径</opt/lampp/logs/20211220/upload/d41d8cd98f00b204e9800998ecf8427e.zip>】
        $redisFilePath = $redis->get($fileMd5.'_path');
        if ($redisFilePath) {
            $returnData['file_path'] = $redisFilePath;
            echo aw_json_encode($returnData);die;
        }

        // 获取分片位置(断点续传功能)
        if ($getPosition) {
            $filePosition = $redis->get($filePositionKey);
            empty($filePosition) && $filePosition = 0;
            // 前端下次应该上传到上次上传分片位置的下个分片
            $returnData['position'] = $filePosition > 0 ? $filePosition +1 : 0;
            echo aw_json_encode($returnData);die;
        }

        if ($_FILES['tmp_file']) {
            // 获取文件第一次上传时间
            $indexFirstUploadDateKey = $fileMd5 . '_date';
            $firstUploadDate = $redis->get($indexFirstUploadDateKey);
            empty($firstUploadDate) && $firstUploadDate = date('Ymd');

            // 生成上传文件的路径信息,按天生成,方便后期清理历史过期文件
            $tempSavePath = '/opt/lampp/logs/'. $firstUploadDate . '/upload_tmp/' . $fileMd5 . '/';
            $lastSavePath = '/opt/lampp/logs/'. $firstUploadDate . '/upload/';
            // 验证路径是否存在,不存在则创建目录
            if (!is_dir($tempSavePath)) {
                $bool = mkdir($tempSavePath,0777,true);
                if ($bool === false) {
                    // 创建文件夹失败
                    throw new AWException('创建分片所在目录失败【'.$tempSavePath.'】');
                }
            }
            $indexFileName = $fileMd5.'_'.$shardIndex;
            $fullFilePath = $tempSavePath . $indexFileName;
            $bool = move_uploaded_file($_FILES['tmp_file']['tmp_name'], $fullFilePath);
            if ($bool === false) {
                throw new AWException('移动分片到临时目录失败'.$_FILES['tmp_file']['tmp_name'].'->【'.$fullFilePath.'】');
            }

            // 文件第一个分片上传后记录上传日期,避免后续分片上传跨天,保存位置错误(有效期:2h)
            if ($shardIndex == 0 && $shardIndex+1 != $shardTotal) {
                $redis->set($indexFirstUploadDateKey, date('Ymd'), 2*60*60);
            }

            // 分片上传成功后, 记录 position , 用来实现断点续传(有效期:10min)
            $redis->set($filePositionKey, $shardIndex, 10*60);

            // 分片上传完毕,进行合并操作
            if ($shardIndex+1 == $shardTotal) {

                //创建要合并的最终文件资源
                $finalFile = $lastSavePath.$fileMd5.'.'.$fileSuffix;
                if (!is_dir($lastSavePath)) {
                    $bool = mkdir($lastSavePath,0777,true);
                    if ($bool === false) { // 创建文件夹失败
                        throw new AWException('创建文件最终保存目录失败【'.$lastSavePath.'】');
                    }
                }

                $finalFileHandler = fopen($finalFile, 'wb');

                // 合并文件前,先删除分片 position;避免合并有问题后,后续重试时上传分片有误
                $redis->del($filePositionKey);

                /**
                 * 这里涉及高并发对文件进行写入,暂时使用usleep函数来防止写入丢失;
                 * 后期如果有写入丢失情况,请使用文件锁,使用文件锁时请考虑死锁问题
                 **/
                for ($i = 0;$i < $shardTotal; $i++) {
                    $tempIndexFile   = $tempSavePath.$fileMd5.'_'.$i;
                    $tempFileHandler = fopen($tempIndexFile, 'rb');
                    $tempFileContent = fread($tempFileHandler, filesize($tempIndexFile));
                    fwrite($finalFileHandler, $tempFileContent);
                    unset($tempFileContent);
                    fclose($tempFileHandler);        // 销毁分片文件资源
                    unlink($tempIndexFile);          // 删除已经合并的分片文件(todo:是否可以放到最后再进行删除)
                    usleep(10000);       // 延迟执行10毫秒
                }
                fclose($finalFileHandler);

                // redis保存文件最终路径,之前上传过就直接返回文件实际路径,保留24h
                $redis->set($fileMd5.'_path', $finalFile, 60*60*24);
                $returnData['file_path'] = $finalFile;
            }
        }
    } catch (AWException $e) {
        $returnData['success'] = false;
        $returnData['msg']     = $e->getMessage();
        // 记录失败日志,方便线上排查
        (new Apilog('shardToUpload'))->writeLog($e->getMessage());
    }

    echo aw_json_encode($returnData);
}


/**
 * 大于5.3版本的时候中文不转义
 * @param $data
 * @return string
 */
function aw_json_encode($data){
    if(version_compare(PHP_VERSION, "5.4", ">")){
        return json_encode($data,JSON_UNESCAPED_UNICODE);
    }
    return json_encode($data);
}

【end】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值