@travetto/model-s3 5.0.7 → 5.0.8

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.
Files changed (2) hide show
  1. package/package.json +3 -2
  2. package/src/service.ts +64 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-s3",
3
- "version": "5.0.7",
3
+ "version": "5.0.8",
4
4
  "description": "S3 backing for the travetto model module.",
5
5
  "keywords": [
6
6
  "s3",
@@ -26,9 +26,10 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@aws-sdk/client-s3": "^3.658.1",
29
+ "@aws-sdk/s3-request-presigner": "^3.658.1",
29
30
  "@aws-sdk/credential-provider-ini": "^3.609.0",
30
31
  "@travetto/config": "^5.0.7",
31
- "@travetto/model": "^5.0.7"
32
+ "@travetto/model": "^5.0.8"
32
33
  },
33
34
  "peerDependencies": {
34
35
  "@travetto/command": "^5.0.7"
package/src/service.ts CHANGED
@@ -2,16 +2,17 @@ import { Readable } from 'node:stream';
2
2
  import { text as toText } from 'node:stream/consumers';
3
3
  import { Agent } from 'node:https';
4
4
 
5
- import { S3, CompletedPart, type CreateMultipartUploadRequest } from '@aws-sdk/client-s3';
5
+ import { S3, CompletedPart, type CreateMultipartUploadRequest, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
6
6
  import type { MetadataBearer } from '@aws-sdk/types';
7
7
  import { NodeHttpHandler } from '@smithy/node-http-handler';
8
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
8
9
 
9
10
  import {
10
11
  ModelCrudSupport, ModelStorageSupport, ModelType, ModelRegistry, ExistsError, NotFoundError, OptionalId,
11
12
  ModelBlobSupport
12
13
  } from '@travetto/model';
13
14
  import { Injectable } from '@travetto/di';
14
- import { Class, AppError, castTo, asFull, BlobMeta, ByteRange, BinaryInput, BinaryUtil } from '@travetto/runtime';
15
+ import { Class, AppError, castTo, asFull, BlobMeta, ByteRange, BinaryInput, BinaryUtil, TimeSpan, TimeUtil } from '@travetto/runtime';
15
16
 
16
17
  import { MODEL_BLOB, ModelBlobUtil } from '@travetto/model/src/internal/service/blob';
17
18
  import { ModelCrudUtil } from '@travetto/model/src/internal/service/crud';
@@ -68,6 +69,9 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
68
69
  }
69
70
  key = `_data/${key}`; // Separate data
70
71
  }
72
+ if (key?.startsWith('/')) {
73
+ key = key.substring(1);
74
+ }
71
75
  if (this.config.namespace) {
72
76
  key = `${this.config.namespace}/${key}`;
73
77
  }
@@ -152,12 +156,26 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
152
156
  }
153
157
 
154
158
  async #deleteKeys(items: { Key: string }[]): Promise<void> {
155
- await this.client.deleteObjects({
156
- Bucket: this.config.bucket,
157
- Delete: {
158
- Objects: items
159
+ try {
160
+ await this.client.deleteObjects({
161
+ Bucket: this.config.bucket,
162
+ Delete: {
163
+ Objects: items
164
+ }
165
+ });
166
+ } catch (err) {
167
+ // Handle GCS
168
+ if (err instanceof Error && err.name === 'NotImplemented') {
169
+ for (const item of items) {
170
+ await this.client.deleteObject({
171
+ Bucket: this.config.bucket,
172
+ Key: item.Key
173
+ });
174
+ }
175
+ } else {
176
+ throw err;
159
177
  }
160
- });
178
+ }
161
179
  }
162
180
 
163
181
  async postConstruct(): Promise<void> {
@@ -293,7 +311,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
293
311
 
294
312
  // Blob support
295
313
  async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite = true): Promise<void> {
296
- if (!overwrite && await this.describeBlob(location).then(() => true, () => false)) {
314
+ if (!overwrite && await this.getBlobMeta(location).then(() => true, () => false)) {
297
315
  return;
298
316
  }
299
317
 
@@ -332,7 +350,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
332
350
  }
333
351
 
334
352
  async getBlob(location: string, range?: ByteRange): Promise<Blob> {
335
- const meta = await this.describeBlob(location);
353
+ const meta = await this.getBlobMeta(location);
336
354
  const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
337
355
  const res = (): Promise<Readable> => this.#getObject(location, final);
338
356
  return BinaryUtil.readableBlob(res, { ...meta, range: final });
@@ -352,7 +370,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
352
370
  }
353
371
  }
354
372
 
355
- async describeBlob(location: string): Promise<BlobMeta> {
373
+ async getBlobMeta(location: string): Promise<BlobMeta> {
356
374
  const obj = await this.headBlob(location);
357
375
 
358
376
  if (obj) {
@@ -375,6 +393,42 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
375
393
  await this.client.deleteObject(this.#q(MODEL_BLOB, location));
376
394
  }
377
395
 
396
+ async updateBlobMeta(location: string, meta: BlobMeta): Promise<void> {
397
+ await this.client.copyObject({
398
+ Bucket: this.config.bucket,
399
+ Key: this.#resolveKey(MODEL_BLOB, location),
400
+ CopySource: `/${this.config.bucket}/${this.#resolveKey(MODEL_BLOB, location)}`,
401
+ ...this.#getMetaBase(meta),
402
+ MetadataDirective: 'REPLACE'
403
+ });
404
+ }
405
+
406
+ // Signed urls
407
+ async getBlobReadUrl(location: string, exp: TimeSpan = '1h'): Promise<string> {
408
+ return await getSignedUrl(
409
+ this.client,
410
+ new GetObjectCommand(this.#q(MODEL_BLOB, location)),
411
+ { expiresIn: TimeUtil.asSeconds(exp) }
412
+ );
413
+ }
414
+
415
+ async getBlobWriteUrl(location: string, meta: BlobMeta, exp: TimeSpan = '1h'): Promise<string> {
416
+ const base = this.#getMetaBase(meta);
417
+ return await getSignedUrl(
418
+ this.client,
419
+ new PutObjectCommand({
420
+ ...this.#q(MODEL_BLOB, location),
421
+ ...base,
422
+ ...(meta.size ? { ContentLength: meta.size } : {}),
423
+ ...((meta.hash && meta.hash !== '-1') ? { ChecksumSHA256: meta.hash } : {}),
424
+ }),
425
+ {
426
+ expiresIn: TimeUtil.asSeconds(exp),
427
+ ...(meta.contentType ? { signableHeaders: new Set(['content-type']) } : {})
428
+ }
429
+ );
430
+ }
431
+
378
432
  // Storage
379
433
  async truncateModel<T extends ModelType>(model: Class<T>): Promise<void> {
380
434
  for await (const items of this.#iterateBucket(model)) {