韩槑槑

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

字数统计: 1.3k阅读时长: 6 min
2019/08/27 Share

本文概括

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 并传递给后端来创建多段上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 创建多段上传
*
* @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 进行分片处理,拿到结果数组

1
2
3
4
5
6
7
8
9
10
11
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 上传链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 为某个分段生成预签名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

1
2
3
4
5
6
7
8
9
10
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 将分片合并,并返回结果信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 完成上传
*
* @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();
}

完整的后端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<?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()
{
//
}
}
CATALOG
  1. 1. 本文概括
  2. 2. 拦截 Ant Design Upload 采用自己的上传
  3. 3. 后端创建分片上传
  4. 4. 前端对 File 进行分片
  5. 5. 循环分片 并预生成相应的上传 URL
  6. 6. 上传分片
  7. 7. 完成上传
  8. 8. 完整的后端代码