@travetto/model-s3 5.0.0-rc.10 → 5.0.0-rc.12
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 +54 -45
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.12",
|
|
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, size, ...meta }: BlobMeta): MetaBase {
|
|
51
48
|
return {
|
|
52
49
|
ContentType: meta.contentType,
|
|
53
50
|
...(meta.contentEncoding ? { ContentEncoding: meta.contentEncoding } : {}),
|
|
@@ -55,14 +52,14 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
55
52
|
...(meta.cacheControl ? { CacheControl: meta.cacheControl } : {}),
|
|
56
53
|
Metadata: {
|
|
57
54
|
...meta,
|
|
58
|
-
size: `${
|
|
55
|
+
...(size ? { size: `${size}` } : {})
|
|
59
56
|
}
|
|
60
57
|
};
|
|
61
58
|
}
|
|
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
|
|
@@ -131,20 +128,21 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
131
128
|
};
|
|
132
129
|
try {
|
|
133
130
|
for await (const chunk of input) {
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
const chunked = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
132
|
+
buffers.push(chunked);
|
|
133
|
+
total += chunked.length;
|
|
136
134
|
if (total > this.config.chunkSize) {
|
|
137
135
|
await flush();
|
|
138
136
|
}
|
|
139
137
|
}
|
|
140
138
|
await flush();
|
|
141
139
|
|
|
142
|
-
await this.client.completeMultipartUpload(this.#q(
|
|
140
|
+
await this.client.completeMultipartUpload(this.#q(MODEL_BLOB, id, {
|
|
143
141
|
UploadId,
|
|
144
142
|
MultipartUpload: { Parts: parts }
|
|
145
143
|
}));
|
|
146
144
|
} catch (err) {
|
|
147
|
-
await this.client.abortMultipartUpload(this.#q(
|
|
145
|
+
await this.client.abortMultipartUpload(this.#q(MODEL_BLOB, id, { UploadId }));
|
|
148
146
|
throw err;
|
|
149
147
|
}
|
|
150
148
|
}
|
|
@@ -296,22 +294,33 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
296
294
|
return -1;
|
|
297
295
|
}
|
|
298
296
|
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
// Blob support
|
|
298
|
+
async insertBlob(location: string, input: BinaryInput, meta?: BlobMeta, errorIfExisting = false): Promise<void> {
|
|
299
|
+
await this.describeBlob(location);
|
|
300
|
+
if (errorIfExisting) {
|
|
301
|
+
throw new ExistsError(ModelBlobNamespace, location);
|
|
302
|
+
}
|
|
303
|
+
return this.upsertBlob(location, input, meta);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta): Promise<void> {
|
|
307
|
+
const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
|
|
308
|
+
|
|
309
|
+
if (blobMeta.size && blobMeta.size < this.config.chunkSize) { // If smaller than chunk size
|
|
301
310
|
// Upload to s3
|
|
302
|
-
await this.client.putObject(this.#q(
|
|
303
|
-
Body:
|
|
304
|
-
ContentLength:
|
|
305
|
-
...this.#getMetaBase(
|
|
311
|
+
await this.client.putObject(this.#q(MODEL_BLOB, location, {
|
|
312
|
+
Body: stream,
|
|
313
|
+
ContentLength: blobMeta.size,
|
|
314
|
+
...this.#getMetaBase(blobMeta),
|
|
306
315
|
}));
|
|
307
316
|
} else {
|
|
308
|
-
await this.#writeMultipart(location,
|
|
317
|
+
await this.#writeMultipart(location, stream, blobMeta);
|
|
309
318
|
}
|
|
310
319
|
}
|
|
311
320
|
|
|
312
|
-
async #getObject(location: string, range
|
|
321
|
+
async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
|
|
313
322
|
// Read from s3
|
|
314
|
-
const res = await this.client.getObject(this.#q(
|
|
323
|
+
const res = await this.client.getObject(this.#q(MODEL_BLOB, location, range ? {
|
|
315
324
|
Range: `bytes=${range.start}-${range.end}`
|
|
316
325
|
} : {}));
|
|
317
326
|
|
|
@@ -329,33 +338,32 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
329
338
|
throw new AppError(`Unable to read type: ${typeof res.Body}`);
|
|
330
339
|
}
|
|
331
340
|
|
|
332
|
-
async
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
return this.#getObject(location, castTo(range));
|
|
341
|
+
async getBlob(location: string, range?: ByteRange): Promise<Blob> {
|
|
342
|
+
const meta = await this.describeBlob(location);
|
|
343
|
+
const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
|
|
344
|
+
const res = (): Promise<Readable> => this.#getObject(location, final);
|
|
345
|
+
return BinaryUtil.readableBlob(res, { ...meta, range: final });
|
|
338
346
|
}
|
|
339
347
|
|
|
340
|
-
async
|
|
341
|
-
const query = this.#q(
|
|
348
|
+
async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
|
|
349
|
+
const query = this.#q(MODEL_BLOB, location);
|
|
342
350
|
try {
|
|
343
351
|
return (await this.client.headObject(query));
|
|
344
352
|
} catch (err) {
|
|
345
353
|
if (isMetadataBearer(err)) {
|
|
346
354
|
if (err.$metadata.httpStatusCode === 404) {
|
|
347
|
-
err = new NotFoundError(
|
|
355
|
+
err = new NotFoundError(MODEL_BLOB, location);
|
|
348
356
|
}
|
|
349
357
|
}
|
|
350
358
|
throw err;
|
|
351
359
|
}
|
|
352
360
|
}
|
|
353
361
|
|
|
354
|
-
async
|
|
355
|
-
const obj = await this.
|
|
362
|
+
async describeBlob(location: string): Promise<BlobMeta> {
|
|
363
|
+
const obj = await this.headBlob(location);
|
|
356
364
|
|
|
357
365
|
if (obj) {
|
|
358
|
-
const ret:
|
|
366
|
+
const ret: BlobMeta = {
|
|
359
367
|
contentType: '',
|
|
360
368
|
...obj.Metadata,
|
|
361
369
|
size: obj.ContentLength!,
|
|
@@ -366,20 +374,21 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
366
374
|
}
|
|
367
375
|
return ret;
|
|
368
376
|
} else {
|
|
369
|
-
throw new NotFoundError(
|
|
377
|
+
throw new NotFoundError(MODEL_BLOB, location);
|
|
370
378
|
}
|
|
371
379
|
}
|
|
372
380
|
|
|
381
|
+
async deleteBlob(location: string): Promise<void> {
|
|
382
|
+
await this.client.deleteObject(this.#q(MODEL_BLOB, location));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Storage
|
|
373
386
|
async truncateModel<T extends ModelType>(model: Class<T>): Promise<void> {
|
|
374
387
|
for await (const items of this.#iterateBucket(model)) {
|
|
375
388
|
await this.#deleteKeys(items);
|
|
376
389
|
}
|
|
377
390
|
}
|
|
378
391
|
|
|
379
|
-
async deleteStream(location: string): Promise<void> {
|
|
380
|
-
await this.client.deleteObject(this.#q(STREAM_SPACE, location));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
392
|
async createStorage(): Promise<void> {
|
|
384
393
|
try {
|
|
385
394
|
await this.client.headBucket({ Bucket: this.config.bucket });
|