@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 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.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.609.0",
28
+ "@aws-sdk/client-s3": "^3.637.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.1",
31
+ "@travetto/model": "^5.0.1"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/command": "^5.0.0-rc.9"
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 { 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);
@@ -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({ Bucket: this.config.bucket, Prefix: cls ? this.#resolveKey(cls) : undefined, Marker });
99
- if (obs.Contents && obs.Contents.length) {
100
- yield (obs.Contents ?? []).map(o => ({ Key: o.Key!, id: o.Key!.split(':').pop()! }));
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: StreamMeta): Promise<void> {
114
- const { UploadId } = await this.client.createMultipartUpload(this.#q(STREAM_SPACE, id, this.#getMetaBase(meta)));
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(STREAM_SPACE, id, {
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
- buffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
135
- total += chunk.length;
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(STREAM_SPACE, id, {
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(STREAM_SPACE, id, { UploadId }));
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
- if (this.config.endpoint.includes('localhost')) {
154
- await Promise.all(items.map(item => this.client.deleteObject({
155
- Bucket: this.config.bucket,
156
- Key: item.Key
157
- })));
158
- } else {
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, item, view, (): Promise<T> => this.get(cls, id));
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
- async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
300
- if (meta.size < this.config.chunkSize) { // If smaller than chunk size
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(STREAM_SPACE, location, {
303
- Body: await toBuffer(input),
304
- ContentLength: meta.size,
305
- ...this.#getMetaBase(meta),
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, input, meta);
310
+ await this.#writeMultipart(location, stream, blobMeta);
309
311
  }
310
312
  }
311
313
 
312
- async #getObject(location: string, range: Required<StreamRange>): Promise<Readable> {
314
+ async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
313
315
  // Read from s3
314
- const res = await this.client.getObject(this.#q(STREAM_SPACE, location, range ? {
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 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));
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 headStream(location: string): Promise<{ Metadata?: Partial<StreamMeta>, ContentLength?: number }> {
341
- const query = this.#q(STREAM_SPACE, location);
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(STREAM_SPACE, location);
348
+ err = new NotFoundError(MODEL_BLOB, location);
348
349
  }
349
350
  }
350
351
  throw err;
351
352
  }
352
353
  }
353
354
 
354
- async describeStream(location: string): Promise<StreamMeta> {
355
- const obj = await this.headStream(location);
355
+ async describeBlob(location: string): Promise<BlobMeta> {
356
+ const obj = await this.headBlob(location);
356
357
 
357
358
  if (obj) {
358
- const ret: StreamMeta = {
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(STREAM_SPACE, location);
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 {