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