@travetto/model-s3 5.0.0-rc.9 → 5.0.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/README.md +1 -1
- package/package.json +5 -5
- package/src/service.ts +50 -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
|
|
3
|
+
"version": "5.0.0",
|
|
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
|
|
31
|
-
"@travetto/model": "^5.0.0
|
|
30
|
+
"@travetto/config": "^5.0.0",
|
|
31
|
+
"@travetto/model": "^5.0.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/command": "^5.0.0
|
|
34
|
+
"@travetto/command": "^5.0.0"
|
|
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
|
|
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, ModelBlobUtil } 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,29 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
296
294
|
return -1;
|
|
297
295
|
}
|
|
298
296
|
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
// Blob support
|
|
298
|
+
async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite = true): Promise<void> {
|
|
299
|
+
if (!overwrite && await this.describeBlob(location).then(() => true, () => false)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
|
|
304
|
+
|
|
305
|
+
if (blobMeta.size && blobMeta.size < this.config.chunkSize) { // If smaller than chunk size
|
|
301
306
|
// Upload to s3
|
|
302
|
-
await this.client.putObject(this.#q(
|
|
303
|
-
Body:
|
|
304
|
-
ContentLength:
|
|
305
|
-
...this.#getMetaBase(
|
|
307
|
+
await this.client.putObject(this.#q(MODEL_BLOB, location, {
|
|
308
|
+
Body: stream,
|
|
309
|
+
ContentLength: blobMeta.size,
|
|
310
|
+
...this.#getMetaBase(blobMeta),
|
|
306
311
|
}));
|
|
307
312
|
} else {
|
|
308
|
-
await this.#writeMultipart(location,
|
|
313
|
+
await this.#writeMultipart(location, stream, blobMeta);
|
|
309
314
|
}
|
|
310
315
|
}
|
|
311
316
|
|
|
312
|
-
async #getObject(location: string, range
|
|
317
|
+
async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
|
|
313
318
|
// Read from s3
|
|
314
|
-
const res = await this.client.getObject(this.#q(
|
|
319
|
+
const res = await this.client.getObject(this.#q(MODEL_BLOB, location, range ? {
|
|
315
320
|
Range: `bytes=${range.start}-${range.end}`
|
|
316
321
|
} : {}));
|
|
317
322
|
|
|
@@ -329,33 +334,32 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
329
334
|
throw new AppError(`Unable to read type: ${typeof res.Body}`);
|
|
330
335
|
}
|
|
331
336
|
|
|
332
|
-
async
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
return this.#getObject(location, castTo(range));
|
|
337
|
+
async getBlob(location: string, range?: ByteRange): Promise<Blob> {
|
|
338
|
+
const meta = await this.describeBlob(location);
|
|
339
|
+
const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
|
|
340
|
+
const res = (): Promise<Readable> => this.#getObject(location, final);
|
|
341
|
+
return BinaryUtil.readableBlob(res, { ...meta, range: final });
|
|
338
342
|
}
|
|
339
343
|
|
|
340
|
-
async
|
|
341
|
-
const query = this.#q(
|
|
344
|
+
async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
|
|
345
|
+
const query = this.#q(MODEL_BLOB, location);
|
|
342
346
|
try {
|
|
343
347
|
return (await this.client.headObject(query));
|
|
344
348
|
} catch (err) {
|
|
345
349
|
if (isMetadataBearer(err)) {
|
|
346
350
|
if (err.$metadata.httpStatusCode === 404) {
|
|
347
|
-
err = new NotFoundError(
|
|
351
|
+
err = new NotFoundError(MODEL_BLOB, location);
|
|
348
352
|
}
|
|
349
353
|
}
|
|
350
354
|
throw err;
|
|
351
355
|
}
|
|
352
356
|
}
|
|
353
357
|
|
|
354
|
-
async
|
|
355
|
-
const obj = await this.
|
|
358
|
+
async describeBlob(location: string): Promise<BlobMeta> {
|
|
359
|
+
const obj = await this.headBlob(location);
|
|
356
360
|
|
|
357
361
|
if (obj) {
|
|
358
|
-
const ret:
|
|
362
|
+
const ret: BlobMeta = {
|
|
359
363
|
contentType: '',
|
|
360
364
|
...obj.Metadata,
|
|
361
365
|
size: obj.ContentLength!,
|
|
@@ -366,20 +370,21 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
366
370
|
}
|
|
367
371
|
return ret;
|
|
368
372
|
} else {
|
|
369
|
-
throw new NotFoundError(
|
|
373
|
+
throw new NotFoundError(MODEL_BLOB, location);
|
|
370
374
|
}
|
|
371
375
|
}
|
|
372
376
|
|
|
377
|
+
async deleteBlob(location: string): Promise<void> {
|
|
378
|
+
await this.client.deleteObject(this.#q(MODEL_BLOB, location));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Storage
|
|
373
382
|
async truncateModel<T extends ModelType>(model: Class<T>): Promise<void> {
|
|
374
383
|
for await (const items of this.#iterateBucket(model)) {
|
|
375
384
|
await this.#deleteKeys(items);
|
|
376
385
|
}
|
|
377
386
|
}
|
|
378
387
|
|
|
379
|
-
async deleteStream(location: string): Promise<void> {
|
|
380
|
-
await this.client.deleteObject(this.#q(STREAM_SPACE, location));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
388
|
async createStorage(): Promise<void> {
|
|
384
389
|
try {
|
|
385
390
|
await this.client.headBucket({ Bucket: this.config.bucket });
|