@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 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.9",
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.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.9",
31
- "@travetto/model": "^5.0.0-rc.9"
30
+ "@travetto/config": "^5.0.0",
31
+ "@travetto/model": "^5.0.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/command": "^5.0.0-rc.9"
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 { 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
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, 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,29 @@ 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 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(STREAM_SPACE, location, {
303
- Body: await toBuffer(input),
304
- ContentLength: meta.size,
305
- ...this.#getMetaBase(meta),
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, input, meta);
313
+ await this.#writeMultipart(location, stream, blobMeta);
309
314
  }
310
315
  }
311
316
 
312
- async #getObject(location: string, range: Required<StreamRange>): Promise<Readable> {
317
+ async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
313
318
  // Read from s3
314
- const res = await this.client.getObject(this.#q(STREAM_SPACE, location, range ? {
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 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));
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 headStream(location: string): Promise<{ Metadata?: Partial<StreamMeta>, ContentLength?: number }> {
341
- const query = this.#q(STREAM_SPACE, location);
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(STREAM_SPACE, location);
351
+ err = new NotFoundError(MODEL_BLOB, location);
348
352
  }
349
353
  }
350
354
  throw err;
351
355
  }
352
356
  }
353
357
 
354
- async describeStream(location: string): Promise<StreamMeta> {
355
- const obj = await this.headStream(location);
358
+ async describeBlob(location: string): Promise<BlobMeta> {
359
+ const obj = await this.headBlob(location);
356
360
 
357
361
  if (obj) {
358
- const ret: StreamMeta = {
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(STREAM_SPACE, location);
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 });