PHP超大文件上传到OSS存储桶,以及断点续传。

流程:

在这里插入图片描述

配置:

这里按照上传文件最大2G来配置的,根据自己的需要来做修改!

Nginx:
在这里插入图片描述
在这里插入图片描述
PHP:
在这里插入图片描述


前端:

这里以 layui 为例
Html:
使用 sparkmd5 获取文件内容的md5,用于保证上传文件的唯一性。

<script src="__STATIC__/admin/js/sparkmd5/sparkmd5.js?v={$version}"></script>
<div class="layui-input-block layui-upload">
       <button type="button" class="layui-btn layui-btn-normal" id="chooseList">选择文件</button>
       <div class="layui-upload-list">
           <table class="layui-table">
               <colgroup>
                   <col width="30%">
                   <col width="10%">
                   <col width="30%">
                   <col width="30%">
               </colgroup>
               <thead>
               <tr><th>文件名</th>
                   <th>大小</th>
                   <th>上传进度</th>
                   <th>操作</th>
               </tr></thead>
               <tbody id="uploadList"></tbody>
           </table>
       </div>
 </div>

页面效果:
在这里插入图片描述

JS:

layui.use(['form', 'upload', 'element'], function () {

    var form = layui.form;
    var upload = layui.upload;
    var element = layui.element;
    var $ = layui.$;
    var progressTimer = 211;    //定时器
    element.render('progress'); //渲染新加的进度条组件

    upload.render({

        elem: '#chooseList',
        elemList: $('#uploadList'), //列表元素对象
        url: '../Ajax/multiupload', //处理上传文件接口
        accept: 'file',
        auto: false,

        choose: function (obj) {

            var data = this.data;
            var files = this.files = obj.pushFile(); //将每次选择的文件追加到文件队列
            var partSize = 1024 * 1024 * 2; //每片文件大小

            for (var key in files) {
                if ($('#upload-'+key).val() == undefined) {

                    var fileName = files[key].name;

                    var fileExt = fileName.substr(fileName.lastIndexOf('.') + 1);
                    fileName = fileName.substr(0, fileName.lastIndexOf('.'));

                    var that = this;
                    var file_size = (files[key].size / 1024).toFixed(2) + 'KB';
                    if (files[key].size > 1000 * 1000) {
                        file_size = (files[key].size / 1024 / 1024).toFixed(2) + 'MB';
                    }
                    if (files[key].size > 1000 * 1000 * 1000) {
                        file_size = (files[key].size / 1024 / 1024 / 1024).toFixed(2) + 'GB';
                    }
                    var tr = $(['<tr id="upload-' + key + '">'
                        , '<td id="file-name-' + key + '">' + files[key].name + '</td>'
                        , '<td>' + file_size + '</td>'
                        , '<td><div class="layui-progress" lay-filter="progress-demo-' + key + '"><div class="layui-progress-bar" lay-percent=""></div></div></td>'
                        , '<td>'
                        , '<input type="hidden" class="status" id="status-' + key + '" value="1"/>'
                        , '<input type="hidden" id="part-num-' + key + '" value="1"/>'
                        , '<input type="hidden" id="upload-id-' + key + '" value=""/>'
                        , '<input type="hidden" id="contents-' + key + '" value=""/>'
                        , '<input type="hidden" id="part-total-' + key + '" value=""/>'
                        , '<span class="layui-icon layui-icon-loading-1 layui-anim layui-anim-rotate layui-anim-loop" id="icon-loop-' + key + '"></span>'
                        , '<span class="layui-icon layui-icon-ok-circle" hidden id="icon-ok-' + key + '"></span>'
                        , '<span class="layui-icon layui-icon-close" hidden id="icon-close-' + key + '"></span>'
                        , '<a class="layui-btn layui-btn-xs layui-btn-danger" id="upload-cancel-' + key + '">取消</a>'
                        , '<a class="layui-btn layui-btn-xs re-upload" id="re-upload-' + key + '">重试</a>'
                        , '<a class="layui-btn layui-btn-normal layui-btn-xs" id="set-title-' + key + '">生成标题</a></span>'
                        , '</td>'
                        , '</tr>'].join(''));
                    that.elemList.append(tr);

                    //单个重传
                    tr.find('#re-upload-' + key).on('click', function () {
                        $('#re-upload-'+key).hide();
                        $('#status-' + key).val('1');
                        getmd5(files[key], partSize).then(e => {
                            if (e != undefined) {
                                multiupload(progressTimer, data, files[key], partSize, key, fileName, fileExt, e, obj);
                            }
                        })
                    });

                    //取消
                    tr.find('#upload-cancel-' + key).on('click', function () {
                        $('#status-'+key).val('-1');
                        $('#upload-' + key).remove();
                        delete files[key];
                        layer.msg('已删除该队列!');
                    });

                    //设置标题
                    tr.find('#set-title-' + key).on('click', function () {
                        $("#title").val($('#file-name-' + key).html());
                    });

                    $('.re-upload').hide();

                    getmd5(files[key], partSize).then(e => {
                        if (e != undefined) {
                            multiupload(progressTimer, data, files[key], partSize, key
                                , fileName, fileExt, e, obj);
                        }
                    })

                }
            }

        },
        done: function (res) {

            //分片上传
            if (res.status == 1) {

                var page = res.part_num;
                var totalPage = res.part_total;
                element.progress('progress-demo-'+res.upload_index, Math.ceil(page * 100 / totalPage) + '%');
                page = parseInt(page) + 1;
                $('#part-num-'+res.upload_index).val(page);
                $('#status-'+res.upload_index).val(res.status);
                $('#upload-id-'+res.upload_index).val(res.upload_id);
                $('#contents-'+res.upload_index).val(res.contents);
                $('#part-total-'+res.upload_index).val(res.part_total);
                $('#icon-loop-'+res.upload_index).show();
                $('#icon-close-'+res.upload_index).hide();

                //上传完成
            } else if (res.status == 2) {
                //取消
                $('#upload-cancel-' + res.upload_index).on('click', function () {
                    $.ajax({
                        type: 'get',
                        url: './delFile',
                        data: {
                            doc_id: res.uploadfile_id
                        },
                        success: function () {
                            layer.msg('上传已取消!');
                        }
                    })
                });
                element.progress('progress-demo-'+res.upload_index, '100%');
                $('#status-'+res.upload_index).val(res.status);
                $('#re-upload-'+res.upload_index).hide();
                $('#icon-ok-'+res.upload_index).show();
                $('#icon-loop-'+res.upload_index).hide();
                layer.msg('上传成功');
                delete this.files[res.upload_index];
                //上传错误
            } else {
                this.error(res.upload_index);
            }

        },

        error: function (index) { //错误回调
            $('#icon-loop-'+index).hide();
            $('#icon-close-'+index).show();
            $('#status-'+index).val('-1');
            $('.re-upload').show();
            $('.status[value=2]').siblings('.re-upload').hide();
            layer.alert("上传失败,请检查网络后重试");
        }
    });
});

/**
 * 分片上传
 */
function multiupload(progressTimer,data,file,partSize,key,fileName,fileExt,hash_name,obj) {
    progressTimer = setInterval(function () {
    	//该文件上传的分片数
        var page = parseInt($('#part-num-'+key).val());
        //该文件的上传状态
        var status = $('#status-'+key).val();
        //上传状态为 2(文件上传完成)或者状态为 -1(上传错误)时关掉定时器
        if (parseInt(status) == 2 || parseInt(status) == -1) {
            clearInterval(progressTimer);
        } else {
        	//上传状态为 1(文件分片上传正常)
            if (status == 1) {
            	//修改状态为 0(等待),避免定时器不停的跑导致重复上传
                $('#status-'+key).val('0');
                data.file_name = fileName;   	//文件名称
                data.part_num = page;           //第几片
                data.file_ext = fileExt;     	//文件类型
                data.upload_index = key;        //文件上传时间下标(毫秒)
                data.file_size = file.size; 	//文件大小
                data.hash_name = hash_name;		//文件内容的md5
                obj.upload(key, file.slice((page - 1) * partSize, page * partSize));
            }
        }
    }, 100);
}

/**
 * 获取文件内容的md5
 * 直接复制就行
 * file 文件
 * chunkSize 分片大小
 */
function getmd5(file, chunkSize) {
    return new Promise((resolve, reject) => {
        let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
        let chunks = Math.ceil(file.size / chunkSize);
        let currentChunk = 0;
        let spark = new SparkMD5.ArrayBuffer();
        let fileReader = new FileReader();
        fileReader.onload = function(e) {
            spark.append(e.target.result);
            currentChunk++;
            if (currentChunk < chunks) {
                loadNext();
            } else {
                let md5 = spark.end();
                resolve(md5);
                //  console.log(md5);
            }
        };
        fileReader.onerror = function(e) {
            reject(e);
        };
        function loadNext() {
            let start = currentChunk * chunkSize;
            let end = start + chunkSize;
            if (end > file.size){
                end = file.size;
            }
            fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
        }
        loadNext();
    });
}

上传中效果图
在这里插入图片描述
上传成功效果图
在这里插入图片描述

上传失败效果图
在这里插入图片描述


PHP:

获取页面传过来的参数生成文件的md5名称。

//上传文件的md5名称(内容md5+文件名+文件类型+文件大小+登录id)
$data['upload_name'] = md5($data['hash_name'].$data['file_name'].$data['file_ext'].$data['file_size'].$data['user_id']);

把上传的分片文件保存到本地,注意框架封装的获取上传文件的详细参数方法可能会出错。

查询上传日志记录表是否存在该上传文件分片的记录。

//检查是否存在该文件
$upload_log = $system_upload_log->getDataByUploadName($data['upload_name'],$data['part_num']);

如果有该分片记录,则返回最新的一条记录,否则返回空数组。

/**
 * 根据md5名称获取数据
 * upload_name  上传的文件md5名称
 * part_num 	分片数
 */
public function getDataByUploadName($upload_name,$part_num){
   $data = $this->where(['upload_name' => $upload_name,'part_num' => $part_num])->select()->toArray();
   return $data ? $this->where(['upload_name' => $upload_name])->order('id desc')->find()->toArray() : $data;
}

如果有记录,则代表是续传,返回前端当前文件的上传记录

if ($upload_log){
  	$data['status'] = $upload_log['status'];        //上传状态
    $data['upload_id'] = $upload_log['upload_id'];  //上传id
    $data['contents'] = $upload_log['contents'];    //上级目录(20210909)
    $data['part_num'] = $upload_log['part_num'];    //日志记录的最后一片
    $data['part_total'] = $upload_log['part_total'];//分片总数
    return ['save' => true,'data' => $data];
}

一些判断

//上传id
$data['upload_id'] = $data['upload_id'] ? $data['upload_id'] : '';
//上级目录(20210909)
$data['contents'] = $data['contents'] ? $data['contents'] : date('Ymd');
//分片总数
$data['part_total'] = $data['part_total'] ? $data['part_total'] : ceil($data['file_size']/(1024*1024*2));
//计算最后一片分片的大小
$data['part_size'] = $data['part_num'] * (1024*1024*2) > $data['file_size']
    ? $data['file_size'] - (($data['part_num']-1) * (1024*1024*2)) : (1024*1024*2);
//是否为最后一片
if ($data['part_num'] == $data['part_total']){
    $data['status'] = 2;
    //获取该文件的所有分片上传到OSS成功的返回md5值(每一片都是一个md5)
    $data['etag'] = $system_upload_log->getEtagByUploadName($data['upload_name']);
}
//OSS的文件路径
$object = 'upload/'.$data['contents'].'/'.$data['upload_name'].'.'.$data['file_ext'];

在OssClient.php文件写一个新方法,参考Oss本身的上传方法 multiuploadFile

/**
 * 分片上传
 *
 * @param string $bucket bucket名称
 * @param string $object object名称
 * @param string $file 需要上传的本地文件的路径
 * @param array $options Key-Value数组
 * @return null
 * @throws OssException
 */
public function multiupload($bucket, $object, $file,$options = null)
{
	//校验bucket,options参数
    $this->precheckCommon($bucket, $object, $options);
    if (empty($file)) {
        throw new OssException("parameter invalid, file is empty");
    }
    //gbk转utf-8
    $uploadFile = OssUtil::encodePath($file);
    if (!isset($options[self::OSS_CONTENT_TYPE])) {
        $options[self::OSS_CONTENT_TYPE] = $this->getMimeType($object, $uploadFile);
    }

    if (!$options['upload_id']) {
        // 初始化
        $options['upload_id'] = $this->initiateMultipartUpload($bucket, $object,
            [   'Content-Type' => $options[self::OSS_CONTENT_TYPE],
                'partSize' => $options['part_size']
            ]);
    }

    // 上传的分片
    $up_options = array(
        self::OSS_FILE_UPLOAD => $uploadFile,       	//上传的文件
        self::OSS_PART_NUM => $options['part_num'],     //第几片
        self::OSS_SEEK_TO => 0,    						//开始字节(从这一片的第几个字节开始,因为我这里每一片都是单独的2M,所以给0)
        self::OSS_LENGTH => $options['part_size'],      //片的大小(2M)
        self::OSS_CHECK_MD5 => false,
    );

    //上传一片
    $options['part_num'] <= $options['part_total'] && $response_upload_part = $this->uploadPart($bucket, $object, $options['upload_id'], $up_options);

    //去掉返回值头尾的引号(返回的etag值是 ""xxxxxxxxxxxxxxxxxxxxxxxxx"")
    if (isset($options['etag'])){
        $options['etag'][] = substr($response_upload_part,1,-1);
    }else{
        $options['etag'] = substr($response_upload_part,1,-1);
    }

    //当是最后一片时,合并
    if ($options['status'] == 2){
        $uploadParts = array();
        foreach ($options['etag'] as $i => $value) {
            $uploadParts[] = array(
                'PartNumber' => ($i + 1),
                'ETag' => $value
            );
        }
        $completer = $this->completeMultipartUpload($bucket, $object, $options['upload_id'], $uploadParts);
        $options['url'] = $completer['info']['url'];
    }

    return $options;
}

分片上传成功后记得删除本地的分片文件,文件合并后会返回 url ,可以存到另一个已上传文件表中,把日志记录表的该文件记录删除。

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
阿⾥云 对象存储 SDK ⽰例 ⽂档版本:20190920 对象存储 SDK ⽰例 ⽂档版本:20190920 I 法律声明....................................................................................I 通⽤约定....................................................................................I 1 SDK ⽂档简介.........................................................................1 2 Java......................................................................................2 2.1 前⾔............................................................................................................2 2.2 安装............................................................................................................3 2.3 快速⼊⻔......................................................................................................4 2.4 初始化.........................................................................................................8 2.5 存储空间....................................................................................................12 2.6 请求者付费模式...........................................................................................17 2.7 单链接限速.................................................................................................19 2.8 存储空间标签..............................................................................................21 2.9 对象标签....................................................................................................23 2.9.1 设置对象标签...................................................................................23 2.9.2 获取对象标签...................................................................................32 2.9.3 删除对象标签...................................................................................32 2.9.4 对象标签和⽣命周期管理....................................................................33 2.10 服务器端加密............................................................................................ 35 2.11 上传⽂件..................................................................................

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值