@travetto/model-s3 7.0.0-rc.1 → 7.0.0-rc.3
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 +3 -3
- package/package.json +4 -4
- package/src/config.ts +5 -1
- package/src/service.ts +61 -61
package/README.md
CHANGED
|
@@ -31,8 +31,8 @@ export class Init {
|
|
|
31
31
|
@InjectableFactory({
|
|
32
32
|
primary: true
|
|
33
33
|
})
|
|
34
|
-
static getModelSource(
|
|
35
|
-
return new S3ModelService(
|
|
34
|
+
static getModelSource(config: S3ModelConfig) {
|
|
35
|
+
return new S3ModelService(config);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
```
|
|
@@ -60,7 +60,7 @@ export class S3ModelConfig {
|
|
|
60
60
|
|
|
61
61
|
chunkSize = 5 * 2 ** 20; // Chunk size in bytes
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
modifyStorage?: boolean;
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Provide host to bucket
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-s3",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.3",
|
|
4
4
|
"description": "S3 backing for the travetto model module.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"s3",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"@aws-sdk/client-s3": "^3.940.0",
|
|
29
29
|
"@aws-sdk/credential-provider-ini": "^3.940.0",
|
|
30
30
|
"@aws-sdk/s3-request-presigner": "^3.940.0",
|
|
31
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
32
|
-
"@travetto/config": "^7.0.0-rc.
|
|
33
|
-
"@travetto/model": "^7.0.0-rc.
|
|
31
|
+
"@travetto/cli": "^7.0.0-rc.3",
|
|
32
|
+
"@travetto/config": "^7.0.0-rc.3",
|
|
33
|
+
"@travetto/model": "^7.0.0-rc.3"
|
|
34
34
|
},
|
|
35
35
|
"travetto": {
|
|
36
36
|
"displayName": "S3 Model Support"
|
package/src/config.ts
CHANGED
|
@@ -27,7 +27,7 @@ export class S3ModelConfig {
|
|
|
27
27
|
|
|
28
28
|
chunkSize = 5 * 2 ** 20; // Chunk size in bytes
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
modifyStorage?: boolean;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Provide host to bucket
|
|
@@ -40,6 +40,10 @@ export class S3ModelConfig {
|
|
|
40
40
|
* Produces the s3 config from the provide details, post construction
|
|
41
41
|
*/
|
|
42
42
|
async postConstruct(): Promise<void> {
|
|
43
|
+
if (!Runtime.production) {
|
|
44
|
+
this.endpoint ??= 'http://localhost:4566'; // From docker
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
if (!this.accessKeyId && !this.secretAccessKey) {
|
|
44
48
|
const creds = await fromIni({ profile: this.profile })();
|
|
45
49
|
this.accessKeyId = creds.accessKeyId;
|
package/src/service.ts
CHANGED
|
@@ -16,12 +16,12 @@ import { Class, AppError, castTo, asFull, BlobMeta, ByteRange, BinaryInput, Bina
|
|
|
16
16
|
|
|
17
17
|
import { S3ModelConfig } from './config.ts';
|
|
18
18
|
|
|
19
|
-
function isMetadataBearer(
|
|
20
|
-
return !!
|
|
19
|
+
function isMetadataBearer(value: unknown): value is MetadataBearer {
|
|
20
|
+
return !!value && typeof value === 'object' && '$metadata' in value;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function hasContentType<T>(
|
|
24
|
-
return
|
|
23
|
+
function hasContentType<T>(value: T): value is T & { contenttype?: string } {
|
|
24
|
+
return value !== undefined && value !== null && Object.hasOwn(value, 'contenttype');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
type MetaBase = Pick<CreateMultipartUploadRequest,
|
|
@@ -72,12 +72,12 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
72
72
|
return this.#basicKey(key);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
#
|
|
75
|
+
#query<U extends object>(cls: string | Class, id: string, extra: U = asFull({})): (U & { Key: string, Bucket: string }) {
|
|
76
76
|
const key = this.#resolveKey(cls, id);
|
|
77
77
|
return { Key: key, Bucket: this.config.bucket, ...extra };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
#
|
|
80
|
+
#queryBlob<U extends object>(id: string, extra: U = asFull({})): (U & { Key: string, Bucket: string }) {
|
|
81
81
|
const key = this.#basicKey(id);
|
|
82
82
|
return { Key: key, Bucket: this.config.bucket, ...extra };
|
|
83
83
|
}
|
|
@@ -96,16 +96,16 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
96
96
|
async * #iterateBucket(cls?: string | Class): AsyncIterable<{ Key: string, id: string }[]> {
|
|
97
97
|
let Marker: string | undefined;
|
|
98
98
|
for (; ;) {
|
|
99
|
-
const
|
|
99
|
+
const items = await this.client.listObjects({
|
|
100
100
|
Bucket: this.config.bucket,
|
|
101
101
|
Prefix: cls ? this.#resolveKey(cls) : this.config.namespace,
|
|
102
102
|
Marker
|
|
103
103
|
});
|
|
104
|
-
if (
|
|
105
|
-
yield
|
|
104
|
+
if (items.Contents?.length) {
|
|
105
|
+
yield items.Contents.map(item => ({ Key: item.Key!, id: item.Key!.split(':').pop()! }));
|
|
106
106
|
}
|
|
107
|
-
if (
|
|
108
|
-
Marker =
|
|
107
|
+
if (items.NextMarker) {
|
|
108
|
+
Marker = items.NextMarker;
|
|
109
109
|
} else {
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
@@ -116,21 +116,21 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
116
116
|
* Write multipart file upload, in chunks
|
|
117
117
|
*/
|
|
118
118
|
async #writeMultipart(id: string, input: Readable, meta: BlobMeta): Promise<void> {
|
|
119
|
-
const { UploadId } = await this.client.createMultipartUpload(this.#
|
|
119
|
+
const { UploadId } = await this.client.createMultipartUpload(this.#queryBlob(id, this.#getMetaBase(meta)));
|
|
120
120
|
|
|
121
121
|
const parts: CompletedPart[] = [];
|
|
122
122
|
let buffers: Buffer[] = [];
|
|
123
123
|
let total = 0;
|
|
124
|
-
let
|
|
124
|
+
let i = 1;
|
|
125
125
|
const flush = async (): Promise<void> => {
|
|
126
126
|
if (!total) { return; }
|
|
127
|
-
const part = await this.client.uploadPart(this.#
|
|
127
|
+
const part = await this.client.uploadPart(this.#queryBlob(id, {
|
|
128
128
|
Body: Buffer.concat(buffers),
|
|
129
|
-
PartNumber:
|
|
129
|
+
PartNumber: i,
|
|
130
130
|
UploadId
|
|
131
131
|
}));
|
|
132
|
-
parts.push({ PartNumber:
|
|
133
|
-
|
|
132
|
+
parts.push({ PartNumber: i, ETag: part.ETag });
|
|
133
|
+
i += 1;
|
|
134
134
|
buffers = [];
|
|
135
135
|
total = 0;
|
|
136
136
|
};
|
|
@@ -145,13 +145,13 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
145
145
|
}
|
|
146
146
|
await flush();
|
|
147
147
|
|
|
148
|
-
await this.client.completeMultipartUpload(this.#
|
|
148
|
+
await this.client.completeMultipartUpload(this.#queryBlob(id, {
|
|
149
149
|
UploadId,
|
|
150
150
|
MultipartUpload: { Parts: parts }
|
|
151
151
|
}));
|
|
152
|
-
} catch (
|
|
153
|
-
await this.client.abortMultipartUpload(this.#
|
|
154
|
-
throw
|
|
152
|
+
} catch (error) {
|
|
153
|
+
await this.client.abortMultipartUpload(this.#queryBlob(id, { UploadId }));
|
|
154
|
+
throw error;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -163,9 +163,9 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
163
163
|
Objects: items
|
|
164
164
|
}
|
|
165
165
|
});
|
|
166
|
-
} catch (
|
|
166
|
+
} catch (error) {
|
|
167
167
|
// Handle GCS
|
|
168
|
-
if (
|
|
168
|
+
if (error instanceof Error && error.name === 'NotImplemented') {
|
|
169
169
|
for (const item of items) {
|
|
170
170
|
await this.client.deleteObject({
|
|
171
171
|
Bucket: this.config.bucket,
|
|
@@ -173,7 +173,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
175
|
} else {
|
|
176
|
-
throw
|
|
176
|
+
throw error;
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
}
|
|
@@ -190,30 +190,30 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
190
190
|
}),
|
|
191
191
|
} : {})
|
|
192
192
|
});
|
|
193
|
-
ModelStorageUtil.
|
|
193
|
+
ModelStorageUtil.storageInitialization(this);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
async head<T extends ModelType>(cls: Class<T>, id: string): Promise<boolean> {
|
|
197
197
|
try {
|
|
198
|
-
const result = await this.client.headObject(this.#
|
|
198
|
+
const result = await this.client.headObject(this.#query(cls, id));
|
|
199
199
|
const { expiresAt } = ModelRegistryIndex.getConfig(cls);
|
|
200
200
|
if (expiresAt && result.ExpiresString && Date.parse(result.ExpiresString) < Date.now()) {
|
|
201
201
|
return false;
|
|
202
202
|
}
|
|
203
203
|
return true;
|
|
204
|
-
} catch (
|
|
205
|
-
if (isMetadataBearer(
|
|
206
|
-
if (
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (isMetadataBearer(error)) {
|
|
206
|
+
if (error.$metadata.httpStatusCode === 404) {
|
|
207
207
|
return false;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
-
throw
|
|
210
|
+
throw error;
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
215
215
|
try {
|
|
216
|
-
const result = await this.client.getObject(this.#
|
|
216
|
+
const result = await this.client.getObject(this.#query(cls, id));
|
|
217
217
|
if (result.Body) {
|
|
218
218
|
const body = await toText(castTo(result.Body));
|
|
219
219
|
const output = await ModelCrudUtil.load(cls, body);
|
|
@@ -230,13 +230,13 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
throw new NotFoundError(cls, id);
|
|
233
|
-
} catch (
|
|
234
|
-
if (isMetadataBearer(
|
|
235
|
-
if (
|
|
236
|
-
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (isMetadataBearer(error)) {
|
|
235
|
+
if (error.$metadata.httpStatusCode === 404) {
|
|
236
|
+
error = new NotFoundError(cls, id);
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
-
throw
|
|
239
|
+
throw error;
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
@@ -246,7 +246,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
246
246
|
prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
247
247
|
}
|
|
248
248
|
const content = Buffer.from(JSON.stringify(prepped), 'utf8');
|
|
249
|
-
await this.client.putObject(this.#
|
|
249
|
+
await this.client.putObject(this.#query(cls, prepped.id, {
|
|
250
250
|
Body: content,
|
|
251
251
|
ContentType: 'application/json',
|
|
252
252
|
ContentLength: content.length,
|
|
@@ -289,7 +289,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
289
289
|
if (!(await this.head(cls, id))) {
|
|
290
290
|
throw new NotFoundError(cls, id);
|
|
291
291
|
}
|
|
292
|
-
await this.client.deleteObject(this.#
|
|
292
|
+
await this.client.deleteObject(this.#query(cls, id));
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
@@ -297,9 +297,9 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
297
297
|
for (const { id } of batch) {
|
|
298
298
|
try {
|
|
299
299
|
yield await this.get(cls, id);
|
|
300
|
-
} catch (
|
|
301
|
-
if (!(
|
|
302
|
-
throw
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (!(error instanceof NotFoundError)) {
|
|
302
|
+
throw error;
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
}
|
|
@@ -321,7 +321,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
321
321
|
|
|
322
322
|
if (blobMeta.size && blobMeta.size < this.config.chunkSize) { // If smaller than chunk size
|
|
323
323
|
// Upload to s3
|
|
324
|
-
await this.client.putObject(this.#
|
|
324
|
+
await this.client.putObject(this.#queryBlob(location, {
|
|
325
325
|
Body: await toBuffer(stream),
|
|
326
326
|
ContentLength: blobMeta.size,
|
|
327
327
|
...this.#getMetaBase(blobMeta),
|
|
@@ -333,7 +333,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
333
333
|
|
|
334
334
|
async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
|
|
335
335
|
// Read from s3
|
|
336
|
-
const result = await this.client.getObject(this.#
|
|
336
|
+
const result = await this.client.getObject(this.#queryBlob(location, range ? {
|
|
337
337
|
Range: `bytes=${range.start}-${range.end}`
|
|
338
338
|
} : {}));
|
|
339
339
|
|
|
@@ -359,40 +359,40 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
|
|
362
|
-
const query = this.#
|
|
362
|
+
const query = this.#queryBlob(location);
|
|
363
363
|
try {
|
|
364
364
|
return (await this.client.headObject(query));
|
|
365
|
-
} catch (
|
|
366
|
-
if (isMetadataBearer(
|
|
367
|
-
if (
|
|
368
|
-
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (isMetadataBearer(error)) {
|
|
367
|
+
if (error.$metadata.httpStatusCode === 404) {
|
|
368
|
+
error = new NotFoundError('Blob', location);
|
|
369
369
|
}
|
|
370
370
|
}
|
|
371
|
-
throw
|
|
371
|
+
throw error;
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
async getBlobMeta(location: string): Promise<BlobMeta> {
|
|
376
|
-
const
|
|
376
|
+
const blob = await this.headBlob(location);
|
|
377
377
|
|
|
378
|
-
if (
|
|
379
|
-
const
|
|
378
|
+
if (blob) {
|
|
379
|
+
const meta: BlobMeta = {
|
|
380
380
|
contentType: '',
|
|
381
|
-
...
|
|
382
|
-
size:
|
|
381
|
+
...blob.Metadata,
|
|
382
|
+
size: blob.ContentLength!,
|
|
383
383
|
};
|
|
384
|
-
if (hasContentType(
|
|
385
|
-
|
|
386
|
-
delete
|
|
384
|
+
if (hasContentType(meta)) {
|
|
385
|
+
meta['contentType'] = meta['contenttype']!;
|
|
386
|
+
delete meta['contenttype'];
|
|
387
387
|
}
|
|
388
|
-
return
|
|
388
|
+
return meta;
|
|
389
389
|
} else {
|
|
390
390
|
throw new NotFoundError('Blob', location);
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
async deleteBlob(location: string): Promise<void> {
|
|
395
|
-
await this.client.deleteObject(this.#
|
|
395
|
+
await this.client.deleteObject(this.#queryBlob(location));
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
async updateBlobMeta(location: string, meta: BlobMeta): Promise<void> {
|
|
@@ -409,7 +409,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
409
409
|
async getBlobReadUrl(location: string, exp: TimeSpan = '1h'): Promise<string> {
|
|
410
410
|
return await getSignedUrl(
|
|
411
411
|
this.client,
|
|
412
|
-
new GetObjectCommand(this.#
|
|
412
|
+
new GetObjectCommand(this.#queryBlob(location)),
|
|
413
413
|
{ expiresIn: TimeUtil.asSeconds(exp) }
|
|
414
414
|
);
|
|
415
415
|
}
|
|
@@ -419,7 +419,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
|
|
|
419
419
|
return await getSignedUrl(
|
|
420
420
|
this.client,
|
|
421
421
|
new PutObjectCommand({
|
|
422
|
-
...this.#
|
|
422
|
+
...this.#queryBlob(location),
|
|
423
423
|
...base,
|
|
424
424
|
...(meta.size ? { ContentLength: meta.size } : {}),
|
|
425
425
|
...((meta.hash && meta.hash !== '-1') ? { ChecksumSHA256: meta.hash } : {}),
|