@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 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.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.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, 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: `${meta.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 === 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
@@ -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
- buffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
135
- total += chunk.length;
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(STREAM_SPACE, id, {
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(STREAM_SPACE, id, { UploadId }));
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
- async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
300
- if (meta.size < this.config.chunkSize) { // If smaller than chunk size
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(STREAM_SPACE, location, {
303
- Body: await toBuffer(input),
304
- ContentLength: meta.size,
305
- ...this.#getMetaBase(meta),
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, input, meta);
317
+ await this.#writeMultipart(location, stream, blobMeta);
309
318
  }
310
319
  }
311
320
 
312
- async #getObject(location: string, range: Required<StreamRange>): Promise<Readable> {
321
+ async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
313
322
  // Read from s3
314
- const res = await this.client.getObject(this.#q(STREAM_SPACE, location, range ? {
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 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));
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 headStream(location: string): Promise<{ Metadata?: Partial<StreamMeta>, ContentLength?: number }> {
341
- const query = this.#q(STREAM_SPACE, location);
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(STREAM_SPACE, location);
355
+ err = new NotFoundError(MODEL_BLOB, location);
348
356
  }
349
357
  }
350
358
  throw err;
351
359
  }
352
360
  }
353
361
 
354
- async describeStream(location: string): Promise<StreamMeta> {
355
- const obj = await this.headStream(location);
362
+ async describeBlob(location: string): Promise<BlobMeta> {
363
+ const obj = await this.headBlob(location);
356
364
 
357
365
  if (obj) {
358
- const ret: StreamMeta = {
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(STREAM_SPACE, location);
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 });