@balena/pinejs-webresource-s3 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.versionbot/CHANGELOG.yml +12 -1
- package/CHANGELOG.md +5 -0
- package/build/index.d.ts +30 -0
- package/build/index.js +67 -1
- package/build/index.js.map +1 -1
- package/lib/index.ts +151 -3
- package/package.json +2 -2
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
- commits:
|
|
2
|
+
- subject: Add beginMultipartUpload/commitMultiPartUpload
|
|
3
|
+
hash: 8ac4f8e08b79392ed762d9732dd43bc5d94d6b71
|
|
4
|
+
body: ""
|
|
5
|
+
footer:
|
|
6
|
+
Change-type: minor
|
|
7
|
+
change-type: minor
|
|
8
|
+
author: Otavio Jacobi
|
|
9
|
+
version: 0.2.0
|
|
10
|
+
title: ""
|
|
11
|
+
date: 2024-04-19T19:04:51.592Z
|
|
1
12
|
- commits:
|
|
2
13
|
- subject: Add flowzone deployment
|
|
3
14
|
hash: aeb1b8c276517b0241f02c9c20027153bb848f9a
|
|
@@ -15,4 +26,4 @@
|
|
|
15
26
|
author: Otavio Jacobi
|
|
16
27
|
version: 0.1.0
|
|
17
28
|
title: ""
|
|
18
|
-
date: 2024-04-19T13:
|
|
29
|
+
date: 2024-04-19T13:57:58.094Z
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file
|
|
|
4
4
|
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
|
5
5
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
# v0.2.0
|
|
8
|
+
## (2024-04-19)
|
|
9
|
+
|
|
10
|
+
* Add beginMultipartUpload/commitMultiPartUpload [Otavio Jacobi]
|
|
11
|
+
|
|
7
12
|
# v0.1.0
|
|
8
13
|
## (2024-04-19)
|
|
9
14
|
|
package/build/index.d.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import type { webResourceHandler } from '@balena/pinejs';
|
|
2
2
|
import type { WebResourceType as WebResource } from '@balena/sbvr-types';
|
|
3
|
+
import type { AnyObject } from 'pinejs-client-core';
|
|
3
4
|
import { TypedError } from 'typed-error';
|
|
5
|
+
interface BeginMultipartUploadPayload {
|
|
6
|
+
filename: string;
|
|
7
|
+
content_type: string;
|
|
8
|
+
size: number;
|
|
9
|
+
chunk_size: number;
|
|
10
|
+
}
|
|
11
|
+
interface UploadPart {
|
|
12
|
+
url: string;
|
|
13
|
+
chunkSize: number;
|
|
14
|
+
partNumber: number;
|
|
15
|
+
}
|
|
16
|
+
interface BeginMultipartUploadHandlerResponse {
|
|
17
|
+
uploadParts: UploadPart[];
|
|
18
|
+
fileKey: string;
|
|
19
|
+
uploadId: string;
|
|
20
|
+
}
|
|
21
|
+
interface CommitMultipartUploadPayload {
|
|
22
|
+
fileKey: string;
|
|
23
|
+
uploadId: string;
|
|
24
|
+
filename: string;
|
|
25
|
+
providerCommitData?: AnyObject;
|
|
26
|
+
}
|
|
4
27
|
export interface S3HandlerProps {
|
|
5
28
|
region: string;
|
|
6
29
|
accessKey: string;
|
|
@@ -27,7 +50,14 @@ export declare class S3Handler implements webResourceHandler.WebResourceHandler
|
|
|
27
50
|
handleFile(resource: webResourceHandler.IncomingFile): Promise<webResourceHandler.UploadResponse>;
|
|
28
51
|
removeFile(href: string): Promise<void>;
|
|
29
52
|
onPreRespond(webResource: WebResource): Promise<WebResource>;
|
|
53
|
+
beginMultipartUpload(fieldName: string, payload: BeginMultipartUploadPayload): Promise<BeginMultipartUploadHandlerResponse>;
|
|
54
|
+
commitMultipartUpload({ fileKey, uploadId, filename, providerCommitData, }: CommitMultipartUploadPayload): Promise<WebResource>;
|
|
30
55
|
private s3SignUrl;
|
|
31
56
|
private getS3URL;
|
|
32
57
|
private getKeyFromHref;
|
|
58
|
+
private getFileKey;
|
|
59
|
+
private getUploadParts;
|
|
60
|
+
private getPartUploadUrl;
|
|
61
|
+
private getChunkSizesWithParts;
|
|
33
62
|
}
|
|
63
|
+
export {};
|
package/build/index.js
CHANGED
|
@@ -41,7 +41,7 @@ class S3Handler {
|
|
|
41
41
|
}
|
|
42
42
|
async handleFile(resource) {
|
|
43
43
|
let size = 0;
|
|
44
|
-
const key =
|
|
44
|
+
const key = this.getFileKey(resource.fieldname, resource.originalname);
|
|
45
45
|
const params = {
|
|
46
46
|
Bucket: this.bucket,
|
|
47
47
|
Key: key,
|
|
@@ -83,6 +83,37 @@ class S3Handler {
|
|
|
83
83
|
}
|
|
84
84
|
return webResource;
|
|
85
85
|
}
|
|
86
|
+
async beginMultipartUpload(fieldName, payload) {
|
|
87
|
+
const fileKey = this.getFileKey(fieldName, payload.filename);
|
|
88
|
+
const createMultiPartResponse = await this.client.send(new client_s3_1.CreateMultipartUploadCommand({
|
|
89
|
+
Bucket: this.bucket,
|
|
90
|
+
Key: fileKey,
|
|
91
|
+
ContentType: payload.content_type,
|
|
92
|
+
}));
|
|
93
|
+
if (createMultiPartResponse.UploadId == null) {
|
|
94
|
+
throw new Error('Failed to create multipart upload.');
|
|
95
|
+
}
|
|
96
|
+
const uploadParts = await this.getUploadParts(fileKey, createMultiPartResponse.UploadId, payload);
|
|
97
|
+
return { fileKey, uploadId: createMultiPartResponse.UploadId, uploadParts };
|
|
98
|
+
}
|
|
99
|
+
async commitMultipartUpload({ fileKey, uploadId, filename, providerCommitData, }) {
|
|
100
|
+
await this.client.send(new client_s3_1.CompleteMultipartUploadCommand({
|
|
101
|
+
Bucket: this.bucket,
|
|
102
|
+
Key: fileKey,
|
|
103
|
+
UploadId: uploadId,
|
|
104
|
+
MultipartUpload: providerCommitData,
|
|
105
|
+
}));
|
|
106
|
+
const headResult = await this.client.send(new client_s3_1.HeadObjectCommand({
|
|
107
|
+
Bucket: this.bucket,
|
|
108
|
+
Key: fileKey,
|
|
109
|
+
}));
|
|
110
|
+
return {
|
|
111
|
+
href: this.getS3URL(fileKey),
|
|
112
|
+
filename: filename,
|
|
113
|
+
size: headResult.ContentLength,
|
|
114
|
+
content_type: headResult.ContentType,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
86
117
|
s3SignUrl(fileKey) {
|
|
87
118
|
const command = new client_s3_1.GetObjectCommand({
|
|
88
119
|
Bucket: this.bucket,
|
|
@@ -99,6 +130,41 @@ class S3Handler {
|
|
|
99
130
|
const hrefWithoutParams = normalizeHref(href);
|
|
100
131
|
return hrefWithoutParams.substring(hrefWithoutParams.lastIndexOf('/') + 1);
|
|
101
132
|
}
|
|
133
|
+
getFileKey(fieldName, fileName) {
|
|
134
|
+
return `${fieldName}_${(0, node_crypto_1.randomUUID)()}_${fileName}`;
|
|
135
|
+
}
|
|
136
|
+
async getUploadParts(fileKey, uploadId, payload) {
|
|
137
|
+
const chunkSizesWithParts = await this.getChunkSizesWithParts(payload.size, payload.chunk_size);
|
|
138
|
+
return Promise.all(chunkSizesWithParts.map(async ({ chunkSize, partNumber }) => ({
|
|
139
|
+
chunkSize,
|
|
140
|
+
partNumber,
|
|
141
|
+
url: await this.getPartUploadUrl(fileKey, uploadId, partNumber, chunkSize),
|
|
142
|
+
})));
|
|
143
|
+
}
|
|
144
|
+
async getPartUploadUrl(fileKey, uploadId, partNumber, partSize) {
|
|
145
|
+
const command = new client_s3_1.UploadPartCommand({
|
|
146
|
+
Bucket: this.bucket,
|
|
147
|
+
Key: fileKey,
|
|
148
|
+
UploadId: uploadId,
|
|
149
|
+
PartNumber: partNumber,
|
|
150
|
+
ContentLength: partSize,
|
|
151
|
+
});
|
|
152
|
+
return (0, s3_request_presigner_1.getSignedUrl)(this.client, command, {
|
|
153
|
+
expiresIn: this.signedUrlExpireTimeSeconds,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async getChunkSizesWithParts(size, chunkSize) {
|
|
157
|
+
const chunkSizesWithParts = [];
|
|
158
|
+
let partNumber = 1;
|
|
159
|
+
let remainingSize = size;
|
|
160
|
+
while (remainingSize > 0) {
|
|
161
|
+
const currentChunkSize = Math.min(remainingSize, chunkSize);
|
|
162
|
+
chunkSizesWithParts.push({ chunkSize: currentChunkSize, partNumber });
|
|
163
|
+
remainingSize -= currentChunkSize;
|
|
164
|
+
partNumber += 1;
|
|
165
|
+
}
|
|
166
|
+
return chunkSizesWithParts;
|
|
167
|
+
}
|
|
102
168
|
}
|
|
103
169
|
exports.S3Handler = S3Handler;
|
|
104
170
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"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"}
|
package/lib/index.ts
CHANGED
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
S3Client,
|
|
5
5
|
type PutObjectCommandInput,
|
|
6
6
|
type S3ClientConfig,
|
|
7
|
+
CreateMultipartUploadCommand,
|
|
8
|
+
UploadPartCommand,
|
|
9
|
+
CompleteMultipartUploadCommand,
|
|
10
|
+
HeadObjectCommand,
|
|
7
11
|
} from '@aws-sdk/client-s3';
|
|
8
12
|
import { Upload } from '@aws-sdk/lib-storage';
|
|
9
13
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
@@ -12,8 +16,36 @@ import type { webResourceHandler } from '@balena/pinejs';
|
|
|
12
16
|
import type { WebResourceType as WebResource } from '@balena/sbvr-types';
|
|
13
17
|
import * as memoize from 'memoizee';
|
|
14
18
|
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import type { AnyObject } from 'pinejs-client-core';
|
|
15
20
|
import { TypedError } from 'typed-error';
|
|
16
21
|
|
|
22
|
+
// TODO: remove me and import from pinejs once v17 is out
|
|
23
|
+
interface BeginMultipartUploadPayload {
|
|
24
|
+
filename: string;
|
|
25
|
+
content_type: string;
|
|
26
|
+
size: number;
|
|
27
|
+
chunk_size: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface UploadPart {
|
|
31
|
+
url: string;
|
|
32
|
+
chunkSize: number;
|
|
33
|
+
partNumber: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface BeginMultipartUploadHandlerResponse {
|
|
37
|
+
uploadParts: UploadPart[];
|
|
38
|
+
fileKey: string;
|
|
39
|
+
uploadId: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CommitMultipartUploadPayload {
|
|
43
|
+
fileKey: string;
|
|
44
|
+
uploadId: string;
|
|
45
|
+
filename: string;
|
|
46
|
+
providerCommitData?: AnyObject;
|
|
47
|
+
}
|
|
48
|
+
|
|
17
49
|
export interface S3HandlerProps {
|
|
18
50
|
region: string;
|
|
19
51
|
accessKey: string;
|
|
@@ -78,9 +110,7 @@ export class S3Handler implements webResourceHandler.WebResourceHandler {
|
|
|
78
110
|
resource: webResourceHandler.IncomingFile,
|
|
79
111
|
): Promise<webResourceHandler.UploadResponse> {
|
|
80
112
|
let size = 0;
|
|
81
|
-
const key =
|
|
82
|
-
resource.originalname
|
|
83
|
-
}`;
|
|
113
|
+
const key = this.getFileKey(resource.fieldname, resource.originalname);
|
|
84
114
|
const params: PutObjectCommandInput = {
|
|
85
115
|
Bucket: this.bucket,
|
|
86
116
|
Key: key,
|
|
@@ -129,6 +159,62 @@ export class S3Handler implements webResourceHandler.WebResourceHandler {
|
|
|
129
159
|
return webResource;
|
|
130
160
|
}
|
|
131
161
|
|
|
162
|
+
public async beginMultipartUpload(
|
|
163
|
+
fieldName: string,
|
|
164
|
+
payload: BeginMultipartUploadPayload,
|
|
165
|
+
): Promise<BeginMultipartUploadHandlerResponse> {
|
|
166
|
+
const fileKey = this.getFileKey(fieldName, payload.filename);
|
|
167
|
+
|
|
168
|
+
const createMultiPartResponse = await this.client.send(
|
|
169
|
+
new CreateMultipartUploadCommand({
|
|
170
|
+
Bucket: this.bucket,
|
|
171
|
+
Key: fileKey,
|
|
172
|
+
ContentType: payload.content_type,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (createMultiPartResponse.UploadId == null) {
|
|
177
|
+
throw new Error('Failed to create multipart upload.');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const uploadParts = await this.getUploadParts(
|
|
181
|
+
fileKey,
|
|
182
|
+
createMultiPartResponse.UploadId,
|
|
183
|
+
payload,
|
|
184
|
+
);
|
|
185
|
+
return { fileKey, uploadId: createMultiPartResponse.UploadId, uploadParts };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public async commitMultipartUpload({
|
|
189
|
+
fileKey,
|
|
190
|
+
uploadId,
|
|
191
|
+
filename,
|
|
192
|
+
providerCommitData,
|
|
193
|
+
}: CommitMultipartUploadPayload): Promise<WebResource> {
|
|
194
|
+
await this.client.send(
|
|
195
|
+
new CompleteMultipartUploadCommand({
|
|
196
|
+
Bucket: this.bucket,
|
|
197
|
+
Key: fileKey,
|
|
198
|
+
UploadId: uploadId,
|
|
199
|
+
MultipartUpload: providerCommitData,
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const headResult = await this.client.send(
|
|
204
|
+
new HeadObjectCommand({
|
|
205
|
+
Bucket: this.bucket,
|
|
206
|
+
Key: fileKey,
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
href: this.getS3URL(fileKey),
|
|
212
|
+
filename: filename,
|
|
213
|
+
size: headResult.ContentLength,
|
|
214
|
+
content_type: headResult.ContentType,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
132
218
|
private s3SignUrl(fileKey: string): Promise<string> {
|
|
133
219
|
const command = new GetObjectCommand({
|
|
134
220
|
Bucket: this.bucket,
|
|
@@ -147,4 +233,66 @@ export class S3Handler implements webResourceHandler.WebResourceHandler {
|
|
|
147
233
|
const hrefWithoutParams = normalizeHref(href);
|
|
148
234
|
return hrefWithoutParams.substring(hrefWithoutParams.lastIndexOf('/') + 1);
|
|
149
235
|
}
|
|
236
|
+
|
|
237
|
+
private getFileKey(fieldName: string, fileName: string) {
|
|
238
|
+
return `${fieldName}_${randomUUID()}_${fileName}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async getUploadParts(
|
|
242
|
+
fileKey: string,
|
|
243
|
+
uploadId: string,
|
|
244
|
+
payload: BeginMultipartUploadPayload,
|
|
245
|
+
): Promise<UploadPart[]> {
|
|
246
|
+
const chunkSizesWithParts = await this.getChunkSizesWithParts(
|
|
247
|
+
payload.size,
|
|
248
|
+
payload.chunk_size,
|
|
249
|
+
);
|
|
250
|
+
return Promise.all(
|
|
251
|
+
chunkSizesWithParts.map(async ({ chunkSize, partNumber }) => ({
|
|
252
|
+
chunkSize,
|
|
253
|
+
partNumber,
|
|
254
|
+
url: await this.getPartUploadUrl(
|
|
255
|
+
fileKey,
|
|
256
|
+
uploadId,
|
|
257
|
+
partNumber,
|
|
258
|
+
chunkSize,
|
|
259
|
+
),
|
|
260
|
+
})),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async getPartUploadUrl(
|
|
265
|
+
fileKey: string,
|
|
266
|
+
uploadId: string,
|
|
267
|
+
partNumber: number,
|
|
268
|
+
partSize: number,
|
|
269
|
+
): Promise<string> {
|
|
270
|
+
const command = new UploadPartCommand({
|
|
271
|
+
Bucket: this.bucket,
|
|
272
|
+
Key: fileKey,
|
|
273
|
+
UploadId: uploadId,
|
|
274
|
+
PartNumber: partNumber,
|
|
275
|
+
ContentLength: partSize,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return getSignedUrl(this.client, command, {
|
|
279
|
+
expiresIn: this.signedUrlExpireTimeSeconds,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async getChunkSizesWithParts(
|
|
284
|
+
size: number,
|
|
285
|
+
chunkSize: number,
|
|
286
|
+
): Promise<Array<Pick<UploadPart, 'chunkSize' | 'partNumber'>>> {
|
|
287
|
+
const chunkSizesWithParts = [];
|
|
288
|
+
let partNumber = 1;
|
|
289
|
+
let remainingSize = size;
|
|
290
|
+
while (remainingSize > 0) {
|
|
291
|
+
const currentChunkSize = Math.min(remainingSize, chunkSize);
|
|
292
|
+
chunkSizesWithParts.push({ chunkSize: currentChunkSize, partNumber });
|
|
293
|
+
remainingSize -= currentChunkSize;
|
|
294
|
+
partNumber += 1;
|
|
295
|
+
}
|
|
296
|
+
return chunkSizesWithParts;
|
|
297
|
+
}
|
|
150
298
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@balena/pinejs-webresource-s3",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A PineJS WebResource handler for storing & serving files on S3",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"typed-error": "^3.2.2"
|
|
29
29
|
},
|
|
30
30
|
"versionist": {
|
|
31
|
-
"publishedAt": "2024-04-
|
|
31
|
+
"publishedAt": "2024-04-19T19:04:51.629Z"
|
|
32
32
|
}
|
|
33
33
|
}
|