@travetto/model-s3 5.0.0-rc.10 → 5.0.0-rc.11
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 +1 -1
- package/package.json +5 -5
- package/src/service.ts +50 -42
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ This module provides an [s3](https://aws.amazon.com/documentation/s3/)-based imp
|
|
|
17
17
|
|
|
18
18
|
Supported features:
|
|
19
19
|
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11)
|
|
20
|
-
* [
|
|
20
|
+
* [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/service/blob.ts#L8)
|
|
21
21
|
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/service/expiry.ts#L11)
|
|
22
22
|
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
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-s3",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.11",
|
|
4
4
|
"description": "S3 backing for the travetto model module.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"s3",
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"directory": "module/model-s3"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aws-sdk/client-s3": "^3.
|
|
28
|
+
"@aws-sdk/client-s3": "^3.631.0",
|
|
29
29
|
"@aws-sdk/credential-provider-ini": "^3.609.0",
|
|
30
|
-
"@travetto/config": "^5.0.0-rc.
|
|
31
|
-
"@travetto/model": "^5.0.0-rc.
|
|
30
|
+
"@travetto/config": "^5.0.0-rc.11",
|
|
31
|
+
"@travetto/model": "^5.0.0-rc.11"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/command": "^5.0.0-rc.
|
|
34
|
+
"@travetto/command": "^5.0.0-rc.10"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/command": {
|
package/src/service.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
|
-
import {
|
|
2
|
+
import { text as toText } from 'node:stream/consumers';
|
|
3
3
|
import { Agent } from 'node:https';
|
|
4
4
|
|
|
5
5
|
import { S3, CompletedPart, type CreateMultipartUploadRequest } from '@aws-sdk/client-s3';
|
|
@@ -7,20 +7,19 @@ import type { MetadataBearer } from '@aws-sdk/types';
|
|
|
7
7
|
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
ModelCrudSupport,
|
|
11
|
-
|
|
12
|
-
StreamRange
|
|
10
|
+
ModelCrudSupport, ModelStorageSupport, ModelType, ModelRegistry, ExistsError, NotFoundError, OptionalId,
|
|
11
|
+
ModelBlobSupport, ModelBlobUtil,
|
|
13
12
|
} from '@travetto/model';
|
|
14
13
|
import { Injectable } from '@travetto/di';
|
|
15
|
-
import { Class, AppError, castTo, asFull } from '@travetto/runtime';
|
|
14
|
+
import { Class, AppError, castTo, asFull, BlobMeta, ByteRange, BinaryInput, BinaryUtil } from '@travetto/runtime';
|
|
16
15
|
|
|
16
|
+
import { MODEL_BLOB, ModelBlobNamespace } from '@travetto/model/src/internal/service/blob';
|
|
17
17
|
import { ModelCrudUtil } from '@travetto/model/src/internal/service/crud';
|
|
18
18
|
import { ModelExpirySupport } from '@travetto/model/src/service/expiry';
|
|
19
19
|
import { ModelExpiryUtil } from '@travetto/model/src/internal/service/expiry';
|
|
20
20
|
import { ModelStorageUtil } from '@travetto/model/src/internal/service/storage';
|
|
21
21
|
|
|
22
22
|
import { S3ModelConfig } from './config';
|
|
23
|
-
import { enforceRange } from '@travetto/model/src/internal/service/stream';
|
|
24
23
|
|
|
25
24
|
function isMetadataBearer(o: unknown): o is MetadataBearer {
|
|
26
25
|
return !!o && typeof o === 'object' && '$metadata' in o;
|
|
@@ -30,8 +29,6 @@ function hasContentType<T>(o: T): o is T & { contenttype?: string } {
|
|
|
30
29
|
return o !== undefined && o !== null && Object.hasOwn(o, 'contenttype');
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
const STREAM_SPACE = '@travetto/model-s3:stream';
|
|
34
|
-
|
|
35
32
|
type MetaBase = Pick<CreateMultipartUploadRequest,
|
|
36
33
|
'ContentType' | 'Metadata' | 'ContentEncoding' | 'ContentLanguage' | 'CacheControl' | 'ContentDisposition'
|
|
37
34
|
>;
|
|
@@ -40,14 +37,14 @@ type MetaBase = Pick<CreateMultipartUploadRequest,
|
|
|
40
37
|
* Asset source backed by S3
|
|
41
38
|
*/
|
|
42
39
|
@Injectable()
|
|
43
|
-
export class S3ModelService implements ModelCrudSupport,
|
|
40
|
+
export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, ModelStorageSupport, ModelExpirySupport {
|
|
44
41
|
|
|
45
42
|
idSource = ModelCrudUtil.uuidSource();
|
|
46
43
|
client: S3;
|
|
47
44
|
|
|
48
45
|
constructor(public readonly config: S3ModelConfig) { }
|
|
49
46
|
|
|
50
|
-
#getMetaBase(meta:
|
|
47
|
+
#getMetaBase({ range, ...meta }: BlobMeta): MetaBase {
|
|
51
48
|
return {
|
|
52
49
|
ContentType: meta.contentType,
|
|
53
50
|
...(meta.contentEncoding ? { ContentEncoding: meta.contentEncoding } : {}),
|
|
@@ -62,7 +59,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
62
59
|
|
|
63
60
|
#resolveKey(cls: Class | string, id?: string): string {
|
|
64
61
|
let key: string;
|
|
65
|
-
if (cls ===
|
|
62
|
+
if (cls === MODEL_BLOB) { // If we are streaming, treat as primary use case
|
|
66
63
|
key = id!; // Store it directly at root
|
|
67
64
|
} else {
|
|
68
65
|
key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
|
|
@@ -110,8 +107,8 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
110
107
|
/**
|
|
111
108
|
* Write multipart file upload, in chunks
|
|
112
109
|
*/
|
|
113
|
-
async #writeMultipart(id: string, input: Readable, meta:
|
|
114
|
-
const { UploadId } = await this.client.createMultipartUpload(this.#q(
|
|
110
|
+
async #writeMultipart(id: string, input: Readable, meta: BlobMeta): Promise<void> {
|
|
111
|
+
const { UploadId } = await this.client.createMultipartUpload(this.#q(MODEL_BLOB, id, this.#getMetaBase(meta)));
|
|
115
112
|
|
|
116
113
|
const parts: CompletedPart[] = [];
|
|
117
114
|
let buffers: Buffer[] = [];
|
|
@@ -119,7 +116,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
119
116
|
let n = 1;
|
|
120
117
|
const flush = async (): Promise<void> => {
|
|
121
118
|
if (!total) { return; }
|
|
122
|
-
const part = await this.client.uploadPart(this.#q(
|
|
119
|
+
const part = await this.client.uploadPart(this.#q(MODEL_BLOB, id, {
|
|
123
120
|
Body: Buffer.concat(buffers),
|
|
124
121
|
PartNumber: n,
|
|
125
122
|
UploadId
|
|
@@ -139,12 +136,12 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
139
136
|
}
|
|
140
137
|
await flush();
|
|
141
138
|
|
|
142
|
-
await this.client.completeMultipartUpload(this.#q(
|
|
139
|
+
await this.client.completeMultipartUpload(this.#q(MODEL_BLOB, id, {
|
|
143
140
|
UploadId,
|
|
144
141
|
MultipartUpload: { Parts: parts }
|
|
145
142
|
}));
|
|
146
143
|
} catch (err) {
|
|
147
|
-
await this.client.abortMultipartUpload(this.#q(
|
|
144
|
+
await this.client.abortMultipartUpload(this.#q(MODEL_BLOB, id, { UploadId }));
|
|
148
145
|
throw err;
|
|
149
146
|
}
|
|
150
147
|
}
|
|
@@ -296,22 +293,33 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
296
293
|
return -1;
|
|
297
294
|
}
|
|
298
295
|
|
|
299
|
-
|
|
300
|
-
|
|
296
|
+
// Blob support
|
|
297
|
+
async insertBlob(location: string, input: BinaryInput, meta?: BlobMeta, errorIfExisting = false): Promise<void> {
|
|
298
|
+
await this.describeBlob(location);
|
|
299
|
+
if (errorIfExisting) {
|
|
300
|
+
throw new ExistsError(ModelBlobNamespace, location);
|
|
301
|
+
}
|
|
302
|
+
return this.upsertBlob(location, input, meta);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta): Promise<void> {
|
|
306
|
+
const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
|
|
307
|
+
|
|
308
|
+
if (blobMeta.size && blobMeta.size < this.config.chunkSize) { // If smaller than chunk size
|
|
301
309
|
// Upload to s3
|
|
302
|
-
await this.client.putObject(this.#q(
|
|
303
|
-
Body:
|
|
304
|
-
ContentLength:
|
|
305
|
-
...this.#getMetaBase(
|
|
310
|
+
await this.client.putObject(this.#q(MODEL_BLOB, location, {
|
|
311
|
+
Body: stream,
|
|
312
|
+
ContentLength: blobMeta.size,
|
|
313
|
+
...this.#getMetaBase(blobMeta),
|
|
306
314
|
}));
|
|
307
315
|
} else {
|
|
308
|
-
await this.#writeMultipart(location,
|
|
316
|
+
await this.#writeMultipart(location, stream, blobMeta);
|
|
309
317
|
}
|
|
310
318
|
}
|
|
311
319
|
|
|
312
|
-
async #getObject(location: string, range
|
|
320
|
+
async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
|
|
313
321
|
// Read from s3
|
|
314
|
-
const res = await this.client.getObject(this.#q(
|
|
322
|
+
const res = await this.client.getObject(this.#q(MODEL_BLOB, location, range ? {
|
|
315
323
|
Range: `bytes=${range.start}-${range.end}`
|
|
316
324
|
} : {}));
|
|
317
325
|
|
|
@@ -329,33 +337,32 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
329
337
|
throw new AppError(`Unable to read type: ${typeof res.Body}`);
|
|
330
338
|
}
|
|
331
339
|
|
|
332
|
-
async
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
return this.#getObject(location, castTo(range));
|
|
340
|
+
async getBlob(location: string, range?: ByteRange): Promise<Blob> {
|
|
341
|
+
const meta = await this.describeBlob(location);
|
|
342
|
+
const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
|
|
343
|
+
const res = (): Promise<Readable> => this.#getObject(location, final);
|
|
344
|
+
return BinaryUtil.readableBlob(res, { ...meta, range: final });
|
|
338
345
|
}
|
|
339
346
|
|
|
340
|
-
async
|
|
341
|
-
const query = this.#q(
|
|
347
|
+
async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
|
|
348
|
+
const query = this.#q(MODEL_BLOB, location);
|
|
342
349
|
try {
|
|
343
350
|
return (await this.client.headObject(query));
|
|
344
351
|
} catch (err) {
|
|
345
352
|
if (isMetadataBearer(err)) {
|
|
346
353
|
if (err.$metadata.httpStatusCode === 404) {
|
|
347
|
-
err = new NotFoundError(
|
|
354
|
+
err = new NotFoundError(MODEL_BLOB, location);
|
|
348
355
|
}
|
|
349
356
|
}
|
|
350
357
|
throw err;
|
|
351
358
|
}
|
|
352
359
|
}
|
|
353
360
|
|
|
354
|
-
async
|
|
355
|
-
const obj = await this.
|
|
361
|
+
async describeBlob(location: string): Promise<BlobMeta> {
|
|
362
|
+
const obj = await this.headBlob(location);
|
|
356
363
|
|
|
357
364
|
if (obj) {
|
|
358
|
-
const ret:
|
|
365
|
+
const ret: BlobMeta = {
|
|
359
366
|
contentType: '',
|
|
360
367
|
...obj.Metadata,
|
|
361
368
|
size: obj.ContentLength!,
|
|
@@ -366,20 +373,21 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
366
373
|
}
|
|
367
374
|
return ret;
|
|
368
375
|
} else {
|
|
369
|
-
throw new NotFoundError(
|
|
376
|
+
throw new NotFoundError(MODEL_BLOB, location);
|
|
370
377
|
}
|
|
371
378
|
}
|
|
372
379
|
|
|
380
|
+
async deleteBlob(location: string): Promise<void> {
|
|
381
|
+
await this.client.deleteObject(this.#q(MODEL_BLOB, location));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Storage
|
|
373
385
|
async truncateModel<T extends ModelType>(model: Class<T>): Promise<void> {
|
|
374
386
|
for await (const items of this.#iterateBucket(model)) {
|
|
375
387
|
await this.#deleteKeys(items);
|
|
376
388
|
}
|
|
377
389
|
}
|
|
378
390
|
|
|
379
|
-
async deleteStream(location: string): Promise<void> {
|
|
380
|
-
await this.client.deleteObject(this.#q(STREAM_SPACE, location));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
391
|
async createStorage(): Promise<void> {
|
|
384
392
|
try {
|
|
385
393
|
await this.client.headBucket({ Bucket: this.config.bucket });
|