@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 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
- * [Streaming](https://github.com/travetto/travetto/tree/main/module/model/src/service/stream.ts#L3)
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.10",
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.609.0",
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.10",
31
- "@travetto/model": "^5.0.0-rc.10"
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.9"
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 { buffer as toBuffer, text as toText } from 'node:stream/consumers';
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, ModelStreamSupport, ModelStorageSupport, StreamMeta,
11
- ModelType, ModelRegistry, ExistsError, NotFoundError, OptionalId,
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, ModelStreamSupport, ModelStorageSupport, ModelExpirySupport {
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: StreamMeta): MetaBase {
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 === STREAM_SPACE) { // If we are streaming, treat as primary use case
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: StreamMeta): Promise<void> {
114
- const { UploadId } = await this.client.createMultipartUpload(this.#q(STREAM_SPACE, id, this.#getMetaBase(meta)));
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(STREAM_SPACE, id, {
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(STREAM_SPACE, id, {
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(STREAM_SPACE, id, { UploadId }));
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
- async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
300
- if (meta.size < this.config.chunkSize) { // If smaller than chunk size
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(STREAM_SPACE, location, {
303
- Body: await toBuffer(input),
304
- ContentLength: meta.size,
305
- ...this.#getMetaBase(meta),
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, input, meta);
316
+ await this.#writeMultipart(location, stream, blobMeta);
309
317
  }
310
318
  }
311
319
 
312
- async #getObject(location: string, range: Required<StreamRange>): Promise<Readable> {
320
+ async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
313
321
  // Read from s3
314
- const res = await this.client.getObject(this.#q(STREAM_SPACE, location, range ? {
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 getStream(location: string, range?: StreamRange): Promise<Readable> {
333
- if (range) {
334
- const meta = await this.describeStream(location);
335
- range = enforceRange(range, meta.size);
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 headStream(location: string): Promise<{ Metadata?: Partial<StreamMeta>, ContentLength?: number }> {
341
- const query = this.#q(STREAM_SPACE, location);
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(STREAM_SPACE, location);
354
+ err = new NotFoundError(MODEL_BLOB, location);
348
355
  }
349
356
  }
350
357
  throw err;
351
358
  }
352
359
  }
353
360
 
354
- async describeStream(location: string): Promise<StreamMeta> {
355
- const obj = await this.headStream(location);
361
+ async describeBlob(location: string): Promise<BlobMeta> {
362
+ const obj = await this.headBlob(location);
356
363
 
357
364
  if (obj) {
358
- const ret: StreamMeta = {
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(STREAM_SPACE, location);
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 });