Ant Design Upload 通過後端預生成 URL 分片上傳大檔案到 AWS S3

韓槑槑發表於2019-08-29

本文概括

首先前端方面使用的是 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()
    {
        //
    }
}

相關文章