@travetto/model-s3 5.0.0-rc.1 → 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 +3 -3
- package/package.json +5 -5
- package/src/config.ts +2 -2
- package/src/service.ts +56 -56
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
|
|
|
@@ -45,7 +45,7 @@ import type s3 from '@aws-sdk/client-s3';
|
|
|
45
45
|
|
|
46
46
|
import { Config, EnvVar } from '@travetto/config';
|
|
47
47
|
import { Field, Required } from '@travetto/schema';
|
|
48
|
-
import {
|
|
48
|
+
import { Runtime } from '@travetto/runtime';
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* S3 Support as an Asset Source
|
|
@@ -100,7 +100,7 @@ export class S3ModelConfig {
|
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
// We are in localhost and not in prod, turn on forcePathStyle
|
|
103
|
-
if (!
|
|
103
|
+
if (!Runtime.production && this.endpoint.includes('localhost')) {
|
|
104
104
|
this.config.forcePathStyle ??= true;
|
|
105
105
|
}
|
|
106
106
|
}
|
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/config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type s3 from '@aws-sdk/client-s3';
|
|
|
3
3
|
|
|
4
4
|
import { Config, EnvVar } from '@travetto/config';
|
|
5
5
|
import { Field, Required } from '@travetto/schema';
|
|
6
|
-
import {
|
|
6
|
+
import { Runtime } from '@travetto/runtime';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* S3 Support as an Asset Source
|
|
@@ -58,7 +58,7 @@ export class S3ModelConfig {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
// We are in localhost and not in prod, turn on forcePathStyle
|
|
61
|
-
if (!
|
|
61
|
+
if (!Runtime.production && this.endpoint.includes('localhost')) {
|
|
62
62
|
this.config.forcePathStyle ??= true;
|
|
63
63
|
}
|
|
64
64
|
}
|
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 } from '@travetto/
|
|
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);
|
|
@@ -77,8 +74,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
77
74
|
return key;
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
#q<U extends object>(cls: string | Class, id: string, extra: U = {} as U): (U & { Key: string, Bucket: string }) {
|
|
77
|
+
#q<U extends object>(cls: string | Class, id: string, extra: U = asFull({})): (U & { Key: string, Bucket: string }) {
|
|
82
78
|
const key = this.#resolveKey(cls, id);
|
|
83
79
|
return { Key: key, Bucket: this.config.bucket, ...extra };
|
|
84
80
|
}
|
|
@@ -111,8 +107,8 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
111
107
|
/**
|
|
112
108
|
* Write multipart file upload, in chunks
|
|
113
109
|
*/
|
|
114
|
-
async #writeMultipart(id: string, input: Readable, meta:
|
|
115
|
-
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)));
|
|
116
112
|
|
|
117
113
|
const parts: CompletedPart[] = [];
|
|
118
114
|
let buffers: Buffer[] = [];
|
|
@@ -120,7 +116,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
120
116
|
let n = 1;
|
|
121
117
|
const flush = async (): Promise<void> => {
|
|
122
118
|
if (!total) { return; }
|
|
123
|
-
const part = await this.client.uploadPart(this.#q(
|
|
119
|
+
const part = await this.client.uploadPart(this.#q(MODEL_BLOB, id, {
|
|
124
120
|
Body: Buffer.concat(buffers),
|
|
125
121
|
PartNumber: n,
|
|
126
122
|
UploadId
|
|
@@ -140,12 +136,12 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
140
136
|
}
|
|
141
137
|
await flush();
|
|
142
138
|
|
|
143
|
-
await this.client.completeMultipartUpload(this.#q(
|
|
139
|
+
await this.client.completeMultipartUpload(this.#q(MODEL_BLOB, id, {
|
|
144
140
|
UploadId,
|
|
145
141
|
MultipartUpload: { Parts: parts }
|
|
146
142
|
}));
|
|
147
143
|
} catch (err) {
|
|
148
|
-
await this.client.abortMultipartUpload(this.#q(
|
|
144
|
+
await this.client.abortMultipartUpload(this.#q(MODEL_BLOB, id, { UploadId }));
|
|
149
145
|
throw err;
|
|
150
146
|
}
|
|
151
147
|
}
|
|
@@ -203,8 +199,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
203
199
|
try {
|
|
204
200
|
const result = await this.client.getObject(this.#q(cls, id));
|
|
205
201
|
if (result.Body) {
|
|
206
|
-
|
|
207
|
-
const body = (await toBuffer(result.Body as Readable)).toString('utf8');
|
|
202
|
+
const body = await toText(castTo(result.Body));
|
|
208
203
|
const output = await ModelCrudUtil.load(cls, body);
|
|
209
204
|
if (output) {
|
|
210
205
|
const { expiresAt } = ModelRegistry.get(cls);
|
|
@@ -230,8 +225,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
230
225
|
}
|
|
231
226
|
|
|
232
227
|
async store<T extends ModelType>(cls: Class<T>, item: OptionalId<T>, preStore = true): Promise<T> {
|
|
233
|
-
|
|
234
|
-
let prepped: T = item as T;
|
|
228
|
+
let prepped: T = castTo(item);
|
|
235
229
|
if (preStore) {
|
|
236
230
|
prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
237
231
|
}
|
|
@@ -299,22 +293,33 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
299
293
|
return -1;
|
|
300
294
|
}
|
|
301
295
|
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
304
309
|
// Upload to s3
|
|
305
|
-
await this.client.putObject(this.#q(
|
|
306
|
-
Body:
|
|
307
|
-
ContentLength:
|
|
308
|
-
...this.#getMetaBase(
|
|
310
|
+
await this.client.putObject(this.#q(MODEL_BLOB, location, {
|
|
311
|
+
Body: stream,
|
|
312
|
+
ContentLength: blobMeta.size,
|
|
313
|
+
...this.#getMetaBase(blobMeta),
|
|
309
314
|
}));
|
|
310
315
|
} else {
|
|
311
|
-
await this.#writeMultipart(location,
|
|
316
|
+
await this.#writeMultipart(location, stream, blobMeta);
|
|
312
317
|
}
|
|
313
318
|
}
|
|
314
319
|
|
|
315
|
-
async #getObject(location: string, range
|
|
320
|
+
async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
|
|
316
321
|
// Read from s3
|
|
317
|
-
const res = await this.client.getObject(this.#q(
|
|
322
|
+
const res = await this.client.getObject(this.#q(MODEL_BLOB, location, range ? {
|
|
318
323
|
Range: `bytes=${range.start}-${range.end}`
|
|
319
324
|
} : {}));
|
|
320
325
|
|
|
@@ -323,49 +328,43 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
323
328
|
}
|
|
324
329
|
|
|
325
330
|
if (typeof res.Body === 'string') { // string
|
|
326
|
-
|
|
327
|
-
return Readable.from(res.Body, { encoding: (res.Body as string).endsWith('=') ? 'base64' : 'utf8' });
|
|
331
|
+
return Readable.from(res.Body, { encoding: castTo<string>(res.Body).endsWith('=') ? 'base64' : 'utf8' });
|
|
328
332
|
} else if (res.Body instanceof Buffer) { // Buffer
|
|
329
333
|
return Readable.from(res.Body);
|
|
330
334
|
} else if ('pipe' in res.Body) { // Stream
|
|
331
|
-
|
|
332
|
-
return res.Body as Readable;
|
|
335
|
+
return castTo<Readable>(res.Body);
|
|
333
336
|
}
|
|
334
337
|
throw new AppError(`Unable to read type: ${typeof res.Body}`);
|
|
335
338
|
}
|
|
336
339
|
|
|
337
|
-
async
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
343
|
-
return this.#getObject(location, range as Required<StreamRange>);
|
|
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 });
|
|
344
345
|
}
|
|
345
346
|
|
|
346
|
-
async
|
|
347
|
-
const query = this.#q(
|
|
347
|
+
async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
|
|
348
|
+
const query = this.#q(MODEL_BLOB, location);
|
|
348
349
|
try {
|
|
349
350
|
return (await this.client.headObject(query));
|
|
350
351
|
} catch (err) {
|
|
351
352
|
if (isMetadataBearer(err)) {
|
|
352
353
|
if (err.$metadata.httpStatusCode === 404) {
|
|
353
|
-
err = new NotFoundError(
|
|
354
|
+
err = new NotFoundError(MODEL_BLOB, location);
|
|
354
355
|
}
|
|
355
356
|
}
|
|
356
357
|
throw err;
|
|
357
358
|
}
|
|
358
359
|
}
|
|
359
360
|
|
|
360
|
-
async
|
|
361
|
-
const obj = await this.
|
|
361
|
+
async describeBlob(location: string): Promise<BlobMeta> {
|
|
362
|
+
const obj = await this.headBlob(location);
|
|
362
363
|
|
|
363
364
|
if (obj) {
|
|
364
|
-
const ret:
|
|
365
|
-
// @ts-expect-error
|
|
365
|
+
const ret: BlobMeta = {
|
|
366
366
|
contentType: '',
|
|
367
|
-
|
|
368
|
-
...obj.Metadata as StreamMeta,
|
|
367
|
+
...obj.Metadata,
|
|
369
368
|
size: obj.ContentLength!,
|
|
370
369
|
};
|
|
371
370
|
if (hasContentType(ret)) {
|
|
@@ -374,20 +373,21 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
374
373
|
}
|
|
375
374
|
return ret;
|
|
376
375
|
} else {
|
|
377
|
-
throw new NotFoundError(
|
|
376
|
+
throw new NotFoundError(MODEL_BLOB, location);
|
|
378
377
|
}
|
|
379
378
|
}
|
|
380
379
|
|
|
380
|
+
async deleteBlob(location: string): Promise<void> {
|
|
381
|
+
await this.client.deleteObject(this.#q(MODEL_BLOB, location));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Storage
|
|
381
385
|
async truncateModel<T extends ModelType>(model: Class<T>): Promise<void> {
|
|
382
386
|
for await (const items of this.#iterateBucket(model)) {
|
|
383
387
|
await this.#deleteKeys(items);
|
|
384
388
|
}
|
|
385
389
|
}
|
|
386
390
|
|
|
387
|
-
async deleteStream(location: string): Promise<void> {
|
|
388
|
-
await this.client.deleteObject(this.#q(STREAM_SPACE, location));
|
|
389
|
-
}
|
|
390
|
-
|
|
391
391
|
async createStorage(): Promise<void> {
|
|
392
392
|
try {
|
|
393
393
|
await this.client.headBucket({ Bucket: this.config.bucket });
|