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

本文概括

首先前端方面使用的是 React 以及 Ant Design,后端使用 PHP 以及 AWS-SDK
通过后端与 AWS 进行交互创建多段上传,拿到后续所需的 KeyID
然后前端将 File 文件进行 slice 分片,并在每次分片后取调用后端接口
此时后端通过之前的 KeyID 来获取当前分片的上传地址(由 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 对象后,获取相应的 namesizetypelastModifiedDate 并传递给后端来创建多段上传

/**
 * 创建多段上传
 *
 * @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 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 2
ShMichaelLi

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

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

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

4年前 评论

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