@travetto/model-s3 6.0.0-rc.1 → 6.0.0-rc.3
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/README.md +4 -13
- package/__index__.ts +2 -2
- package/package.json +7 -7
- package/src/service.ts +47 -47
package/README.md
CHANGED
|
@@ -16,9 +16,10 @@ yarn add @travetto/model-s3
|
|
|
16
16
|
This module provides an [s3](https://aws.amazon.com/documentation/s3/)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and stream against [s3](https://aws.amazon.com/documentation/s3/).
|
|
17
17
|
|
|
18
18
|
Supported features:
|
|
19
|
-
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/
|
|
20
|
-
* [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/
|
|
21
|
-
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/
|
|
19
|
+
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
|
|
20
|
+
* [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8)
|
|
21
|
+
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
|
|
22
|
+
|
|
22
23
|
Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source or config, you can override and register it with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module.
|
|
23
24
|
|
|
24
25
|
**Code: Wiring up a custom Model Source**
|
|
@@ -40,16 +41,6 @@ where the [S3ModelConfig](https://github.com/travetto/travetto/tree/main/module/
|
|
|
40
41
|
|
|
41
42
|
**Code: Structure of S3ModelConfig**
|
|
42
43
|
```typescript
|
|
43
|
-
import { fromIni } from '@aws-sdk/credential-provider-ini';
|
|
44
|
-
import type s3 from '@aws-sdk/client-s3';
|
|
45
|
-
|
|
46
|
-
import { Config, EnvVar } from '@travetto/config';
|
|
47
|
-
import { Field, Required } from '@travetto/schema';
|
|
48
|
-
import { Runtime } from '@travetto/runtime';
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* S3 Support as an Asset Source
|
|
52
|
-
*/
|
|
53
44
|
@Config('model.s3')
|
|
54
45
|
export class S3ModelConfig {
|
|
55
46
|
region = 'us-east-1'; // AWS Region
|
package/__index__.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './src/config';
|
|
2
|
-
export * from './src/service';
|
|
1
|
+
export * from './src/config.ts';
|
|
2
|
+
export * from './src/service.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-s3",
|
|
3
|
-
"version": "6.0.0-rc.
|
|
3
|
+
"version": "6.0.0-rc.3",
|
|
4
4
|
"description": "S3 backing for the travetto model module.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"s3",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"directory": "module/model-s3"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aws-sdk/client-s3": "^3.
|
|
29
|
-
"@aws-sdk/credential-provider-ini": "^3.
|
|
30
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
31
|
-
"@travetto/cli": "^6.0.0-rc.
|
|
32
|
-
"@travetto/config": "^6.0.0-rc.
|
|
33
|
-
"@travetto/model": "^6.0.0-rc.
|
|
28
|
+
"@aws-sdk/client-s3": "^3.796.0",
|
|
29
|
+
"@aws-sdk/credential-provider-ini": "^3.796.0",
|
|
30
|
+
"@aws-sdk/s3-request-presigner": "^3.796.0",
|
|
31
|
+
"@travetto/cli": "^6.0.0-rc.3",
|
|
32
|
+
"@travetto/config": "^6.0.0-rc.2",
|
|
33
|
+
"@travetto/model": "^6.0.0-rc.2"
|
|
34
34
|
},
|
|
35
35
|
"travetto": {
|
|
36
36
|
"displayName": "S3 Model Support"
|
package/src/service.ts
CHANGED
|
@@ -9,18 +9,13 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
ModelCrudSupport, ModelStorageSupport, ModelType, ModelRegistry, ExistsError, NotFoundError, OptionalId,
|
|
12
|
-
ModelBlobSupport
|
|
12
|
+
ModelBlobSupport, ModelExpirySupport, ModelBlobUtil, ModelCrudUtil, ModelExpiryUtil, ModelStorageUtil,
|
|
13
|
+
|
|
13
14
|
} from '@travetto/model';
|
|
14
15
|
import { Injectable } from '@travetto/di';
|
|
15
16
|
import { Class, AppError, castTo, asFull, BlobMeta, ByteRange, BinaryInput, BinaryUtil, TimeSpan, TimeUtil } from '@travetto/runtime';
|
|
16
17
|
|
|
17
|
-
import {
|
|
18
|
-
import { ModelCrudUtil } from '@travetto/model/src/internal/service/crud';
|
|
19
|
-
import { ModelExpirySupport } from '@travetto/model/src/service/expiry';
|
|
20
|
-
import { ModelExpiryUtil } from '@travetto/model/src/internal/service/expiry';
|
|
21
|
-
import { ModelStorageUtil } from '@travetto/model/src/internal/service/storage';
|
|
22
|
-
|
|
23
|
-
import { S3ModelConfig } from './config';
|
|
18
|
+
import { S3ModelConfig } from './config.ts';
|
|
24
19
|
|
|
25
20
|
function isMetadataBearer(o: unknown): o is MetadataBearer {
|
|
26
21
|
return !!o && typeof o === 'object' && '$metadata' in o;
|
|
@@ -46,7 +41,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
46
41
|
|
|
47
42
|
constructor(config: S3ModelConfig) { this.config = config; }
|
|
48
43
|
|
|
49
|
-
#getMetaBase({ range, size, ...meta }: BlobMeta): MetaBase {
|
|
44
|
+
#getMetaBase({ range: _, size, ...meta }: BlobMeta): MetaBase {
|
|
50
45
|
return {
|
|
51
46
|
ContentType: meta.contentType,
|
|
52
47
|
...(meta.contentEncoding ? { ContentEncoding: meta.contentEncoding } : {}),
|
|
@@ -59,17 +54,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
59
54
|
};
|
|
60
55
|
}
|
|
61
56
|
|
|
62
|
-
#
|
|
63
|
-
let key: string;
|
|
64
|
-
if (cls === MODEL_BLOB) { // If we are streaming, treat as primary use case
|
|
65
|
-
key = id!; // Store it directly at root
|
|
66
|
-
} else {
|
|
67
|
-
key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
|
|
68
|
-
if (id) {
|
|
69
|
-
key = `${key}:${id}`;
|
|
70
|
-
}
|
|
71
|
-
key = `_data/${key}`; // Separate data
|
|
72
|
-
}
|
|
57
|
+
#basicKey(key: string): string {
|
|
73
58
|
if (key?.startsWith('/')) {
|
|
74
59
|
key = key.substring(1);
|
|
75
60
|
}
|
|
@@ -79,11 +64,26 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
79
64
|
return key;
|
|
80
65
|
}
|
|
81
66
|
|
|
67
|
+
#resolveKey(cls: Class | string, id?: string): string {
|
|
68
|
+
let key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
|
|
69
|
+
if (id) {
|
|
70
|
+
key = `${key}:${id}`;
|
|
71
|
+
}
|
|
72
|
+
key = `_data/${key}`; // Separate data
|
|
73
|
+
return this.#basicKey(key);
|
|
74
|
+
}
|
|
75
|
+
|
|
82
76
|
#q<U extends object>(cls: string | Class, id: string, extra: U = asFull({})): (U & { Key: string, Bucket: string }) {
|
|
83
77
|
const key = this.#resolveKey(cls, id);
|
|
84
78
|
return { Key: key, Bucket: this.config.bucket, ...extra };
|
|
85
79
|
}
|
|
86
80
|
|
|
81
|
+
#qBlob<U extends object>(id: string, extra: U = asFull({})): (U & { Key: string, Bucket: string }) {
|
|
82
|
+
const key = this.#basicKey(id);
|
|
83
|
+
return { Key: key, Bucket: this.config.bucket, ...extra };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
87
|
#getExpiryConfig<T extends ModelType>(cls: Class<T>, item: T): { Expires?: Date } {
|
|
88
88
|
if (ModelRegistry.get(cls).expiresAt) {
|
|
89
89
|
const { expiresAt } = ModelExpiryUtil.getExpiryState(cls, item);
|
|
@@ -117,7 +117,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
117
117
|
* Write multipart file upload, in chunks
|
|
118
118
|
*/
|
|
119
119
|
async #writeMultipart(id: string, input: Readable, meta: BlobMeta): Promise<void> {
|
|
120
|
-
const { UploadId } = await this.client.createMultipartUpload(this.#
|
|
120
|
+
const { UploadId } = await this.client.createMultipartUpload(this.#qBlob(id, this.#getMetaBase(meta)));
|
|
121
121
|
|
|
122
122
|
const parts: CompletedPart[] = [];
|
|
123
123
|
let buffers: Buffer[] = [];
|
|
@@ -125,7 +125,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
125
125
|
let n = 1;
|
|
126
126
|
const flush = async (): Promise<void> => {
|
|
127
127
|
if (!total) { return; }
|
|
128
|
-
const part = await this.client.uploadPart(this.#
|
|
128
|
+
const part = await this.client.uploadPart(this.#qBlob(id, {
|
|
129
129
|
Body: Buffer.concat(buffers),
|
|
130
130
|
PartNumber: n,
|
|
131
131
|
UploadId
|
|
@@ -146,12 +146,12 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
146
146
|
}
|
|
147
147
|
await flush();
|
|
148
148
|
|
|
149
|
-
await this.client.completeMultipartUpload(this.#
|
|
149
|
+
await this.client.completeMultipartUpload(this.#qBlob(id, {
|
|
150
150
|
UploadId,
|
|
151
151
|
MultipartUpload: { Parts: parts }
|
|
152
152
|
}));
|
|
153
153
|
} catch (err) {
|
|
154
|
-
await this.client.abortMultipartUpload(this.#
|
|
154
|
+
await this.client.abortMultipartUpload(this.#qBlob(id, { UploadId }));
|
|
155
155
|
throw err;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -196,9 +196,9 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
196
196
|
|
|
197
197
|
async head<T extends ModelType>(cls: Class<T>, id: string): Promise<boolean> {
|
|
198
198
|
try {
|
|
199
|
-
const
|
|
199
|
+
const result = await this.client.headObject(this.#q(cls, id));
|
|
200
200
|
const { expiresAt } = ModelRegistry.get(cls);
|
|
201
|
-
if (expiresAt &&
|
|
201
|
+
if (expiresAt && result.Expires && result.Expires.getTime() < Date.now()) {
|
|
202
202
|
return false;
|
|
203
203
|
}
|
|
204
204
|
return true;
|
|
@@ -308,7 +308,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
// Expiry
|
|
311
|
-
async deleteExpired<T extends ModelType>(
|
|
311
|
+
async deleteExpired<T extends ModelType>(_cls: Class<T>): Promise<number> {
|
|
312
312
|
return -1;
|
|
313
313
|
}
|
|
314
314
|
|
|
@@ -322,7 +322,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
322
322
|
|
|
323
323
|
if (blobMeta.size && blobMeta.size < this.config.chunkSize) { // If smaller than chunk size
|
|
324
324
|
// Upload to s3
|
|
325
|
-
await this.client.putObject(this.#
|
|
325
|
+
await this.client.putObject(this.#qBlob(location, {
|
|
326
326
|
Body: await toBuffer(stream),
|
|
327
327
|
ContentLength: blobMeta.size,
|
|
328
328
|
...this.#getMetaBase(blobMeta),
|
|
@@ -334,39 +334,39 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
334
334
|
|
|
335
335
|
async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
|
|
336
336
|
// Read from s3
|
|
337
|
-
const
|
|
337
|
+
const result = await this.client.getObject(this.#qBlob(location, range ? {
|
|
338
338
|
Range: `bytes=${range.start}-${range.end}`
|
|
339
339
|
} : {}));
|
|
340
340
|
|
|
341
|
-
if (!
|
|
341
|
+
if (!result.Body) {
|
|
342
342
|
throw new AppError('Unable to read type: undefined');
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
if (typeof
|
|
346
|
-
return Readable.from(
|
|
347
|
-
} else if (
|
|
348
|
-
return Readable.from(
|
|
349
|
-
} else if ('pipe' in
|
|
350
|
-
return castTo<Readable>(
|
|
345
|
+
if (typeof result.Body === 'string') { // string
|
|
346
|
+
return Readable.from(result.Body, { encoding: castTo<string>(result.Body).endsWith('=') ? 'base64' : 'utf8' });
|
|
347
|
+
} else if (result.Body instanceof Buffer) { // Buffer
|
|
348
|
+
return Readable.from(result.Body);
|
|
349
|
+
} else if ('pipe' in result.Body) { // Stream
|
|
350
|
+
return castTo<Readable>(result.Body);
|
|
351
351
|
}
|
|
352
|
-
throw new AppError(`Unable to read type: ${typeof
|
|
352
|
+
throw new AppError(`Unable to read type: ${typeof result.Body}`);
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
async getBlob(location: string, range?: ByteRange): Promise<Blob> {
|
|
356
356
|
const meta = await this.getBlobMeta(location);
|
|
357
357
|
const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
|
|
358
|
-
const
|
|
359
|
-
return BinaryUtil.readableBlob(
|
|
358
|
+
const result = (): Promise<Readable> => this.#getObject(location, final);
|
|
359
|
+
return BinaryUtil.readableBlob(result, { ...meta, range: final });
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
|
|
363
|
-
const query = this.#
|
|
363
|
+
const query = this.#qBlob(location);
|
|
364
364
|
try {
|
|
365
365
|
return (await this.client.headObject(query));
|
|
366
366
|
} catch (err) {
|
|
367
367
|
if (isMetadataBearer(err)) {
|
|
368
368
|
if (err.$metadata.httpStatusCode === 404) {
|
|
369
|
-
err = new NotFoundError(
|
|
369
|
+
err = new NotFoundError('Blob', location);
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
throw err;
|
|
@@ -388,19 +388,19 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
388
388
|
}
|
|
389
389
|
return ret;
|
|
390
390
|
} else {
|
|
391
|
-
throw new NotFoundError(
|
|
391
|
+
throw new NotFoundError('Blob', location);
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
394
|
|
|
395
395
|
async deleteBlob(location: string): Promise<void> {
|
|
396
|
-
await this.client.deleteObject(this.#
|
|
396
|
+
await this.client.deleteObject(this.#qBlob(location));
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
async updateBlobMeta(location: string, meta: BlobMeta): Promise<void> {
|
|
400
400
|
await this.client.copyObject({
|
|
401
401
|
Bucket: this.config.bucket,
|
|
402
|
-
Key: this.#
|
|
403
|
-
CopySource: `/${this.config.bucket}/${this.#
|
|
402
|
+
Key: this.#basicKey(location),
|
|
403
|
+
CopySource: `/${this.config.bucket}/${this.#basicKey(location)}`,
|
|
404
404
|
...this.#getMetaBase(meta),
|
|
405
405
|
MetadataDirective: 'REPLACE'
|
|
406
406
|
});
|
|
@@ -410,7 +410,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
410
410
|
async getBlobReadUrl(location: string, exp: TimeSpan = '1h'): Promise<string> {
|
|
411
411
|
return await getSignedUrl(
|
|
412
412
|
this.client,
|
|
413
|
-
new GetObjectCommand(this.#
|
|
413
|
+
new GetObjectCommand(this.#qBlob(location)),
|
|
414
414
|
{ expiresIn: TimeUtil.asSeconds(exp) }
|
|
415
415
|
);
|
|
416
416
|
}
|
|
@@ -420,7 +420,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
420
420
|
return await getSignedUrl(
|
|
421
421
|
this.client,
|
|
422
422
|
new PutObjectCommand({
|
|
423
|
-
...this.#
|
|
423
|
+
...this.#qBlob(location),
|
|
424
424
|
...base,
|
|
425
425
|
...(meta.size ? { ContentLength: meta.size } : {}),
|
|
426
426
|
...((meta.hash && meta.hash !== '-1') ? { ChecksumSHA256: meta.hash } : {}),
|