@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.
@@ -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:49:39.626Z
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 = `${resource.fieldname}_${(0, node_crypto_1.randomUUID)()}_${resource.originalname}`;
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"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"}
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 = `${resource.fieldname}_${randomUUID()}_${
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.1.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-19T13:49:39.669Z"
31
+ "publishedAt": "2024-04-19T19:04:51.629Z"
32
32
  }
33
33
  }