@travetto/model-s3 5.0.0-rc.9 → 5.0.1
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 +65 -63
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.
|
|
3
|
+
"version": "5.0.1",
|
|
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.637.0",
|
|
29
29
|
"@aws-sdk/credential-provider-ini": "^3.609.0",
|
|
30
|
-
"@travetto/config": "^5.0.
|
|
31
|
-
"@travetto/model": "^5.0.
|
|
30
|
+
"@travetto/config": "^5.0.1",
|
|
31
|
+
"@travetto/model": "^5.0.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/command": "^5.0.
|
|
34
|
+
"@travetto/command": "^5.0.1"
|
|
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);
|
|
@@ -95,9 +92,13 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
95
92
|
async * #iterateBucket(cls?: string | Class): AsyncIterable<{ Key: string, id: string }[]> {
|
|
96
93
|
let Marker: string | undefined;
|
|
97
94
|
for (; ;) {
|
|
98
|
-
const obs = await this.client.listObjects({
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
const obs = await this.client.listObjects({
|
|
96
|
+
Bucket: this.config.bucket,
|
|
97
|
+
Prefix: cls ? this.#resolveKey(cls) : this.config.namespace,
|
|
98
|
+
Marker
|
|
99
|
+
});
|
|
100
|
+
if (obs.Contents?.length) {
|
|
101
|
+
yield obs.Contents.map(o => ({ Key: o.Key!, id: o.Key!.split(':').pop()! }));
|
|
101
102
|
}
|
|
102
103
|
if (obs.NextMarker) {
|
|
103
104
|
Marker = obs.NextMarker;
|
|
@@ -110,8 +111,8 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
110
111
|
/**
|
|
111
112
|
* Write multipart file upload, in chunks
|
|
112
113
|
*/
|
|
113
|
-
async #writeMultipart(id: string, input: Readable, meta:
|
|
114
|
-
const { UploadId } = await this.client.createMultipartUpload(this.#q(
|
|
114
|
+
async #writeMultipart(id: string, input: Readable, meta: BlobMeta): Promise<void> {
|
|
115
|
+
const { UploadId } = await this.client.createMultipartUpload(this.#q(MODEL_BLOB, id, this.#getMetaBase(meta)));
|
|
115
116
|
|
|
116
117
|
const parts: CompletedPart[] = [];
|
|
117
118
|
let buffers: Buffer[] = [];
|
|
@@ -119,7 +120,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
119
120
|
let n = 1;
|
|
120
121
|
const flush = async (): Promise<void> => {
|
|
121
122
|
if (!total) { return; }
|
|
122
|
-
const part = await this.client.uploadPart(this.#q(
|
|
123
|
+
const part = await this.client.uploadPart(this.#q(MODEL_BLOB, id, {
|
|
123
124
|
Body: Buffer.concat(buffers),
|
|
124
125
|
PartNumber: n,
|
|
125
126
|
UploadId
|
|
@@ -131,38 +132,32 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
131
132
|
};
|
|
132
133
|
try {
|
|
133
134
|
for await (const chunk of input) {
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
const chunked = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
136
|
+
buffers.push(chunked);
|
|
137
|
+
total += chunked.length;
|
|
136
138
|
if (total > this.config.chunkSize) {
|
|
137
139
|
await flush();
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
await flush();
|
|
141
143
|
|
|
142
|
-
await this.client.completeMultipartUpload(this.#q(
|
|
144
|
+
await this.client.completeMultipartUpload(this.#q(MODEL_BLOB, id, {
|
|
143
145
|
UploadId,
|
|
144
146
|
MultipartUpload: { Parts: parts }
|
|
145
147
|
}));
|
|
146
148
|
} catch (err) {
|
|
147
|
-
await this.client.abortMultipartUpload(this.#q(
|
|
149
|
+
await this.client.abortMultipartUpload(this.#q(MODEL_BLOB, id, { UploadId }));
|
|
148
150
|
throw err;
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
async #deleteKeys(items: { Key: string }[]): Promise<void> {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
await this.client.deleteObjects({
|
|
160
|
-
Bucket: this.config.bucket,
|
|
161
|
-
Delete: {
|
|
162
|
-
Objects: items
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
}
|
|
155
|
+
await this.client.deleteObjects({
|
|
156
|
+
Bucket: this.config.bucket,
|
|
157
|
+
Delete: {
|
|
158
|
+
Objects: items
|
|
159
|
+
}
|
|
160
|
+
});
|
|
166
161
|
}
|
|
167
162
|
|
|
168
163
|
async postConstruct(): Promise<void> {
|
|
@@ -265,7 +260,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
265
260
|
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
266
261
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
267
262
|
const id = item.id;
|
|
268
|
-
const prepped = await ModelCrudUtil.naivePartialUpdate(cls,
|
|
263
|
+
const prepped = await ModelCrudUtil.naivePartialUpdate(cls, () => this.get(cls, id), item, view);
|
|
269
264
|
return this.store<T>(cls, prepped, false);
|
|
270
265
|
}
|
|
271
266
|
|
|
@@ -296,22 +291,29 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
296
291
|
return -1;
|
|
297
292
|
}
|
|
298
293
|
|
|
299
|
-
|
|
300
|
-
|
|
294
|
+
// Blob support
|
|
295
|
+
async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite = true): Promise<void> {
|
|
296
|
+
if (!overwrite && await this.describeBlob(location).then(() => true, () => false)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
|
|
301
|
+
|
|
302
|
+
if (blobMeta.size && blobMeta.size < this.config.chunkSize) { // If smaller than chunk size
|
|
301
303
|
// Upload to s3
|
|
302
|
-
await this.client.putObject(this.#q(
|
|
303
|
-
Body:
|
|
304
|
-
ContentLength:
|
|
305
|
-
...this.#getMetaBase(
|
|
304
|
+
await this.client.putObject(this.#q(MODEL_BLOB, location, {
|
|
305
|
+
Body: stream,
|
|
306
|
+
ContentLength: blobMeta.size,
|
|
307
|
+
...this.#getMetaBase(blobMeta),
|
|
306
308
|
}));
|
|
307
309
|
} else {
|
|
308
|
-
await this.#writeMultipart(location,
|
|
310
|
+
await this.#writeMultipart(location, stream, blobMeta);
|
|
309
311
|
}
|
|
310
312
|
}
|
|
311
313
|
|
|
312
|
-
async #getObject(location: string, range
|
|
314
|
+
async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
|
|
313
315
|
// Read from s3
|
|
314
|
-
const res = await this.client.getObject(this.#q(
|
|
316
|
+
const res = await this.client.getObject(this.#q(MODEL_BLOB, location, range ? {
|
|
315
317
|
Range: `bytes=${range.start}-${range.end}`
|
|
316
318
|
} : {}));
|
|
317
319
|
|
|
@@ -329,33 +331,32 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
329
331
|
throw new AppError(`Unable to read type: ${typeof res.Body}`);
|
|
330
332
|
}
|
|
331
333
|
|
|
332
|
-
async
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
return this.#getObject(location, castTo(range));
|
|
334
|
+
async getBlob(location: string, range?: ByteRange): Promise<Blob> {
|
|
335
|
+
const meta = await this.describeBlob(location);
|
|
336
|
+
const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
|
|
337
|
+
const res = (): Promise<Readable> => this.#getObject(location, final);
|
|
338
|
+
return BinaryUtil.readableBlob(res, { ...meta, range: final });
|
|
338
339
|
}
|
|
339
340
|
|
|
340
|
-
async
|
|
341
|
-
const query = this.#q(
|
|
341
|
+
async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
|
|
342
|
+
const query = this.#q(MODEL_BLOB, location);
|
|
342
343
|
try {
|
|
343
344
|
return (await this.client.headObject(query));
|
|
344
345
|
} catch (err) {
|
|
345
346
|
if (isMetadataBearer(err)) {
|
|
346
347
|
if (err.$metadata.httpStatusCode === 404) {
|
|
347
|
-
err = new NotFoundError(
|
|
348
|
+
err = new NotFoundError(MODEL_BLOB, location);
|
|
348
349
|
}
|
|
349
350
|
}
|
|
350
351
|
throw err;
|
|
351
352
|
}
|
|
352
353
|
}
|
|
353
354
|
|
|
354
|
-
async
|
|
355
|
-
const obj = await this.
|
|
355
|
+
async describeBlob(location: string): Promise<BlobMeta> {
|
|
356
|
+
const obj = await this.headBlob(location);
|
|
356
357
|
|
|
357
358
|
if (obj) {
|
|
358
|
-
const ret:
|
|
359
|
+
const ret: BlobMeta = {
|
|
359
360
|
contentType: '',
|
|
360
361
|
...obj.Metadata,
|
|
361
362
|
size: obj.ContentLength!,
|
|
@@ -366,20 +367,21 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
366
367
|
}
|
|
367
368
|
return ret;
|
|
368
369
|
} else {
|
|
369
|
-
throw new NotFoundError(
|
|
370
|
+
throw new NotFoundError(MODEL_BLOB, location);
|
|
370
371
|
}
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
async deleteBlob(location: string): Promise<void> {
|
|
375
|
+
await this.client.deleteObject(this.#q(MODEL_BLOB, location));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Storage
|
|
373
379
|
async truncateModel<T extends ModelType>(model: Class<T>): Promise<void> {
|
|
374
380
|
for await (const items of this.#iterateBucket(model)) {
|
|
375
381
|
await this.#deleteKeys(items);
|
|
376
382
|
}
|
|
377
383
|
}
|
|
378
384
|
|
|
379
|
-
async deleteStream(location: string): Promise<void> {
|
|
380
|
-
await this.client.deleteObject(this.#q(STREAM_SPACE, location));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
385
|
async createStorage(): Promise<void> {
|
|
384
386
|
try {
|
|
385
387
|
await this.client.headBucket({ Bucket: this.config.bucket });
|
|
@@ -390,7 +392,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
390
392
|
|
|
391
393
|
async deleteStorage(): Promise<void> {
|
|
392
394
|
if (this.config.namespace) {
|
|
393
|
-
for await (const items of this.#iterateBucket(
|
|
395
|
+
for await (const items of this.#iterateBucket()) {
|
|
394
396
|
await this.#deleteKeys(items);
|
|
395
397
|
}
|
|
396
398
|
} else {
|