@travetto/model-s3 7.1.3 → 8.0.0-alpha.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/package.json +7 -7
- package/src/service.ts +65 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-s3",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "S3 backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"directory": "module/model-s3"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@aws-sdk/client-s3": "^3.
|
|
30
|
-
"@aws-sdk/credential-provider-ini": "^3.
|
|
31
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
32
|
-
"@travetto/config": "^
|
|
33
|
-
"@travetto/model": "^
|
|
29
|
+
"@aws-sdk/client-s3": "^3.1000.0",
|
|
30
|
+
"@aws-sdk/credential-provider-ini": "^3.972.13",
|
|
31
|
+
"@aws-sdk/s3-request-presigner": "^3.1000.0",
|
|
32
|
+
"@travetto/config": "^8.0.0-alpha.0",
|
|
33
|
+
"@travetto/model": "^8.0.0-alpha.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@travetto/cli": "^
|
|
36
|
+
"@travetto/cli": "^8.0.0-alpha.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@travetto/cli": {
|
package/src/service.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Readable } from 'node:stream';
|
|
2
|
-
import { text as toText, buffer as toBuffer } from 'node:stream/consumers';
|
|
3
1
|
import { Agent } from 'node:https';
|
|
4
2
|
|
|
5
3
|
import { S3, type CompletedPart, type CreateMultipartUploadRequest, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
@@ -9,12 +7,12 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
|
9
7
|
|
|
10
8
|
import {
|
|
11
9
|
type ModelCrudSupport, type ModelStorageSupport, type ModelType, ModelRegistryIndex, ExistsError, NotFoundError, type OptionalId,
|
|
12
|
-
type ModelBlobSupport, type ModelExpirySupport,
|
|
10
|
+
type ModelBlobSupport, type ModelExpirySupport, ModelCrudUtil, ModelExpiryUtil, ModelStorageUtil
|
|
13
11
|
} from '@travetto/model';
|
|
14
12
|
import { Injectable } from '@travetto/di';
|
|
15
13
|
import {
|
|
16
|
-
type Class,
|
|
17
|
-
type
|
|
14
|
+
type Class, RuntimeError, castTo, asFull, type BinaryMetadata, type ByteRange, type BinaryType,
|
|
15
|
+
BinaryUtil, type TimeSpan, TimeUtil, type BinaryArray, CodecUtil, BinaryMetadataUtil, TypedObject, JSONUtil
|
|
18
16
|
} from '@travetto/runtime';
|
|
19
17
|
|
|
20
18
|
import type { S3ModelConfig } from './config.ts';
|
|
@@ -27,7 +25,7 @@ function hasContentType<T>(value: T): value is T & { contenttype?: string } {
|
|
|
27
25
|
return value !== undefined && value !== null && Object.hasOwn(value, 'contenttype');
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
type
|
|
28
|
+
type S3Metadata = Pick<CreateMultipartUploadRequest,
|
|
31
29
|
'ContentType' | 'Metadata' | 'ContentEncoding' | 'ContentLanguage' | 'CacheControl' | 'ContentDisposition'
|
|
32
30
|
>;
|
|
33
31
|
|
|
@@ -43,16 +41,16 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
43
41
|
|
|
44
42
|
constructor(config: S3ModelConfig) { this.config = config; }
|
|
45
43
|
|
|
46
|
-
#
|
|
44
|
+
#getMetadata(metadata: BinaryMetadata): S3Metadata {
|
|
47
45
|
return {
|
|
48
|
-
ContentType:
|
|
49
|
-
...(
|
|
50
|
-
...(
|
|
51
|
-
...(
|
|
52
|
-
Metadata:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
ContentType: metadata.contentType,
|
|
47
|
+
...(metadata.contentEncoding ? { ContentEncoding: metadata.contentEncoding } : {}),
|
|
48
|
+
...(metadata.contentLanguage ? { ContentLanguage: metadata.contentLanguage } : {}),
|
|
49
|
+
...(metadata.cacheControl ? { CacheControl: metadata.cacheControl } : {}),
|
|
50
|
+
Metadata: TypedObject.fromEntries(
|
|
51
|
+
TypedObject.entries(metadata)
|
|
52
|
+
.map(([key, value]) => [key, typeof value === 'string' ? value : JSONUtil.toUTF8(value)] as const)
|
|
53
|
+
)
|
|
56
54
|
};
|
|
57
55
|
}
|
|
58
56
|
|
|
@@ -117,17 +115,17 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
117
115
|
/**
|
|
118
116
|
* Write multipart file upload, in chunks
|
|
119
117
|
*/
|
|
120
|
-
async #writeMultipart(id: string, input:
|
|
121
|
-
const { UploadId } = await this.client.createMultipartUpload(this.#queryBlob(id, this.#
|
|
118
|
+
async #writeMultipart(id: string, input: BinaryType, metadata: BinaryMetadata): Promise<void> {
|
|
119
|
+
const { UploadId } = await this.client.createMultipartUpload(this.#queryBlob(id, this.#getMetadata(metadata)));
|
|
122
120
|
|
|
123
121
|
const parts: CompletedPart[] = [];
|
|
124
|
-
let buffers:
|
|
122
|
+
let buffers: BinaryArray[] = [];
|
|
125
123
|
let total = 0;
|
|
126
124
|
let i = 1;
|
|
127
125
|
const flush = async (): Promise<void> => {
|
|
128
126
|
if (!total) { return; }
|
|
129
127
|
const part = await this.client.uploadPart(this.#queryBlob(id, {
|
|
130
|
-
Body:
|
|
128
|
+
Body: BinaryUtil.binaryArrayToUint8Array(BinaryUtil.combineBinaryArrays(buffers)),
|
|
131
129
|
PartNumber: i,
|
|
132
130
|
UploadId
|
|
133
131
|
}));
|
|
@@ -137,10 +135,10 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
137
135
|
total = 0;
|
|
138
136
|
};
|
|
139
137
|
try {
|
|
140
|
-
for await (const chunk of input) {
|
|
141
|
-
const chunked =
|
|
138
|
+
for await (const chunk of BinaryUtil.toBinaryStream(input)) {
|
|
139
|
+
const chunked = CodecUtil.readUtf8Chunk(chunk);
|
|
142
140
|
buffers.push(chunked);
|
|
143
|
-
total += chunked.
|
|
141
|
+
total += chunked.byteLength;
|
|
144
142
|
if (total > this.config.chunkSize) {
|
|
145
143
|
await flush();
|
|
146
144
|
}
|
|
@@ -217,7 +215,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
217
215
|
try {
|
|
218
216
|
const result = await this.client.getObject(this.#query(cls, id));
|
|
219
217
|
if (result.Body) {
|
|
220
|
-
const body = await
|
|
218
|
+
const body = await BinaryUtil.toBinaryArray(result.Body);
|
|
221
219
|
const output = await ModelCrudUtil.load(cls, body);
|
|
222
220
|
if (output) {
|
|
223
221
|
const { expiresAt } = ModelRegistryIndex.getConfig(cls);
|
|
@@ -247,11 +245,11 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
247
245
|
if (preStore) {
|
|
248
246
|
prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
249
247
|
}
|
|
250
|
-
const content =
|
|
248
|
+
const content = JSONUtil.toBinaryArray(prepped);
|
|
251
249
|
await this.client.putObject(this.#query(cls, prepped.id, {
|
|
252
|
-
Body: content,
|
|
250
|
+
Body: BinaryUtil.binaryArrayToUint8Array(content),
|
|
253
251
|
ContentType: 'application/json',
|
|
254
|
-
ContentLength: content.
|
|
252
|
+
ContentLength: content.byteLength,
|
|
255
253
|
...this.#getExpiryConfig(cls, prepped)
|
|
256
254
|
}));
|
|
257
255
|
return prepped;
|
|
@@ -314,53 +312,52 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
314
312
|
}
|
|
315
313
|
|
|
316
314
|
// Blob support
|
|
317
|
-
async upsertBlob(location: string, input:
|
|
318
|
-
if (!overwrite && await this.
|
|
315
|
+
async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
|
|
316
|
+
if (!overwrite && await this.getBlobMetadata(location).then(() => true, () => false)) {
|
|
319
317
|
return;
|
|
320
318
|
}
|
|
321
319
|
|
|
322
|
-
const
|
|
320
|
+
const resolved = await BinaryMetadataUtil.compute(input, metadata);
|
|
321
|
+
|
|
322
|
+
const length = BinaryMetadataUtil.readLength(resolved);
|
|
323
323
|
|
|
324
|
-
if (
|
|
324
|
+
if (length && length < this.config.chunkSize) { // If smaller than chunk size
|
|
325
|
+
const blob = this.#queryBlob(location, {
|
|
326
|
+
Body: BinaryUtil.toReadable(input),
|
|
327
|
+
ContentLength: length,
|
|
328
|
+
...this.#getMetadata(resolved),
|
|
329
|
+
});
|
|
325
330
|
// Upload to s3
|
|
326
|
-
await this.client.putObject(
|
|
327
|
-
Body: await toBuffer(stream),
|
|
328
|
-
ContentLength: blobMeta.size,
|
|
329
|
-
...this.#getMetaBase(blobMeta),
|
|
330
|
-
}));
|
|
331
|
+
await this.client.putObject(blob);
|
|
331
332
|
} else {
|
|
332
|
-
await this.#writeMultipart(location,
|
|
333
|
+
await this.#writeMultipart(location, input, resolved);
|
|
333
334
|
}
|
|
334
335
|
}
|
|
335
336
|
|
|
336
|
-
async #getObject(location: string, range?: Required<ByteRange>): Promise<
|
|
337
|
+
async #getObject(location: string, range?: Required<ByteRange>): Promise<BinaryType> {
|
|
337
338
|
// Read from s3
|
|
338
339
|
const result = await this.client.getObject(this.#queryBlob(location, range ? {
|
|
339
340
|
Range: `bytes=${range.start}-${range.end}`
|
|
340
341
|
} : {}));
|
|
341
342
|
|
|
342
|
-
|
|
343
|
-
throw new AppError('Unable to read type: undefined');
|
|
344
|
-
}
|
|
343
|
+
const body: BinaryType | string | undefined = castTo(result.Body);
|
|
345
344
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return
|
|
345
|
+
switch (typeof body) {
|
|
346
|
+
case 'undefined': throw new RuntimeError('Unable to read type: undefined');
|
|
347
|
+
case 'string': return body.endsWith('=') ?
|
|
348
|
+
CodecUtil.fromBase64String(body) :
|
|
349
|
+
CodecUtil.fromUTF8String(body);
|
|
350
|
+
default: return body;
|
|
352
351
|
}
|
|
353
|
-
throw new AppError(`Unable to read type: ${typeof result.Body}`);
|
|
354
352
|
}
|
|
355
353
|
|
|
356
354
|
async getBlob(location: string, range?: ByteRange): Promise<Blob> {
|
|
357
|
-
const
|
|
358
|
-
const final = range ?
|
|
359
|
-
|
|
360
|
-
return BinaryUtil.readableBlob(result, { ...meta, range: final });
|
|
355
|
+
const metadata = await this.getBlobMetadata(location);
|
|
356
|
+
const final = range ? BinaryMetadataUtil.enforceRange(range, metadata) : undefined;
|
|
357
|
+
return BinaryMetadataUtil.makeBlob(() => this.#getObject(location, final), { ...metadata, range: final });
|
|
361
358
|
}
|
|
362
359
|
|
|
363
|
-
async headBlob(location: string): Promise<{ Metadata?:
|
|
360
|
+
async headBlob(location: string): Promise<{ Metadata?: BinaryMetadata, ContentLength?: number }> {
|
|
364
361
|
const query = this.#queryBlob(location);
|
|
365
362
|
try {
|
|
366
363
|
return (await this.client.headObject(query));
|
|
@@ -374,20 +371,20 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
374
371
|
}
|
|
375
372
|
}
|
|
376
373
|
|
|
377
|
-
async
|
|
374
|
+
async getBlobMetadata(location: string): Promise<BinaryMetadata> {
|
|
378
375
|
const blob = await this.headBlob(location);
|
|
379
376
|
|
|
380
377
|
if (blob) {
|
|
381
|
-
const
|
|
378
|
+
const metadata: BinaryMetadata = {
|
|
382
379
|
contentType: '',
|
|
383
380
|
...blob.Metadata,
|
|
384
381
|
size: blob.ContentLength!,
|
|
385
382
|
};
|
|
386
|
-
if (hasContentType(
|
|
387
|
-
|
|
388
|
-
delete
|
|
383
|
+
if (hasContentType(metadata)) {
|
|
384
|
+
metadata['contentType'] = metadata['contenttype']!;
|
|
385
|
+
delete metadata['contenttype'];
|
|
389
386
|
}
|
|
390
|
-
return
|
|
387
|
+
return metadata;
|
|
391
388
|
} else {
|
|
392
389
|
throw new NotFoundError('Blob', location);
|
|
393
390
|
}
|
|
@@ -397,38 +394,38 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
397
394
|
await this.client.deleteObject(this.#queryBlob(location));
|
|
398
395
|
}
|
|
399
396
|
|
|
400
|
-
async
|
|
397
|
+
async updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void> {
|
|
401
398
|
await this.client.copyObject({
|
|
402
399
|
Bucket: this.config.bucket,
|
|
403
400
|
Key: this.#basicKey(location),
|
|
404
401
|
CopySource: `/${this.config.bucket}/${this.#basicKey(location)}`,
|
|
405
|
-
...this.#
|
|
402
|
+
...this.#getMetadata(metadata),
|
|
406
403
|
MetadataDirective: 'REPLACE'
|
|
407
404
|
});
|
|
408
405
|
}
|
|
409
406
|
|
|
410
407
|
// Signed urls
|
|
411
|
-
async getBlobReadUrl(location: string,
|
|
408
|
+
async getBlobReadUrl(location: string, expiresIn: TimeSpan = '1h'): Promise<string> {
|
|
412
409
|
return await getSignedUrl(
|
|
413
410
|
this.client,
|
|
414
411
|
new GetObjectCommand(this.#queryBlob(location)),
|
|
415
|
-
{ expiresIn: TimeUtil.
|
|
412
|
+
{ expiresIn: TimeUtil.duration(expiresIn, 's') }
|
|
416
413
|
);
|
|
417
414
|
}
|
|
418
415
|
|
|
419
|
-
async getBlobWriteUrl(location: string,
|
|
420
|
-
const base = this.#
|
|
416
|
+
async getBlobWriteUrl(location: string, metadata: BinaryMetadata, expiresIn: TimeSpan = '1h'): Promise<string> {
|
|
417
|
+
const base = this.#getMetadata(metadata);
|
|
421
418
|
return await getSignedUrl(
|
|
422
419
|
this.client,
|
|
423
420
|
new PutObjectCommand({
|
|
424
421
|
...this.#queryBlob(location),
|
|
425
422
|
...base,
|
|
426
|
-
...(
|
|
427
|
-
...((
|
|
423
|
+
...(metadata.size ? { ContentLength: metadata.size } : {}),
|
|
424
|
+
...((metadata.hash && metadata.hash !== '-1') ? { ChecksumSHA256: metadata.hash } : {}),
|
|
428
425
|
}),
|
|
429
426
|
{
|
|
430
|
-
expiresIn: TimeUtil.
|
|
431
|
-
...(
|
|
427
|
+
expiresIn: TimeUtil.duration(expiresIn, 's'),
|
|
428
|
+
...(metadata.contentType ? { signableHeaders: new Set(['content-type']) } : {})
|
|
432
429
|
}
|
|
433
430
|
);
|
|
434
431
|
}
|