Ant Design Upload 通过后端预生成 URL 分片上传大文件到 AWS S3

本文概括

首先前端方面使用的是 React 以及 Ant Design,后端使用 PHP 以及 AWS-SDK
通过后端与 AWS 进行交互创建多段上传,拿到后续所需的 Key 和 ID
然后前端将 File 文件进行 slice 分片,并在每次分片后取调用后端接口
此时后端通过之前的 Key 和 ID 来获取当前分片的上传地址(由 AWS 返回)
前端不同分片拿到各自的上传地址后,各自异步上传到相应的 URL 即可
当上传完毕后调用后端接口,后端再调用 AWS 的完成上传接口,让 AWS 对分片进行合并即可

拦截 Ant Design Upload 采用自己的上传

在 Upload 组件的 beforeUpload 方法中返回 FALSE,然后在 return 之前插入自己的逻辑即可

const uploadProps = {
    name: 'file',
    multiple: true,
    accept: 'video/*',
    beforeUpload: function(file: RcFile, _: RcFile[]) {
        // 在此处填入上传逻辑

        return false;
    },
    onChange: function(info: UploadChangeParam<UploadFile>) {
        const { status } = info.file;
        if (status === 'done') {
            message.success(`${info.file.name} file uploaded successfully.`);
        } else if (status === 'error') {
            message.error(`${info.file.name} file upload failed.`);
        }
    },
};

后端创建分片上传

前端获取到上传的文件 File 对象后,获取相应的 name、size、type、lastModifiedDate 并传递给后端来创建多段上传

/**
 * 创建多段上传
 *
 * @param array $fileInfo
 *
 * $info = [
 *      'name' => '文件名称'
 *      'size' => '文件大小'
 *      'type' => '文件类型'
 *      'lastModifiedDate' => '最后操作时间'
 * ]
 *
 * @return array
 *
 * @author hanmeimei
 */
public function create(array $fileInfo)
{
    // 为了避免文件名包含中文或空格等,采用 uniqid 生成新的文件名
    $fileInfo['name'] = $this->generateFileName($fileInfo['name']);

    $res = $this->s3->createMultipartUpload([
        // S3 桶
        'Bucket'      => self::BUCKET,
        // 存储路径,自定义
        'Key'         => $this->getPrefix() . $fileInfo['name'],
        'ContentType' => $fileInfo['type'],
        'Metadata'    => $fileInfo
    ]);

    return [
        'id'  => $res->get('UploadId'),
        'key' => $res->get('Key'),
    ];
}

前端对 File 进行分片

对 File 进行分片处理,拿到结果数组

const getBlobs = (file: RcFile) => {
    let start = 0;
    const blobs: Blob[] = [];

    while (start < file.size) {
        const filePart = file.slice(start, Math.min((start += partSize), file.size));
        if (filePart.size > 0) blobs.push(filePart);
    }

    return blobs;
};

循环分片 并预生成相应的上传 URL

循环上方生成的分片数组,并获取对应分片的 size 已经 number(可以直接使用数组索引来代替)
然后调用后端接口,生成当前分片对应的 AWS 上传链接

/**
 * 为某个分段生成预签名url
 *
 * @param string $key    创建多段上传拿到的 Key
 * @param string $id     创建多段上传拿到的 Id
 * @param int    $number 当前分片为第几片 (从 1 开始)
 * @param int    $length 当前分片内容大小
 *
 * @return string
 *
 * @author hanmeimei
 */
public function part(string $key, string $id, int $number, int $length)
{
    $command = $this->s3->getCommand('UploadPart', [
        'Bucket'        => self::BUCKET,
        'Key'           => $key,
        'UploadId'      => $id,
        'PartNumber'    => $number,
        'ContentLength' => $length
    ]);

    // 预签名url有效期48小时
    return (string)$this->s3->createPresignedRequest($command, '+48 hours')->getUri();
}

上传分片

拿到对应的分片上传链接后,通过异步上传分片对象 Blob

export async function sendS3(url: string, data: Blob) {
    const response: Response = await fetch(url, {
        method: 'PUT',
        body: data,
    });

    await response.text();

    return response.status;
}

完成上传

通过计数器来判断上传成功的次数,并与分片数组的长度进行对比
达到相等时即可调用后端 完成上传 接口,让 AWS 将分片合并,并返回结果信息

/**
 * 完成上传
 *
 * @param string $key 创建多段上传拿到的 Key
 * @param string $id  创建多段上传拿到的 Id
 *
 * @return array
 *
 * @author hanmeimei
 */
public function complete(string $key, string $id)
{
    $partsModel = $this->s3->listParts([
        'Bucket'   => self::BUCKET,
        'Key'      => $key,
        'UploadId' => $id,
    ]);

    $this->s3->completeMultipartUpload([
        'Bucket'          => self::BUCKET,
        'Key'             => $key,
        'UploadId'        => $id,
        'MultipartUpload' => [
            "Parts" => $partsModel["Parts"],
        ],
    ]);

    return $partsModel->toArray();
}

完整的后端代码

<?php

namespace App\Kernel\Support;

use Aws\Result;
use Aws\S3\S3Client;
use Psr\Http\Message\RequestInterface;

/**
 * Class S3MultipartUpload
 *
 * @author  hanmeimei
 *
 * @package App\Kernel\Support
 */
class S3MultipartUpload
{
    /**
     * @var S3MultipartUpload
     */
    private static $instance;

    /**
     * @var S3Client;
     */
    private $s3;

    /**
     * @var string
     */
    const BUCKET = 'bucket';

    /**
     * Get Instance
     *
     * @return S3MultipartUpload
     *
     * @author viest
     */
    public static function getInstance(): S3MultipartUpload
    {
        if (!self::$instance) {
            self::$instance = new static();
        }

        return self::$instance;
    }

    /**
     * 创建多段上传
     *
     * @param array $fileInfo
     *
     * @return array
     *
     * @author hanmeimei
     */
    public function create(array $fileInfo)
    {
        $fileInfo['name'] = $this->generateFileName($fileInfo['name']);

        $res = $this->s3->createMultipartUpload([
            'Bucket'      => self::BUCKET,
            'Key'         => $this->getPrefix() . $fileInfo['name'],
            'ContentType' => $fileInfo['type'],
            'Metadata'    => $fileInfo
        ]);

        return [
            'id'  => $res->get('UploadId'),
            'key' => $res->get('Key'),
        ];
    }

    /**
     * 为某个分段生成预签名url
     *
     * @param string $key
     * @param string $id
     * @param int    $number
     * @param int    $length
     *
     * @return string
     *
     * @author hanmeimei
     */
    public function part(string $key, string $id, int $number, int $length)
    {
        $command = $this->s3->getCommand('UploadPart', [
            'Bucket'        => self::BUCKET,
            'Key'           => $key,
            'UploadId'      => $id,
            'PartNumber'    => $number,
            'ContentLength' => $length
        ]);

        // 预签名url有效期48小时
        return (string)$this->s3->createPresignedRequest($command, '+48 hours')->getUri();
    }

    /**
     * 完成上传
     *
     * @param string $key
     * @param string $id
     *
     * @return array
     *
     * @author hanmeimei
     */
    public function complete(string $key, string $id)
    {
        $partsModel = $this->s3->listParts([
            'Bucket'   => self::BUCKET,
            'Key'      => $key,
            'UploadId' => $id,
        ]);

        $this->s3->completeMultipartUpload([
            'Bucket'          => self::BUCKET,
            'Key'             => $key,
            'UploadId'        => $id,
            'MultipartUpload' => [
                "Parts" => $partsModel["Parts"],
            ],
        ]);

        return $partsModel->toArray();
    }

    /**
     * 终止上传
     *
     * @param string $key
     * @param string $id
     *
     * @return bool
     *
     * @author hanmeimei
     */
    public function abort(string $key, string $id)
    {
        $this->s3->abortMultipartUpload([
            'Bucket'   => self::BUCKET,
            'Key'      => $key,
            'UploadId' => $id
        ]);

        return true;
    }

    /**
     * 获取图片路径前缀
     *
     * @return string
     *
     * @author hanmeimei
     */
    private function getPrefix()
    {
        $prefix = env('APP_DEBUG') ? 'develop/video/' : 'video/';

        return $prefix . auth()->user()->id . '/' . date('Ym') . '/';
    }

    /**
     * 生成文件名
     *
     * @param string|NULL $name
     *
     * @return string
     *
     * @author hanmeimei
     */
    private function generateFileName(string $name)
    {
        return uniqid() . strrchr($name, '.');
    }

    /**
     * Upload constructor.
     */
    private function __construct()
    {
        $this->s3 = new S3Client([
            'version' => 'latest',
            'region'  => 'ap-northeast-1',
            'profile' => 's3'
        ]);
    }

    /**
     * Disable Clone
     */
    private function __clone()
    {
        //
    }

    /**
     * Disable Serialization
     */
    private function __sleep()
    {
        //
    }
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 2
ShMichaelLi

想问下这种业务,一定要后端分片吗?有没有前端分片的方案,直传aws。

6年前 评论
韩槑槑 (楼主) 6年前

如果要是定时分片上传楼主有没有思路?

6年前 评论

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