@bitblit/ratchet-aws 6.0.146-alpha → 6.0.148-alpha

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 (103) hide show
  1. package/package.json +5 -4
  2. package/src/batch/aws-batch-background-processor.spec.ts +22 -0
  3. package/src/batch/aws-batch-background-processor.ts +71 -0
  4. package/src/batch/aws-batch-ratchet.spec.ts +42 -0
  5. package/src/batch/aws-batch-ratchet.ts +70 -0
  6. package/src/build/ratchet-aws-info.ts +19 -0
  7. package/src/cache/memory-storage-provider.ts +39 -0
  8. package/src/cache/simple-cache-object-wrapper.ts +11 -0
  9. package/src/cache/simple-cache-read-options.ts +9 -0
  10. package/src/cache/simple-cache-storage-provider.ts +15 -0
  11. package/src/cache/simple-cache.spec.ts +42 -0
  12. package/src/cache/simple-cache.ts +81 -0
  13. package/src/cloudwatch/cloud-watch-log-group-ratchet.spec.ts +26 -0
  14. package/src/cloudwatch/cloud-watch-log-group-ratchet.ts +105 -0
  15. package/src/cloudwatch/cloud-watch-logs-ratchet.spec.ts +123 -0
  16. package/src/cloudwatch/cloud-watch-logs-ratchet.ts +232 -0
  17. package/src/cloudwatch/cloud-watch-metrics-ratchet.spec.ts +30 -0
  18. package/src/cloudwatch/cloud-watch-metrics-ratchet.ts +98 -0
  19. package/src/dao/example-prototype-dao-item.ts +8 -0
  20. package/src/dao/memory-prototype-dao-provider.ts +16 -0
  21. package/src/dao/prototype-dao-config.ts +8 -0
  22. package/src/dao/prototype-dao-db.ts +4 -0
  23. package/src/dao/prototype-dao-provider.ts +6 -0
  24. package/src/dao/prototype-dao.spec.ts +33 -0
  25. package/src/dao/prototype-dao.ts +110 -0
  26. package/src/dao/s3-simple-dao.ts +96 -0
  27. package/src/dao/simple-dao-item.ts +13 -0
  28. package/src/dynamodb/dynamo-ratchet-like.ts +61 -0
  29. package/src/dynamodb/dynamo-ratchet.spec.ts +206 -0
  30. package/src/dynamodb/dynamo-ratchet.ts +850 -0
  31. package/src/dynamodb/dynamo-table-ratchet.spec.ts +23 -0
  32. package/src/dynamodb/dynamo-table-ratchet.ts +189 -0
  33. package/src/dynamodb/hash-spreader.spec.ts +22 -0
  34. package/src/dynamodb/hash-spreader.ts +89 -0
  35. package/src/dynamodb/impl/dynamo-db-storage-provider.spec.ts +60 -0
  36. package/src/dynamodb/impl/dynamo-db-storage-provider.ts +140 -0
  37. package/src/dynamodb/impl/dynamo-db-sync-lock.spec.ts +41 -0
  38. package/src/dynamodb/impl/dynamo-db-sync-lock.ts +78 -0
  39. package/src/dynamodb/impl/dynamo-expiring-code-provider.ts +31 -0
  40. package/src/dynamodb/impl/dynamo-runtime-parameter-provider.spec.ts +65 -0
  41. package/src/dynamodb/impl/dynamo-runtime-parameter-provider.ts +44 -0
  42. package/src/ec2/ec2-ratchet.spec.ts +45 -0
  43. package/src/ec2/ec2-ratchet.ts +169 -0
  44. package/src/ecr/ecr-unused-image-cleaner-options.ts +9 -0
  45. package/src/ecr/ecr-unused-image-cleaner-output.ts +8 -0
  46. package/src/ecr/ecr-unused-image-cleaner-repository-output.ts +10 -0
  47. package/src/ecr/ecr-unused-image-cleaner.spec.ts +40 -0
  48. package/src/ecr/ecr-unused-image-cleaner.ts +183 -0
  49. package/src/ecr/retained-image-descriptor.ts +7 -0
  50. package/src/ecr/retained-image-reason.ts +4 -0
  51. package/src/ecr/used-image-finder.ts +6 -0
  52. package/src/ecr/used-image-finders/aws-batch-used-image-finder.ts +40 -0
  53. package/src/ecr/used-image-finders/lambda-used-image-finder.ts +51 -0
  54. package/src/environment/cascade-environment-service-provider.ts +28 -0
  55. package/src/environment/env-var-environment-service-provider.ts +36 -0
  56. package/src/environment/environment-service-config.ts +7 -0
  57. package/src/environment/environment-service-provider.ts +7 -0
  58. package/src/environment/environment-service.spec.ts +41 -0
  59. package/src/environment/environment-service.ts +89 -0
  60. package/src/environment/fixed-environment-service-provider.ts +26 -0
  61. package/src/environment/ssm-environment-service-provider.spec.ts +18 -0
  62. package/src/environment/ssm-environment-service-provider.ts +71 -0
  63. package/src/expiring-code/expiring-code-params.ts +7 -0
  64. package/src/expiring-code/expiring-code-provider.ts +6 -0
  65. package/src/expiring-code/expiring-code-ratchet.spec.ts +10 -0
  66. package/src/expiring-code/expiring-code-ratchet.ts +44 -0
  67. package/src/expiring-code/expiring-code.ts +6 -0
  68. package/src/iam/aws-credentials-ratchet.ts +25 -0
  69. package/src/lambda/lambda-event-detector.ts +55 -0
  70. package/src/lambda/lambda-event-type-guards.ts +38 -0
  71. package/src/model/cloud-watch-metrics-minute-level-dynamo-count-request.ts +18 -0
  72. package/src/model/dynamo-count-result.ts +8 -0
  73. package/src/route53/route-53-ratchet.ts +77 -0
  74. package/src/runtime-parameter/cached-stored-runtime-parameter.ts +5 -0
  75. package/src/runtime-parameter/global-variable-override-runtime-parameter-provider.spec.ts +41 -0
  76. package/src/runtime-parameter/global-variable-override-runtime-parameter-provider.ts +82 -0
  77. package/src/runtime-parameter/memory-runtime-parameter-provider.ts +42 -0
  78. package/src/runtime-parameter/runtime-parameter-provider.ts +12 -0
  79. package/src/runtime-parameter/runtime-parameter-ratchet.spec.ts +53 -0
  80. package/src/runtime-parameter/runtime-parameter-ratchet.ts +84 -0
  81. package/src/runtime-parameter/stored-runtime-parameter.ts +6 -0
  82. package/src/s3/expanded-file-children.ts +5 -0
  83. package/src/s3/impl/s3-environment-service-provider.ts +41 -0
  84. package/src/s3/impl/s3-expiring-code-provider.spec.ts +63 -0
  85. package/src/s3/impl/s3-expiring-code-provider.ts +71 -0
  86. package/src/s3/impl/s3-prototype-dao-provider.spec.ts +45 -0
  87. package/src/s3/impl/s3-prototype-dao-provider.ts +37 -0
  88. package/src/s3/impl/s3-remote-file-tracking-provider-options.ts +6 -0
  89. package/src/s3/impl/s3-remote-file-tracking-provider.spec.ts +67 -0
  90. package/src/s3/impl/s3-remote-file-tracking-provider.ts +157 -0
  91. package/src/s3/impl/s3-storage-provider.spec.ts +32 -0
  92. package/src/s3/impl/s3-storage-provider.ts +60 -0
  93. package/src/s3/s3-cache-ratchet-like.ts +64 -0
  94. package/src/s3/s3-cache-ratchet.spec.ts +150 -0
  95. package/src/s3/s3-cache-ratchet.ts +476 -0
  96. package/src/s3/s3-location-sync-ratchet.ts +207 -0
  97. package/src/s3/s3-ratchet.spec.ts +26 -0
  98. package/src/s3/s3-ratchet.ts +26 -0
  99. package/src/ses/ses-mail-sending-provider.ts +85 -0
  100. package/src/sns/sns-ratchet.spec.ts +24 -0
  101. package/src/sns/sns-ratchet.ts +52 -0
  102. package/src/sync-lock/memory-sync-lock.ts +48 -0
  103. package/src/sync-lock/sync-lock-provider.ts +5 -0
@@ -0,0 +1,476 @@
1
+ import {
2
+ CompleteMultipartUploadCommandOutput,
3
+ CopyObjectCommand,
4
+ CopyObjectCommandInput,
5
+ CopyObjectCommandOutput,
6
+ DeleteObjectCommand,
7
+ DeleteObjectCommandInput,
8
+ DeleteObjectCommandOutput,
9
+ GetObjectCommand,
10
+ GetObjectCommandInput,
11
+ GetObjectCommandOutput,
12
+ HeadObjectCommand,
13
+ HeadObjectCommandOutput,
14
+ ListObjectsCommand,
15
+ ListObjectsCommandInput,
16
+ ListObjectsCommandOutput,
17
+ ListObjectsV2CommandInput,
18
+ ListObjectsV2CommandOutput,
19
+ NoSuchKey,
20
+ NotFound, PutObjectCommand,
21
+ PutObjectCommandInput,
22
+ PutObjectCommandOutput,
23
+ S3Client
24
+ } from "@aws-sdk/client-s3";
25
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
26
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
27
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
28
+ import { StopWatch } from '@bitblit/ratchet-common/lang/stop-watch';
29
+ import { WebStreamRatchet } from '@bitblit/ratchet-common/lang/web-stream-ratchet';
30
+
31
+ import { StreamingBlobPayloadInputTypes } from '@smithy/types';
32
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
33
+ import { Progress, Upload } from '@aws-sdk/lib-storage';
34
+ import { Readable } from 'stream';
35
+ import { S3CacheRatchetLike } from './s3-cache-ratchet-like.js';
36
+ import { ExpandedFileChildren } from './expanded-file-children.js';
37
+
38
+ export class S3CacheRatchet implements S3CacheRatchetLike {
39
+ constructor(
40
+ private s3: S3Client,
41
+ private defaultBucket: string = null,
42
+ ) {
43
+ RequireRatchet.notNullOrUndefined(this.s3, 's3');
44
+ }
45
+
46
+ public get s3Client(): S3Client {
47
+ return this.s3;
48
+ }
49
+
50
+ public static applyCacheControlMaxAge(input: PutObjectCommandInput, seconds: number): PutObjectCommandInput {
51
+ if (input && seconds) {
52
+ input.CacheControl = 'max-age=' + seconds;
53
+ }
54
+ return input;
55
+ }
56
+
57
+ public static applyUserMetaData(input: PutObjectCommandInput, key: string, value: string): PutObjectCommandInput {
58
+ if (input && StringRatchet.trimToNull(key) && StringRatchet.trimToNull(value)) {
59
+ input.Metadata = input.Metadata || {};
60
+ input.Metadata[key] = value;
61
+ }
62
+ return input;
63
+ }
64
+
65
+ public getDefaultBucket(): string {
66
+ return this.defaultBucket;
67
+ }
68
+
69
+ public getS3Client(): S3Client {
70
+ return this.s3;
71
+ }
72
+
73
+ public async fileExists(key: string, bucket: string = null): Promise<boolean> {
74
+ try {
75
+ const head: HeadObjectCommandOutput = await this.fetchMetaForCacheFile(key, this.bucketVal(bucket));
76
+ return !!head;
77
+ } catch (err) {
78
+ Logger.silly('Error calling file exists (as expected) %s', err);
79
+ return false;
80
+ }
81
+ }
82
+
83
+ public async fetchCacheFilePassThru(req: GetObjectCommandInput): Promise<GetObjectCommandOutput> {
84
+ let rval: GetObjectCommandOutput = null;
85
+ try {
86
+ rval = await this.s3.send(new GetObjectCommand(req));
87
+ } catch (err) {
88
+ if (err instanceof NoSuchKey) {
89
+ Logger.debug('Key %s not found - returning null', req.Key);
90
+ rval = null;
91
+ } else {
92
+ // Rethrow everything else
93
+ throw err;
94
+ }
95
+ }
96
+ return rval;
97
+ }
98
+
99
+ public async fetchCacheFileAsS3GetObjectCommandOutput(key: string, bucket: string = null): Promise<GetObjectCommandOutput> {
100
+ let rval: GetObjectCommandOutput = null;
101
+ try {
102
+ const params: GetObjectCommandInput = {
103
+ Bucket: this.bucketVal(bucket),
104
+ Key: key,
105
+ };
106
+ rval = await this.s3.send(new GetObjectCommand(params));
107
+ } catch (err) {
108
+ if (err instanceof NoSuchKey) {
109
+ Logger.debug('Key %s not found - returning null', key);
110
+ rval = null;
111
+ } else {
112
+ // Rethrow everything else
113
+ throw err;
114
+ }
115
+ }
116
+ return rval;
117
+ }
118
+
119
+ public async fetchCacheFileAsReadableStream(key: string, bucket: string = null): Promise<ReadableStream> {
120
+ const out: GetObjectCommandOutput = await this.fetchCacheFileAsS3GetObjectCommandOutput(key, bucket);
121
+
122
+ return out.Body.transformToWebStream();
123
+ }
124
+
125
+ public async fetchCacheFileAsBuffer(key: string, bucket: string = null): Promise<Buffer> {
126
+ let rval: Buffer = null;
127
+ const out: GetObjectCommandOutput = await this.fetchCacheFileAsS3GetObjectCommandOutput(key, bucket);
128
+ if (out?.Body) {
129
+ const tmp: Uint8Array = await out.Body.transformToByteArray();
130
+ rval = Buffer.from(tmp);
131
+ }
132
+ return rval;
133
+ }
134
+
135
+ public async fetchCacheFileAsString(key: string, bucket: string = null): Promise<string> {
136
+ let rval: string = null;
137
+ const out: GetObjectCommandOutput = await this.fetchCacheFileAsS3GetObjectCommandOutput(key, bucket);
138
+ if (out?.Body) {
139
+ rval = await out.Body.transformToString();
140
+ }
141
+ return rval;
142
+ }
143
+
144
+ public async fetchCacheFileAsObject<T>(key: string, bucket: string = null): Promise<T> {
145
+ const value: string = await this.fetchCacheFileAsString(key, bucket);
146
+ return value ? (JSON.parse(value) as T) : null;
147
+ }
148
+
149
+ public async removeCacheFile(key: string, bucket: string = null): Promise<DeleteObjectCommandOutput> {
150
+ let rval: DeleteObjectCommandOutput = null;
151
+ const params: DeleteObjectCommandInput = {
152
+ Bucket: this.bucketVal(bucket),
153
+ Key: key,
154
+ };
155
+ try {
156
+ rval = await this.s3.send(new DeleteObjectCommand(params));
157
+ } catch (err) {
158
+ if (err && err instanceof NotFound) {
159
+ Logger.info('Swallowing 404 deleting missing object %s %s', bucket, key);
160
+ rval = null;
161
+ } else {
162
+ throw err;
163
+ }
164
+ }
165
+ return rval;
166
+ }
167
+
168
+ // Given new board data, write it to the S3 file and set the refresh flag appropriately
169
+ public async writeObjectToCacheFile(
170
+ key: string,
171
+ dataObject: any,
172
+ template?: Partial<PutObjectCommandInput>,
173
+ bucket?: string,
174
+ ): Promise<CompleteMultipartUploadCommandOutput> {
175
+ const json = JSON.stringify(dataObject);
176
+ return this.writeStringToCacheFile(key, json, template, bucket);
177
+ }
178
+
179
+ // Given new board data, write it to the S3 file and set the refresh flag appropriately
180
+ public async writeStringToCacheFile(
181
+ key: string,
182
+ dataString: string,
183
+ template?: Partial<PutObjectCommandInput>,
184
+ bucket?: string,
185
+ ): Promise<CompleteMultipartUploadCommandOutput> {
186
+ const stream: ReadableStream = WebStreamRatchet.stringToWebReadableStream(dataString);
187
+ return this.writeStreamToCacheFile(key, stream, template, bucket);
188
+ }
189
+
190
+ public async writeStreamToCacheFile(
191
+ key: string,
192
+ data: ReadableStream | Readable, //StreamingBlobPayloadInputTypes
193
+ template?: Partial<PutObjectCommandInput>,
194
+ bucket?: string,
195
+ progressFn: (progress: Progress) => void = (progress) => {
196
+ Logger.debug('Uploading : %s', progress);
197
+ },
198
+ ): Promise<CompleteMultipartUploadCommandOutput> {
199
+ const params: PutObjectCommandInput = Object.assign({}, template || {}, {
200
+ Bucket: this.bucketVal(bucket),
201
+ Key: key,
202
+ Body: data as StreamingBlobPayloadInputTypes,
203
+ });
204
+
205
+ const upload: Upload = new Upload({
206
+ client: this.s3,
207
+ params: params,
208
+ tags: [],
209
+ queueSize: 4,
210
+ partSize: 1024 * 1024 * 5,
211
+ leavePartsOnError: false,
212
+ });
213
+
214
+ if (progressFn) {
215
+ upload.on('httpUploadProgress', progressFn);
216
+ }
217
+ const result: CompleteMultipartUploadCommandOutput = await upload.done();
218
+
219
+ return result;
220
+ }
221
+
222
+ public async synchronize(
223
+ srcPrefix: string,
224
+ targetPrefix: string,
225
+ targetRatchet: S3CacheRatchetLike = this,
226
+ recurseSubFolders: boolean = false,
227
+ ): Promise<string[]> {
228
+ RequireRatchet.notNullOrUndefined(srcPrefix, 'srcPrefix');
229
+ RequireRatchet.notNullOrUndefined(targetPrefix, 'targetPrefix');
230
+ RequireRatchet.true(srcPrefix.endsWith('/'), 'srcPrefix must end in /');
231
+ RequireRatchet.true(targetPrefix.endsWith('/'), 'targetPrefix must end in /');
232
+ let rval: string[] = [];
233
+ const sourceFiles: string[] = await this.directChildrenOfPrefix(srcPrefix);
234
+ const targetFiles: string[] = await targetRatchet.directChildrenOfPrefix(targetPrefix);
235
+ const sw: StopWatch = new StopWatch();
236
+
237
+ for (let i = 0; i < sourceFiles.length; i++) {
238
+ const sourceFile: string = sourceFiles[i];
239
+ Logger.info('Processing %s : %s', sourceFile, sw.dumpExpected(i / sourceFiles.length));
240
+ if (sourceFile.endsWith('/')) {
241
+ if (recurseSubFolders) {
242
+ Logger.info('%s is a subfolder - recursing');
243
+ const subs: string[] = await this.synchronize(
244
+ srcPrefix + sourceFile,
245
+ targetPrefix + sourceFile,
246
+ targetRatchet,
247
+ recurseSubFolders,
248
+ );
249
+ Logger.info('Got %d back from %s', subs.length, sourceFile);
250
+ rval = rval.concat(subs);
251
+ } else {
252
+ Logger.info('%s is a subfolder and recurse not specified - skipping', sourceFile);
253
+ }
254
+ } else {
255
+ let shouldCopy: boolean = true;
256
+ const srcMeta: HeadObjectCommandOutput = await this.fetchMetaForCacheFile(srcPrefix + sourceFile);
257
+ if (targetFiles.includes(sourceFile)) {
258
+ const targetMeta: HeadObjectCommandOutput = await targetRatchet.fetchMetaForCacheFile(targetPrefix + sourceFile);
259
+ if (srcMeta.ETag === targetMeta.ETag) {
260
+ Logger.debug('Skipping - identical');
261
+ shouldCopy = false;
262
+ }
263
+ }
264
+ if (shouldCopy) {
265
+ Logger.debug('Copying...');
266
+ const srcStream: ReadableStream = await this.fetchCacheFileAsReadableStream(srcPrefix + sourceFile);
267
+ try {
268
+ const written: PutObjectCommandOutput = await targetRatchet.writeStreamToCacheFile(
269
+ targetPrefix + sourceFile,
270
+ srcStream,
271
+ srcMeta as unknown as PutObjectCommandInput, // Carry forward any metadata
272
+ undefined,
273
+ );
274
+ Logger.silly('Write result : %j', written);
275
+ rval.push(sourceFile);
276
+ } catch (err) {
277
+ Logger.error('Failed to sync : %s : %s', sourceFile, err);
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ Logger.info('Found %d files, copied %d', sourceFiles.length, rval.length);
284
+ sw.log();
285
+ return rval;
286
+ }
287
+
288
+ public async fetchMetaForCacheFile(key: string, bucket: string = null): Promise<HeadObjectCommandOutput> {
289
+ let rval: HeadObjectCommandOutput = null;
290
+ try {
291
+ rval = await this.s3.send(
292
+ new HeadObjectCommand({
293
+ Bucket: this.bucketVal(bucket),
294
+ Key: key,
295
+ }),
296
+ );
297
+ } catch (err) {
298
+ if (err && err instanceof NotFound) {
299
+ Logger.info('Cache file %s %s not found returning null', this.bucketVal(bucket), key);
300
+ rval = null;
301
+ } else {
302
+ Logger.error('Unrecognized error, rethrowing : %s', err, err);
303
+ throw err;
304
+ }
305
+ }
306
+ return rval;
307
+ }
308
+
309
+ public async cacheFileAgeInSeconds(key: string, bucket: string = null): Promise<number> {
310
+ try {
311
+ const res: HeadObjectCommandOutput = await this.fetchMetaForCacheFile(key, bucket);
312
+ if (res && res.LastModified) {
313
+ return Math.floor((new Date().getTime() - res.LastModified.getTime()) / 1000);
314
+ } else {
315
+ Logger.warn('Cache file %s %s had no last modified returning null', this.bucketVal(bucket), key);
316
+ return null;
317
+ }
318
+ } catch (err) {
319
+ if (err && err instanceof NotFound) {
320
+ Logger.warn('Cache file %s %s not found returning null', this.bucketVal(bucket), key);
321
+ return null;
322
+ } else {
323
+ throw err;
324
+ }
325
+ }
326
+ }
327
+
328
+ // Shortcut to copy/delete
329
+ public async renameFile(
330
+ srcKey: string,
331
+ dstKey: string,
332
+ srcBucket: string = null,
333
+ dstBucket: string = null,
334
+ ): Promise<CopyObjectCommandOutput> {
335
+ Logger.info('Rename %s to %s (%s/%s)', srcKey, dstKey, srcBucket, dstBucket);
336
+ const output: CopyObjectCommandOutput = await this.copyFile(srcKey, dstKey, srcBucket, dstBucket);
337
+ if (output) {
338
+ await this.removeCacheFile(srcKey, srcBucket);
339
+ }
340
+ return output;
341
+ }
342
+
343
+ public async copyFile(
344
+ srcKey: string,
345
+ dstKey: string,
346
+ srcBucket: string = null,
347
+ dstBucket: string = null,
348
+ template?: Partial<CopyObjectCommandInput>
349
+ ): Promise<CopyObjectCommandOutput> {
350
+ const params: CopyObjectCommandInput = Object.assign({MetadataDirective:'COPY'}, template ?? {}) as CopyObjectCommandInput;
351
+ params.CopySource= '/' + this.bucketVal(srcBucket) + '/' + srcKey;
352
+ params.Bucket= this.bucketVal(dstBucket);
353
+ params.Key= dstKey;
354
+ Logger.info('Performing copy with %j', params);
355
+ const rval: CopyObjectCommandOutput = await this.s3.send(new CopyObjectCommand(params));
356
+ return rval;
357
+ }
358
+
359
+ public async quietCopyFile(srcKey: string, dstKey: string, srcBucket: string = null, dstBucket: string = null): Promise<boolean> {
360
+ let rval: boolean = false;
361
+ try {
362
+ const _tmp: CopyObjectCommandOutput = await this.copyFile(srcKey, dstKey, srcBucket, dstBucket);
363
+ rval = true;
364
+ } catch (err) {
365
+ Logger.silly('Failed to copy file in S3 : %s', err);
366
+ }
367
+ return rval;
368
+ }
369
+
370
+ public async preSignedDownloadUrlForCacheFile(key: string, expirationSeconds = 3600, bucket: string = null): Promise<string> {
371
+ const getCommand: GetObjectCommandInput = {
372
+ Bucket: this.bucketVal(bucket),
373
+ Key: key,
374
+ };
375
+ const link: string = await getSignedUrl(this.s3, new GetObjectCommand(getCommand), { expiresIn: expirationSeconds });
376
+ return link;
377
+ }
378
+
379
+ public async preSignedUploadUrlForCacheFile(key: string, expirationSeconds = 3600, bucket: string = null): Promise<string> {
380
+ const putCommand: PutObjectCommandInput = {
381
+ Bucket: this.bucketVal(bucket),
382
+ Key: key,
383
+ };
384
+ const link: string = await getSignedUrl(this.s3, new PutObjectCommand(putCommand), { expiresIn: expirationSeconds });
385
+ return link;
386
+ }
387
+
388
+ public async directChildrenOfPrefix(
389
+ prefix: string,
390
+ expandFiles = false,
391
+ bucket: string = null,
392
+ maxToReturn: number = null,
393
+ ): Promise<string[]> {
394
+ const returnValue: any[] = [];
395
+
396
+ const params: ListObjectsCommandInput = {
397
+ Bucket: this.bucketVal(bucket),
398
+ Prefix: prefix,
399
+ Delimiter: '/',
400
+ };
401
+
402
+ let response: ListObjectsCommandOutput = null;
403
+ do {
404
+ response = await this.s3.send(new ListObjectsCommand(params));
405
+
406
+ const prefixLength = prefix.length;
407
+ // Process directories
408
+ if (response['CommonPrefixes']) {
409
+ response['CommonPrefixes'].forEach((cp) => {
410
+ if (!maxToReturn || returnValue.length < maxToReturn) {
411
+ const value = cp['Prefix'].substring(prefixLength);
412
+ returnValue.push(value);
413
+ }
414
+ });
415
+ }
416
+
417
+ // Process files
418
+ if (response['Contents']) {
419
+ await Promise.all(
420
+ response['Contents'].map(async (cp) => {
421
+ if (!maxToReturn || returnValue.length < maxToReturn) {
422
+ if (expandFiles) {
423
+ const expanded: ExpandedFileChildren = {
424
+ link: await this.preSignedDownloadUrlForCacheFile(cp['Key'], 3600, bucket),
425
+ name: cp['Key'].substring(prefixLength),
426
+ size: cp['Size'],
427
+ };
428
+ returnValue.push(expanded);
429
+ } else {
430
+ returnValue.push(cp['Key'].substring(prefixLength));
431
+ }
432
+ }
433
+ }),
434
+ );
435
+ }
436
+ params.Marker = response.NextMarker;
437
+ } while (params.Marker && (!maxToReturn || returnValue.length < maxToReturn));
438
+
439
+ return returnValue;
440
+ }
441
+
442
+ public async allSubFoldersOfPrefix(prefix: string, bucket: string = null): Promise<string[]> {
443
+ const returnValue: string[] = [prefix];
444
+ let idx: number = 0;
445
+
446
+ while (idx < returnValue.length) {
447
+ const next: string = returnValue[idx++];
448
+ Logger.debug('Pulling %s (%d remaining)', next, returnValue.length - idx);
449
+ const req: ListObjectsV2CommandInput = {
450
+ Bucket: this.bucketVal(bucket),
451
+ Prefix: next,
452
+ Delimiter: '/',
453
+ };
454
+ let resp: ListObjectsV2CommandOutput = null;
455
+
456
+ do {
457
+ req.ContinuationToken = resp ? resp.NextContinuationToken : null;
458
+ resp = await this.s3.send(new ListObjectsCommand(req));
459
+ resp.CommonPrefixes.forEach((p) => {
460
+ returnValue.push(p.Prefix);
461
+ });
462
+ Logger.debug('g:%j', resp);
463
+ } while (resp.NextContinuationToken);
464
+ }
465
+
466
+ return returnValue;
467
+ }
468
+
469
+ private bucketVal(explicitBucket: string): string {
470
+ const rval: string = explicitBucket ? explicitBucket : this.defaultBucket;
471
+ if (!rval) {
472
+ throw 'You must set either the default bucket or pass it explicitly';
473
+ }
474
+ return rval;
475
+ }
476
+ }
@@ -0,0 +1,207 @@
1
+ import {
2
+ CopyObjectCommand,
3
+ CopyObjectCommandInput,
4
+ GetObjectCommand,
5
+ GetObjectOutput,
6
+ ListObjectsV2Command,
7
+ ListObjectsV2CommandInput,
8
+ ListObjectsV2CommandOutput,
9
+ PutObjectCommandInput,
10
+ S3Client,
11
+ } from '@aws-sdk/client-s3';
12
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
13
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
14
+ import { PromiseRatchet } from '@bitblit/ratchet-common/lang/promise-ratchet';
15
+ import { Upload } from '@aws-sdk/lib-storage';
16
+
17
+ export interface S3LocationSyncRatchetConfig {
18
+ srcS3: S3Client;
19
+ srcBucket: string;
20
+ srcPrefix: string;
21
+ dstS3: S3Client;
22
+ dstBucket: string;
23
+ dstPrefix: string;
24
+ maxNumThreads?: number;
25
+ maxRetries?: number;
26
+ }
27
+
28
+ interface BucketCmpResult {
29
+ needCopy: S3Object[];
30
+ existed: S3Object[];
31
+ diff: S3Object[];
32
+ }
33
+
34
+ interface S3Object {
35
+ Key: string;
36
+ ETag: string;
37
+ Size: number;
38
+ LastModified: Date;
39
+ }
40
+
41
+ export class S3LocationSyncRatchet {
42
+ private config: S3LocationSyncRatchetConfig;
43
+
44
+ constructor(config: S3LocationSyncRatchetConfig) {
45
+ RequireRatchet.notNullOrUndefined(config, 'config');
46
+
47
+ this.config = config;
48
+
49
+ if (!this.config.maxNumThreads) {
50
+ this.config.maxNumThreads = 15;
51
+ }
52
+
53
+ if (!this.config.maxRetries) {
54
+ this.config.maxRetries = 5;
55
+ }
56
+ }
57
+
58
+ public updateSrcPrefix(prefix: string): void {
59
+ this.config.srcPrefix = prefix;
60
+ }
61
+
62
+ public updateDstPrefix(prefix: string): void {
63
+ this.config.dstPrefix = prefix;
64
+ }
65
+
66
+ public async copyObject(key: string, size: number, express = false): Promise<void> {
67
+ const dstKey = key.replace(this.config.srcPrefix, this.config.dstPrefix);
68
+ let completedCopying = false;
69
+ let retries = 0;
70
+ while (!completedCopying && retries < this.config.maxRetries) {
71
+ Logger.debug(`${retries > 0 ? `Retry ${retries} ` : ''}${express ? 'Express' : 'Slow'} copying
72
+ [${[this.config.srcBucket, key].join('/')} ---> ${[this.config.dstBucket, dstKey].join('/')}]`);
73
+ try {
74
+ if (express) {
75
+ const params: CopyObjectCommandInput = {
76
+ CopySource: encodeURIComponent([this.config.srcBucket, key].join('/')),
77
+ Bucket: this.config.dstBucket,
78
+ Key: dstKey,
79
+ MetadataDirective: 'COPY',
80
+ };
81
+ await this.config.dstS3.send(new CopyObjectCommand(params));
82
+ } else {
83
+ const fetched: GetObjectOutput = await this.config.srcS3.send(new GetObjectCommand({ Bucket: this.config.srcBucket, Key: key }));
84
+ const params: PutObjectCommandInput = {
85
+ Bucket: this.config.dstBucket,
86
+ Key: dstKey,
87
+ Body: fetched.Body,
88
+ ContentLength: size,
89
+ };
90
+
91
+ const upload: Upload = new Upload({
92
+ client: this.config.dstS3,
93
+ params: params,
94
+ tags: [],
95
+ queueSize: 4,
96
+ partSize: 1024 * 1024 * 5,
97
+ leavePartsOnError: false,
98
+ });
99
+
100
+ upload.on('httpUploadProgress', (progress) => {
101
+ Logger.debug('Uploading : %s', progress);
102
+ });
103
+ await upload.done();
104
+ }
105
+
106
+ completedCopying = true;
107
+ } catch (err) {
108
+ Logger.warn(
109
+ `Can't ${express ? 'express' : 'slow'} copy
110
+ [${[this.config.srcBucket, key].join('/')} ---> ${[this.config.dstBucket, dstKey].join('/')}]: %j`,
111
+ err,
112
+ );
113
+ retries++;
114
+ }
115
+ }
116
+
117
+ Logger.debug(`Finished ${express ? 'express' : 'slow'} copying
118
+ [${[this.config.srcBucket, key].join('/')} ---> ${[this.config.dstBucket, dstKey].join('/')}]`);
119
+ }
120
+
121
+ public async listObjects(bucket: string, prefix: string, s3: S3Client): Promise<any> {
122
+ Logger.info(`Scanning bucket [${[bucket, prefix].join('/')}]`);
123
+
124
+ const params: ListObjectsV2CommandInput = {
125
+ Bucket: bucket,
126
+ Prefix: prefix,
127
+ };
128
+
129
+ let more = true;
130
+ const rval = {};
131
+
132
+ while (more) {
133
+ const response: ListObjectsV2CommandOutput = await s3.send(new ListObjectsV2Command(params));
134
+ more = response.IsTruncated;
135
+ response.Contents.forEach((obj) => {
136
+ rval[obj.Key] = { Key: obj.Key, LastModified: obj.LastModified, ETag: obj.ETag, Size: obj.Size };
137
+ });
138
+
139
+ if (more) {
140
+ params.ContinuationToken = response.NextContinuationToken;
141
+ }
142
+ }
143
+ return rval;
144
+ }
145
+
146
+ public async startSyncing(): Promise<boolean> {
147
+ Logger.info(`Syncing [${this.config.srcBucket}/${this.config.srcPrefix}
148
+ ---> ${this.config.dstBucket}/${this.config.dstPrefix}]`);
149
+ const cp = async (obj: S3Object) => {
150
+ await this.copyObject(obj.Key, obj.Size);
151
+ };
152
+
153
+ let cmpResult: BucketCmpResult = await this.compareSrcAndDst();
154
+
155
+ if (cmpResult.needCopy.length > 0 || cmpResult.diff.length > 0) {
156
+ await PromiseRatchet.runBoundedParallelSingleParam<any>(cp, cmpResult.needCopy, this, this.config.maxNumThreads);
157
+ await PromiseRatchet.runBoundedParallelSingleParam<any>(cp, cmpResult.diff, this, this.config.maxNumThreads);
158
+
159
+ Logger.info('Verifying...');
160
+ cmpResult = await this.compareSrcAndDst();
161
+ Logger.debug('Compare result %j', cmpResult);
162
+ }
163
+ return cmpResult.needCopy.length === 0 && cmpResult.diff.length === 0;
164
+ }
165
+
166
+ private async compareSrcAndDst(): Promise<BucketCmpResult> {
167
+ const getSrc: Promise<any> = this.listObjects(this.config.srcBucket, this.config.srcPrefix, this.config.srcS3);
168
+ const getDst: Promise<any> = this.listObjects(this.config.dstBucket, this.config.dstPrefix, this.config.dstS3);
169
+
170
+ const srcObjs = await getSrc;
171
+ const dstObjs = await getDst;
172
+
173
+ const rval: BucketCmpResult = {
174
+ needCopy: [],
175
+ existed: [],
176
+ diff: [],
177
+ };
178
+
179
+ await PromiseRatchet.runBoundedParallelSingleParam(
180
+ async (key) => {
181
+ const sObj: any = srcObjs[key];
182
+ const dstKey: string = key.replace(this.config.srcPrefix, this.config.dstPrefix);
183
+ const dObj: any = dstKey in dstObjs ? dstObjs[dstKey] : undefined;
184
+ if (!dObj) {
185
+ rval.needCopy.push(sObj);
186
+ return;
187
+ }
188
+
189
+ if (sObj.Size !== dObj.Size) {
190
+ rval.diff.push(sObj);
191
+ return;
192
+ }
193
+
194
+ if (sObj.LastModified.getTime() <= dObj.LastModified.getTime()) {
195
+ rval.existed.push(sObj);
196
+ return;
197
+ }
198
+
199
+ rval.diff.push(sObj);
200
+ },
201
+ Object.keys(srcObjs),
202
+ this,
203
+ this.config.maxNumThreads,
204
+ );
205
+ return rval;
206
+ }
207
+ }
@@ -0,0 +1,26 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import { S3Ratchet } from './s3-ratchet.js';
3
+ import { mockClient } from 'aws-sdk-client-mock';
4
+ import { beforeEach, describe, expect, test } from 'vitest';
5
+
6
+ let mockS3;
7
+
8
+ describe('#S3Ratchet', function () {
9
+ mockS3 = mockClient(S3Client);
10
+ beforeEach(() => {
11
+ mockS3.reset();
12
+ });
13
+
14
+ test('should checkS3UrlForValidity', async () => {
15
+ expect(S3Ratchet.checkS3UrlForValidity('s3://test/out/b.txt')).toBeTruthy();
16
+ expect(S3Ratchet.checkS3UrlForValidity('http://test/out/b.txt')).toBeFalsy();
17
+ });
18
+
19
+ test('should extractBucketFromURL', async () => {
20
+ expect(S3Ratchet.extractBucketFromURL('s3://test/out/b.txt')).toEqual('test');
21
+ });
22
+
23
+ test('should extractKeyFromURL', async () => {
24
+ expect(S3Ratchet.extractKeyFromURL('s3://test/out/b.txt')).toEqual('out/b.txt');
25
+ });
26
+ });