@quatrain/storage-s3 1.2.4 → 1.2.6

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.
@@ -1,4 +1,4 @@
1
- import { AbstractStorageAdapter, FileType, FileResponseLinkType, StorageParameters, DownloadFileMetaType } from '@quatrain/storage';
1
+ import { AbstractStorageAdapter, FileType, FileResponseLinkType, StorageParameters, DownloadFileMetaType, BucketStatsType } from '@quatrain/storage';
2
2
  import { Readable, Stream } from 'node:stream';
3
3
  import { S3Client } from '@aws-sdk/client-s3';
4
4
  /**
@@ -111,4 +111,12 @@ export declare class S3StorageAdapter extends AbstractStorageAdapter {
111
111
  * @returns A promise resolving to the upload URL instructions.
112
112
  */
113
113
  getUploadUrl(file: FileType, expiresIn?: number): Promise<FileResponseLinkType>;
114
+ /**
115
+ * Computes statistics for a given bucket in S3.
116
+ * Iterates over all objects in the bucket to compute total count and size.
117
+ *
118
+ * @param bucket - Target bucket name.
119
+ * @returns A promise resolving to bucket statistics.
120
+ */
121
+ getBucketStats(bucket?: string): Promise<BucketStatsType>;
114
122
  }
@@ -209,7 +209,7 @@ class S3StorageAdapter extends storage_1.AbstractStorageAdapter {
209
209
  });
210
210
  const item = yield ((_a = this._client) === null || _a === void 0 ? void 0 : _a.send(command));
211
211
  const ByteArray = yield ((_b = item === null || item === void 0 ? void 0 : item.Body) === null || _b === void 0 ? void 0 : _b.transformToByteArray());
212
- const buffer = Buffer.from(ByteArray, 'base64');
212
+ const buffer = Buffer.from(ByteArray);
213
213
  const readable = new node_stream_1.Readable();
214
214
  readable.push(buffer);
215
215
  readable.push(null);
@@ -250,7 +250,7 @@ class S3StorageAdapter extends storage_1.AbstractStorageAdapter {
250
250
  });
251
251
  const item = yield this._client.send(command);
252
252
  const ByteArray = yield ((_a = item.Body) === null || _a === void 0 ? void 0 : _a.transformToByteArray());
253
- const buffer = Buffer.from(ByteArray, 'base64');
253
+ const buffer = Buffer.from(ByteArray);
254
254
  const readable = new node_stream_1.Readable();
255
255
  readable.push(buffer);
256
256
  readable.push(null);
@@ -294,5 +294,69 @@ class S3StorageAdapter extends storage_1.AbstractStorageAdapter {
294
294
  };
295
295
  });
296
296
  }
297
+ /**
298
+ * Computes statistics for a given bucket in S3.
299
+ * Iterates over all objects in the bucket to compute total count and size.
300
+ *
301
+ * @param bucket - Target bucket name.
302
+ * @returns A promise resolving to bucket statistics.
303
+ */
304
+ getBucketStats(bucket) {
305
+ return __awaiter(this, void 0, void 0, function* () {
306
+ var _a;
307
+ const targetBucket = bucket || this._params.config.bucket;
308
+ let totalObjects = 0;
309
+ let totalSize = 0;
310
+ let isTruncated = true;
311
+ let continuationToken = undefined;
312
+ const folders = {};
313
+ storage_1.Storage.info(`Fetching bucket stats for ${targetBucket}`);
314
+ while (isTruncated) {
315
+ const input = {
316
+ Bucket: targetBucket,
317
+ ContinuationToken: continuationToken,
318
+ };
319
+ const command = new client_s3_1.ListObjectsV2Command(input);
320
+ const res = yield this._client.send(command);
321
+ if (res.Contents) {
322
+ for (const item of res.Contents) {
323
+ const size = item.Size || 0;
324
+ totalObjects++;
325
+ totalSize += size;
326
+ if (item.Key) {
327
+ const parts = item.Key.split('/');
328
+ parts.pop(); // remove file name to get exact folder
329
+ const folderPath = parts.length > 0 ? parts.join('/') : '/';
330
+ const folderName = parts.length > 0 ? parts[parts.length - 1] : '/';
331
+ if (!folders[folderPath]) {
332
+ folders[folderPath] = {
333
+ name: folderName,
334
+ path: folderPath,
335
+ totalObjects: 0,
336
+ totalSize: 0,
337
+ lastModified: item.LastModified
338
+ };
339
+ }
340
+ else if (item.LastModified) {
341
+ if (!folders[folderPath].lastModified || item.LastModified > folders[folderPath].lastModified) {
342
+ folders[folderPath].lastModified = item.LastModified;
343
+ }
344
+ }
345
+ folders[folderPath].totalObjects++;
346
+ folders[folderPath].totalSize += size;
347
+ }
348
+ }
349
+ }
350
+ isTruncated = (_a = res.IsTruncated) !== null && _a !== void 0 ? _a : false;
351
+ continuationToken = res.NextContinuationToken;
352
+ }
353
+ return {
354
+ bucket: targetBucket,
355
+ totalObjects,
356
+ totalSize,
357
+ folders,
358
+ };
359
+ });
360
+ }
297
361
  }
298
362
  exports.S3StorageAdapter = S3StorageAdapter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quatrain/storage-s3",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "Storage adapter for S3 compatible services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@aws-sdk/client-s3": "^3.337.0",
35
35
  "@aws-sdk/s3-request-presigner": "^3.342.0",
36
- "@quatrain/storage": "^1.2.5",
36
+ "@quatrain/storage": "^1.2.7",
37
37
  "fs-extra": "^11.2.0"
38
38
  },
39
39
  "scripts": {
@@ -5,6 +5,7 @@ import {
5
5
  FileResponseLinkType,
6
6
  StorageParameters,
7
7
  DownloadFileMetaType,
8
+ BucketStatsType,
8
9
  } from '@quatrain/storage'
9
10
  import { Readable, Stream } from 'node:stream'
10
11
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
@@ -17,6 +18,9 @@ import {
17
18
  CopyObjectCommand,
18
19
  HeadObjectCommand,
19
20
  ListBucketsCommand,
21
+ ListObjectsV2Command,
22
+ ListObjectsV2CommandInput,
23
+ ListObjectsV2CommandOutput,
20
24
  } from '@aws-sdk/client-s3'
21
25
 
22
26
  /**
@@ -236,7 +240,7 @@ export class S3StorageAdapter extends AbstractStorageAdapter {
236
240
  })
237
241
  const item = await this._client?.send(command)
238
242
  const ByteArray: any = await item?.Body?.transformToByteArray()
239
- const buffer = Buffer.from(ByteArray, 'base64')
243
+ const buffer = Buffer.from(ByteArray)
240
244
  const readable = new Readable()
241
245
  readable.push(buffer)
242
246
  readable.push(null)
@@ -281,7 +285,7 @@ export class S3StorageAdapter extends AbstractStorageAdapter {
281
285
  })
282
286
  const item = await this._client.send(command)
283
287
  const ByteArray: any = await item.Body?.transformToByteArray()
284
- const buffer = Buffer.from(ByteArray, 'base64')
288
+ const buffer = Buffer.from(ByteArray)
285
289
  const readable = new Readable()
286
290
  readable.push(buffer)
287
291
  readable.push(null)
@@ -331,4 +335,70 @@ export class S3StorageAdapter extends AbstractStorageAdapter {
331
335
  expiresIn,
332
336
  }
333
337
  }
338
+
339
+ /**
340
+ * Computes statistics for a given bucket in S3.
341
+ * Iterates over all objects in the bucket to compute total count and size.
342
+ *
343
+ * @param bucket - Target bucket name.
344
+ * @returns A promise resolving to bucket statistics.
345
+ */
346
+ async getBucketStats(bucket?: string): Promise<BucketStatsType> {
347
+ const targetBucket = bucket || this._params.config.bucket
348
+ let totalObjects = 0
349
+ let totalSize = 0
350
+ let isTruncated = true
351
+ let continuationToken: string | undefined = undefined
352
+ const folders: Record<string, any> = {}
353
+
354
+ Storage.info(`Fetching bucket stats for ${targetBucket}`)
355
+
356
+ while (isTruncated) {
357
+ const input: ListObjectsV2CommandInput = {
358
+ Bucket: targetBucket,
359
+ ContinuationToken: continuationToken,
360
+ }
361
+ const command = new ListObjectsV2Command(input)
362
+ const res: ListObjectsV2CommandOutput = await this._client.send(command)
363
+ if (res.Contents) {
364
+ for (const item of res.Contents) {
365
+ const size = item.Size || 0
366
+ totalObjects++
367
+ totalSize += size
368
+
369
+ if (item.Key) {
370
+ const parts = item.Key.split('/')
371
+ parts.pop() // remove file name to get exact folder
372
+ const folderPath = parts.length > 0 ? parts.join('/') : '/'
373
+ const folderName = parts.length > 0 ? parts[parts.length - 1] : '/'
374
+
375
+ if (!folders[folderPath]) {
376
+ folders[folderPath] = {
377
+ name: folderName,
378
+ path: folderPath,
379
+ totalObjects: 0,
380
+ totalSize: 0,
381
+ lastModified: item.LastModified
382
+ }
383
+ } else if (item.LastModified) {
384
+ if (!folders[folderPath].lastModified || item.LastModified > folders[folderPath].lastModified) {
385
+ folders[folderPath].lastModified = item.LastModified
386
+ }
387
+ }
388
+ folders[folderPath].totalObjects++
389
+ folders[folderPath].totalSize += size
390
+ }
391
+ }
392
+ }
393
+ isTruncated = res.IsTruncated ?? false
394
+ continuationToken = res.NextContinuationToken
395
+ }
396
+
397
+ return {
398
+ bucket: targetBucket,
399
+ totalObjects,
400
+ totalSize,
401
+ folders,
402
+ }
403
+ }
334
404
  }