@travetto/model-s3 7.1.4 → 8.0.0-alpha.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.
Files changed (2) hide show
  1. package/package.json +7 -7
  2. package/src/service.ts +65 -68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-s3",
3
- "version": "7.1.4",
3
+ "version": "8.0.0-alpha.0",
4
4
  "type": "module",
5
5
  "description": "S3 backing for the travetto model module.",
6
6
  "keywords": [
@@ -26,14 +26,14 @@
26
26
  "directory": "module/model-s3"
27
27
  },
28
28
  "dependencies": {
29
- "@aws-sdk/client-s3": "^3.966.0",
30
- "@aws-sdk/credential-provider-ini": "^3.966.0",
31
- "@aws-sdk/s3-request-presigner": "^3.966.0",
32
- "@travetto/config": "^7.1.4",
33
- "@travetto/model": "^7.1.4"
29
+ "@aws-sdk/client-s3": "^3.1000.0",
30
+ "@aws-sdk/credential-provider-ini": "^3.972.13",
31
+ "@aws-sdk/s3-request-presigner": "^3.1000.0",
32
+ "@travetto/config": "^8.0.0-alpha.0",
33
+ "@travetto/model": "^8.0.0-alpha.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/cli": "^7.1.4"
36
+ "@travetto/cli": "^8.0.0-alpha.0"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@travetto/cli": {
package/src/service.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { Readable } from 'node:stream';
2
- import { text as toText, buffer as toBuffer } from 'node:stream/consumers';
3
1
  import { Agent } from 'node:https';
4
2
 
5
3
  import { S3, type CompletedPart, type CreateMultipartUploadRequest, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
@@ -9,12 +7,12 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
9
7
 
10
8
  import {
11
9
  type ModelCrudSupport, type ModelStorageSupport, type ModelType, ModelRegistryIndex, ExistsError, NotFoundError, type OptionalId,
12
- type ModelBlobSupport, type ModelExpirySupport, ModelBlobUtil, ModelCrudUtil, ModelExpiryUtil, ModelStorageUtil
10
+ type ModelBlobSupport, type ModelExpirySupport, ModelCrudUtil, ModelExpiryUtil, ModelStorageUtil
13
11
  } from '@travetto/model';
14
12
  import { Injectable } from '@travetto/di';
15
13
  import {
16
- type Class, AppError, castTo, asFull, type BlobMeta,
17
- type ByteRange, type BinaryInput, BinaryUtil, type TimeSpan, TimeUtil
14
+ type Class, RuntimeError, castTo, asFull, type BinaryMetadata, type ByteRange, type BinaryType,
15
+ BinaryUtil, type TimeSpan, TimeUtil, type BinaryArray, CodecUtil, BinaryMetadataUtil, TypedObject, JSONUtil
18
16
  } from '@travetto/runtime';
19
17
 
20
18
  import type { S3ModelConfig } from './config.ts';
@@ -27,7 +25,7 @@ function hasContentType<T>(value: T): value is T & { contenttype?: string } {
27
25
  return value !== undefined && value !== null && Object.hasOwn(value, 'contenttype');
28
26
  }
29
27
 
30
- type MetaBase = Pick<CreateMultipartUploadRequest,
28
+ type S3Metadata = Pick<CreateMultipartUploadRequest,
31
29
  'ContentType' | 'Metadata' | 'ContentEncoding' | 'ContentLanguage' | 'CacheControl' | 'ContentDisposition'
32
30
  >;
33
31
 
@@ -43,16 +41,16 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
43
41
 
44
42
  constructor(config: S3ModelConfig) { this.config = config; }
45
43
 
46
- #getMetaBase({ range: _, size, ...meta }: BlobMeta): MetaBase {
44
+ #getMetadata(metadata: BinaryMetadata): S3Metadata {
47
45
  return {
48
- ContentType: meta.contentType,
49
- ...(meta.contentEncoding ? { ContentEncoding: meta.contentEncoding } : {}),
50
- ...(meta.contentLanguage ? { ContentLanguage: meta.contentLanguage } : {}),
51
- ...(meta.cacheControl ? { CacheControl: meta.cacheControl } : {}),
52
- Metadata: {
53
- ...meta,
54
- ...(size ? { size: `${size}` } : {})
55
- }
46
+ ContentType: metadata.contentType,
47
+ ...(metadata.contentEncoding ? { ContentEncoding: metadata.contentEncoding } : {}),
48
+ ...(metadata.contentLanguage ? { ContentLanguage: metadata.contentLanguage } : {}),
49
+ ...(metadata.cacheControl ? { CacheControl: metadata.cacheControl } : {}),
50
+ Metadata: TypedObject.fromEntries(
51
+ TypedObject.entries(metadata)
52
+ .map(([key, value]) => [key, typeof value === 'string' ? value : JSONUtil.toUTF8(value)] as const)
53
+ )
56
54
  };
57
55
  }
58
56
 
@@ -117,17 +115,17 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
117
115
  /**
118
116
  * Write multipart file upload, in chunks
119
117
  */
120
- async #writeMultipart(id: string, input: Readable, meta: BlobMeta): Promise<void> {
121
- const { UploadId } = await this.client.createMultipartUpload(this.#queryBlob(id, this.#getMetaBase(meta)));
118
+ async #writeMultipart(id: string, input: BinaryType, metadata: BinaryMetadata): Promise<void> {
119
+ const { UploadId } = await this.client.createMultipartUpload(this.#queryBlob(id, this.#getMetadata(metadata)));
122
120
 
123
121
  const parts: CompletedPart[] = [];
124
- let buffers: Buffer[] = [];
122
+ let buffers: BinaryArray[] = [];
125
123
  let total = 0;
126
124
  let i = 1;
127
125
  const flush = async (): Promise<void> => {
128
126
  if (!total) { return; }
129
127
  const part = await this.client.uploadPart(this.#queryBlob(id, {
130
- Body: Buffer.concat(buffers),
128
+ Body: BinaryUtil.binaryArrayToUint8Array(BinaryUtil.combineBinaryArrays(buffers)),
131
129
  PartNumber: i,
132
130
  UploadId
133
131
  }));
@@ -137,10 +135,10 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
137
135
  total = 0;
138
136
  };
139
137
  try {
140
- for await (const chunk of input) {
141
- const chunked = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
138
+ for await (const chunk of BinaryUtil.toBinaryStream(input)) {
139
+ const chunked = CodecUtil.readUtf8Chunk(chunk);
142
140
  buffers.push(chunked);
143
- total += chunked.length;
141
+ total += chunked.byteLength;
144
142
  if (total > this.config.chunkSize) {
145
143
  await flush();
146
144
  }
@@ -217,7 +215,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
217
215
  try {
218
216
  const result = await this.client.getObject(this.#query(cls, id));
219
217
  if (result.Body) {
220
- const body = await toText(castTo(result.Body));
218
+ const body = await BinaryUtil.toBinaryArray(result.Body);
221
219
  const output = await ModelCrudUtil.load(cls, body);
222
220
  if (output) {
223
221
  const { expiresAt } = ModelRegistryIndex.getConfig(cls);
@@ -247,11 +245,11 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
247
245
  if (preStore) {
248
246
  prepped = await ModelCrudUtil.preStore(cls, item, this);
249
247
  }
250
- const content = Buffer.from(JSON.stringify(prepped), 'utf8');
248
+ const content = JSONUtil.toBinaryArray(prepped);
251
249
  await this.client.putObject(this.#query(cls, prepped.id, {
252
- Body: content,
250
+ Body: BinaryUtil.binaryArrayToUint8Array(content),
253
251
  ContentType: 'application/json',
254
- ContentLength: content.length,
252
+ ContentLength: content.byteLength,
255
253
  ...this.#getExpiryConfig(cls, prepped)
256
254
  }));
257
255
  return prepped;
@@ -314,53 +312,52 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
314
312
  }
315
313
 
316
314
  // Blob support
317
- async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite = true): Promise<void> {
318
- if (!overwrite && await this.getBlobMeta(location).then(() => true, () => false)) {
315
+ async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
316
+ if (!overwrite && await this.getBlobMetadata(location).then(() => true, () => false)) {
319
317
  return;
320
318
  }
321
319
 
322
- const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
320
+ const resolved = await BinaryMetadataUtil.compute(input, metadata);
321
+
322
+ const length = BinaryMetadataUtil.readLength(resolved);
323
323
 
324
- if (blobMeta.size && blobMeta.size < this.config.chunkSize) { // If smaller than chunk size
324
+ if (length && length < this.config.chunkSize) { // If smaller than chunk size
325
+ const blob = this.#queryBlob(location, {
326
+ Body: BinaryUtil.toReadable(input),
327
+ ContentLength: length,
328
+ ...this.#getMetadata(resolved),
329
+ });
325
330
  // Upload to s3
326
- await this.client.putObject(this.#queryBlob(location, {
327
- Body: await toBuffer(stream),
328
- ContentLength: blobMeta.size,
329
- ...this.#getMetaBase(blobMeta),
330
- }));
331
+ await this.client.putObject(blob);
331
332
  } else {
332
- await this.#writeMultipart(location, stream, blobMeta);
333
+ await this.#writeMultipart(location, input, resolved);
333
334
  }
334
335
  }
335
336
 
336
- async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
337
+ async #getObject(location: string, range?: Required<ByteRange>): Promise<BinaryType> {
337
338
  // Read from s3
338
339
  const result = await this.client.getObject(this.#queryBlob(location, range ? {
339
340
  Range: `bytes=${range.start}-${range.end}`
340
341
  } : {}));
341
342
 
342
- if (!result.Body) {
343
- throw new AppError('Unable to read type: undefined');
344
- }
343
+ const body: BinaryType | string | undefined = castTo(result.Body);
345
344
 
346
- if (typeof result.Body === 'string') { // string
347
- return Readable.from(result.Body, { encoding: castTo<string>(result.Body).endsWith('=') ? 'base64' : 'utf8' });
348
- } else if (result.Body instanceof Buffer) { // Buffer
349
- return Readable.from(result.Body);
350
- } else if ('pipe' in result.Body) { // Stream
351
- return castTo<Readable>(result.Body);
345
+ switch (typeof body) {
346
+ case 'undefined': throw new RuntimeError('Unable to read type: undefined');
347
+ case 'string': return body.endsWith('=') ?
348
+ CodecUtil.fromBase64String(body) :
349
+ CodecUtil.fromUTF8String(body);
350
+ default: return body;
352
351
  }
353
- throw new AppError(`Unable to read type: ${typeof result.Body}`);
354
352
  }
355
353
 
356
354
  async getBlob(location: string, range?: ByteRange): Promise<Blob> {
357
- const meta = await this.getBlobMeta(location);
358
- const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
359
- const result = (): Promise<Readable> => this.#getObject(location, final);
360
- return BinaryUtil.readableBlob(result, { ...meta, range: final });
355
+ const metadata = await this.getBlobMetadata(location);
356
+ const final = range ? BinaryMetadataUtil.enforceRange(range, metadata) : undefined;
357
+ return BinaryMetadataUtil.makeBlob(() => this.#getObject(location, final), { ...metadata, range: final });
361
358
  }
362
359
 
363
- async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
360
+ async headBlob(location: string): Promise<{ Metadata?: BinaryMetadata, ContentLength?: number }> {
364
361
  const query = this.#queryBlob(location);
365
362
  try {
366
363
  return (await this.client.headObject(query));
@@ -374,20 +371,20 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
374
371
  }
375
372
  }
376
373
 
377
- async getBlobMeta(location: string): Promise<BlobMeta> {
374
+ async getBlobMetadata(location: string): Promise<BinaryMetadata> {
378
375
  const blob = await this.headBlob(location);
379
376
 
380
377
  if (blob) {
381
- const meta: BlobMeta = {
378
+ const metadata: BinaryMetadata = {
382
379
  contentType: '',
383
380
  ...blob.Metadata,
384
381
  size: blob.ContentLength!,
385
382
  };
386
- if (hasContentType(meta)) {
387
- meta['contentType'] = meta['contenttype']!;
388
- delete meta['contenttype'];
383
+ if (hasContentType(metadata)) {
384
+ metadata['contentType'] = metadata['contenttype']!;
385
+ delete metadata['contenttype'];
389
386
  }
390
- return meta;
387
+ return metadata;
391
388
  } else {
392
389
  throw new NotFoundError('Blob', location);
393
390
  }
@@ -397,38 +394,38 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
397
394
  await this.client.deleteObject(this.#queryBlob(location));
398
395
  }
399
396
 
400
- async updateBlobMeta(location: string, meta: BlobMeta): Promise<void> {
397
+ async updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void> {
401
398
  await this.client.copyObject({
402
399
  Bucket: this.config.bucket,
403
400
  Key: this.#basicKey(location),
404
401
  CopySource: `/${this.config.bucket}/${this.#basicKey(location)}`,
405
- ...this.#getMetaBase(meta),
402
+ ...this.#getMetadata(metadata),
406
403
  MetadataDirective: 'REPLACE'
407
404
  });
408
405
  }
409
406
 
410
407
  // Signed urls
411
- async getBlobReadUrl(location: string, exp: TimeSpan = '1h'): Promise<string> {
408
+ async getBlobReadUrl(location: string, expiresIn: TimeSpan = '1h'): Promise<string> {
412
409
  return await getSignedUrl(
413
410
  this.client,
414
411
  new GetObjectCommand(this.#queryBlob(location)),
415
- { expiresIn: TimeUtil.asSeconds(exp) }
412
+ { expiresIn: TimeUtil.duration(expiresIn, 's') }
416
413
  );
417
414
  }
418
415
 
419
- async getBlobWriteUrl(location: string, meta: BlobMeta, exp: TimeSpan = '1h'): Promise<string> {
420
- const base = this.#getMetaBase(meta);
416
+ async getBlobWriteUrl(location: string, metadata: BinaryMetadata, expiresIn: TimeSpan = '1h'): Promise<string> {
417
+ const base = this.#getMetadata(metadata);
421
418
  return await getSignedUrl(
422
419
  this.client,
423
420
  new PutObjectCommand({
424
421
  ...this.#queryBlob(location),
425
422
  ...base,
426
- ...(meta.size ? { ContentLength: meta.size } : {}),
427
- ...((meta.hash && meta.hash !== '-1') ? { ChecksumSHA256: meta.hash } : {}),
423
+ ...(metadata.size ? { ContentLength: metadata.size } : {}),
424
+ ...((metadata.hash && metadata.hash !== '-1') ? { ChecksumSHA256: metadata.hash } : {}),
428
425
  }),
429
426
  {
430
- expiresIn: TimeUtil.asSeconds(exp),
431
- ...(meta.contentType ? { signableHeaders: new Set(['content-type']) } : {})
427
+ expiresIn: TimeUtil.duration(expiresIn, 's'),
428
+ ...(metadata.contentType ? { signableHeaders: new Set(['content-type']) } : {})
432
429
  }
433
430
  );
434
431
  }