记录一个大文件的分片上传,利用前端分片,对接阿里云oss

最近做了一个大文件分片上传的功能,记录下

1.首先是安装阿里云oss扩展

composer require aliyuncs/oss-sdk-php

去阿里云oss获取配置文件

AccessKey ID = ***
AccessKey Secret = ***
Bucket名称 = ***
Endpoint = ***

2.前端上传,对文件进行分片

<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">

    <div class="form-group">
        <label class="control-label col-xs-12 col-sm-2">{:__('选择本地文件')}:</label>
        <div class="col-xs-12 col-sm-8">
            <input type="file" id="fileInput">
            <div>
                <a href="#" onclick="startUpload()"><i class="fa fa-upload"></i>选择完点击上传(请等待上传完成)</a>
            </div>
            <div id="progress" style="margin-top:10px;"></div>
        </div>
    </div>
    <div class="form-group layer-footer">
        <label class="control-label col-xs-12 col-sm-2"></label>
        <div class="col-xs-12 col-sm-8">
            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
        </div>
    </div>
</form>

<script>
    let chunkSize = 5 * 1024 * 1024; // 分片大小5MB
    let uploadId = '';
    let objectName = '';
    let parts = [];

    const CHUNK_SIZE = 5 * 1024 * 1024; // 分片阈值5MB

    async function startUpload() {
        const file = document.getElementById('fileInput').files[0];
        if (!file) {
            layer.msg('请选择文件', {icon: 1});
        }

        // 根据文件大小选择上传方式
        if (file.size <= CHUNK_SIZE) {
            await directUpload(file);
        } else {
            await chunkedUpload(file);
        }
    }

    // 开始上传
    async function directUpload(file) {
        const formData = new FormData();
        formData.append('file', file);
        formData.append('file_name', file.name);

        // 显示进度条
        const progressBar = document.getElementById('progress');
        progressBar.innerHTML = '上传进度:0%';

        try {
            const res = await fetch('/api/directUpload', {
                method: 'POST',
                body: formData,
            });

            const data = await res.json();
            if (data.code === 1) {
                progressBar.innerHTML = '上传进度:100%';
                $("#c-name").val(data.name);
                $("#c-fullurl").val(data.fullurl);
                layer.msg('上传成功', {icon: 1});
            } else {
                throw new Error(data.msg);
            }
        } catch (error) {
            progressBar.innerHTML = '上传失败';
            console.error('直接上传失败:', error);
        }
    }

    async function chunkedUpload(file) {
        const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
        let uploadedChunks = 0;

        // 初始化分片上传
        const initRes = await fetch('api/initUpload', {
            method: 'POST',
            body: JSON.stringify({filename: file.name}),
            headers: {'Content-Type': 'application/json'}
        });
        const initData = await initRes.json();
        if (initData.code !== 1) return alert('初始化失败');

        const {uploadId, objectName} = initData;
        const parts = [];

        // 上传所有分片
        for (let i = 0; i < totalChunks; i++) {
            const start = i * CHUNK_SIZE;
            const end = Math.min(start + CHUNK_SIZE, file.size);
            const chunk = file.slice(start, end);

            const formData = new FormData();
            formData.append('part', chunk);
            formData.append('uploadId', uploadId);
            formData.append('objectName', objectName);
            formData.append('partNumber', i + 1);

            const uploadRes = await fetch('api/uploadPart', {
                method: 'POST',
                body: formData
            });
            const partData = await uploadRes.json();

            if (partData.code === 1) {
                parts.push({
                    PartNumber: partData.partNumber,
                    ETag: partData.etag
                });
                uploadedChunks++;

                // 更新进度
                const progress = (uploadedChunks / totalChunks * 100).toFixed(2);
                document.getElementById('progress').innerHTML = `上传进度:${progress}%`;
            }
        }

        // 合并分片
        const completeRes = await fetch('api/completeUpload', {
            method: 'POST',
            body: JSON.stringify({
                uploadId,
                objectName,
                parts: JSON.stringify(parts)
            }),
            headers: {'Content-Type': 'application/json'}
        });
        const completeData = await completeRes.json();

        if (completeData.code === 1) {
            $("#c-name").val(completeData.name);
            $("#c-fullurl").val(completeData.fullurl);
            layer.msg('上传成功', {icon: 1});
        } else {
            layer.msg('上传失败' + completeData.msg, {icon: 2});
        }
    }
</script>

2.后端控制器

<?php

namespace app\****;

use OSS\Core\OssException;
use OSS\OssClient;

class Attachment
{
    // 初始化分片上传
    public function initUpload()
    {
        $object = 'uploads/' . date('Ymd') . '/' . $this->request->post('filename');
        try {
            $ossClient = new OssClient(
                config('alioss.accessKeyId'),
                config('alioss.accessKeySecret'),
                config('alioss.endpoint')
            );
            $uploadId = $ossClient->initiateMultipartUpload(config('alioss.bucket'), $object);
            return json([
                'code' => 1,
                'uploadId' => $uploadId,
                'objectName' => $object
            ]);
        } catch (OssException $e) {
            return json(['code' => 0, 'msg' => $e->getMessage()]);
        }
    }

    // 上传分片
    public function uploadPart()
    {
        $data = $this->request->post();
        try {
            $ossClient = new OssClient(
                config('alioss.accessKeyId'),
                config('alioss.accessKeySecret'),
                config('alioss.endpoint')
            );
            $options = [
                OssClient::OSS_FILE_UPLOAD => $_FILES['part']['tmp_name'],
                OssClient::OSS_PART_NUM => $data['partNumber'],
                OssClient::OSS_CHECK_MD5 => true
            ];
            $etag = $ossClient->uploadPart(
                config('alioss.bucket'),
                $data['objectName'],
                $data['uploadId'],
                $options
            );
            return json([
                'code' => 1,
                'etag' => $etag,
                'partNumber' => $data['partNumber']
            ]);
        } catch (OssException $e) {
            return json(['code' => 0, 'msg' => $e->getMessage()]);
        }
    }

    // 完成上传
    public function completeUpload()
    {
        $data = $this->request->post();
        try {
            $ossClient = new OssClient(
                config('alioss.accessKeyId'),
                config('alioss.accessKeySecret'),
                config('alioss.endpoint')
            );
            $result = $ossClient->completeMultipartUpload(
                config('alioss.bucket'),
                $data['objectName'],
                $data['uploadId'],
                json_decode($data['parts'], true)
            );
            return json([
                'code' => 1,
                'url' => $result['oss-request-url'],
                'name' => pathinfo($data['objectName'], PATHINFO_FILENAME),
                'fullurl' => strstr($result['oss-request-url'], '?', true),
            ]);
        } catch (OssException $e) {
            return json(['code' => 0, 'msg' => $e->getMessage()]);
        }
    }

    // 直接上传完整文件
    public function directUpload()
    {
        try {
            $ossClient = new OssClient(
                config('alioss.accessKeyId'),
                config('alioss.accessKeySecret'),
                config('alioss.endpoint')
            );

            $file = $_FILES['file'];
            $file_name = $this->request->request('file_name', '');
            $object = 'uploads/' . date('Ymd') . '/' . $file['name'];

            $result = $ossClient->uploadFile(
                config('alioss.bucket'),
                $object,
                $file['tmp_name']
            );

            return json([
                'code' => 1,
                'url' => $result['oss-request-url'],
                'name' => pathinfo($file_name, PATHINFO_FILENAME),
                'fullurl' => $result['oss-request-url'],
            ]);
        } catch (OssException $e) {
            return json(['code' => 0, 'msg' => $e->getMessage()]);
        }
    }
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
在等待的日子里,努力工作,刻苦读书,锻炼身体,谦卑做人,养得深根,日后才能枝叶茂盛
阿神
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!