@quatrain/storage-s3 1.2.5 → 1.2.7

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, prefix?: string): Promise<BucketStatsType>;
114
122
  }
@@ -294,5 +294,71 @@ 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, prefix) {
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} (prefix: ${prefix || 'none'})`);
314
+ const s3Prefix = prefix ? (prefix.endsWith('/') ? prefix : prefix + '/') : undefined;
315
+ while (isTruncated) {
316
+ const input = {
317
+ Bucket: targetBucket,
318
+ ContinuationToken: continuationToken,
319
+ Prefix: s3Prefix,
320
+ };
321
+ const command = new client_s3_1.ListObjectsV2Command(input);
322
+ const res = yield this._client.send(command);
323
+ if (res.Contents) {
324
+ for (const item of res.Contents) {
325
+ const size = item.Size || 0;
326
+ totalObjects++;
327
+ totalSize += size;
328
+ if (item.Key) {
329
+ const parts = item.Key.split('/');
330
+ parts.pop(); // remove file name to get exact folder
331
+ const folderPath = parts.length > 0 ? parts.join('/') : '/';
332
+ const folderName = parts.length > 0 ? parts[parts.length - 1] : '/';
333
+ if (!folders[folderPath]) {
334
+ folders[folderPath] = {
335
+ name: folderName,
336
+ path: folderPath,
337
+ totalObjects: 0,
338
+ totalSize: 0,
339
+ lastModified: item.LastModified
340
+ };
341
+ }
342
+ else if (item.LastModified) {
343
+ if (!folders[folderPath].lastModified || item.LastModified > folders[folderPath].lastModified) {
344
+ folders[folderPath].lastModified = item.LastModified;
345
+ }
346
+ }
347
+ folders[folderPath].totalObjects++;
348
+ folders[folderPath].totalSize += size;
349
+ }
350
+ }
351
+ }
352
+ isTruncated = (_a = res.IsTruncated) !== null && _a !== void 0 ? _a : false;
353
+ continuationToken = res.NextContinuationToken;
354
+ }
355
+ return {
356
+ bucket: targetBucket,
357
+ totalObjects,
358
+ totalSize,
359
+ folders,
360
+ };
361
+ });
362
+ }
297
363
  }
298
364
  exports.S3StorageAdapter = S3StorageAdapter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quatrain/storage-s3",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
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.6",
36
+ "@quatrain/storage": "^1.2.8",
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
  /**
@@ -331,4 +335,73 @@ 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, prefix?: 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} (prefix: ${prefix || 'none'})`)
355
+
356
+ const s3Prefix = prefix ? (prefix.endsWith('/') ? prefix : prefix + '/') : undefined
357
+
358
+ while (isTruncated) {
359
+ const input: ListObjectsV2CommandInput = {
360
+ Bucket: targetBucket,
361
+ ContinuationToken: continuationToken,
362
+ Prefix: s3Prefix,
363
+ }
364
+ const command = new ListObjectsV2Command(input)
365
+ const res: ListObjectsV2CommandOutput = await this._client.send(command)
366
+ if (res.Contents) {
367
+ for (const item of res.Contents) {
368
+ const size = item.Size || 0
369
+ totalObjects++
370
+ totalSize += size
371
+
372
+ if (item.Key) {
373
+ const parts = item.Key.split('/')
374
+ parts.pop() // remove file name to get exact folder
375
+ const folderPath = parts.length > 0 ? parts.join('/') : '/'
376
+ const folderName = parts.length > 0 ? parts[parts.length - 1] : '/'
377
+
378
+ if (!folders[folderPath]) {
379
+ folders[folderPath] = {
380
+ name: folderName,
381
+ path: folderPath,
382
+ totalObjects: 0,
383
+ totalSize: 0,
384
+ lastModified: item.LastModified
385
+ }
386
+ } else if (item.LastModified) {
387
+ if (!folders[folderPath].lastModified || item.LastModified > folders[folderPath].lastModified) {
388
+ folders[folderPath].lastModified = item.LastModified
389
+ }
390
+ }
391
+ folders[folderPath].totalObjects++
392
+ folders[folderPath].totalSize += size
393
+ }
394
+ }
395
+ }
396
+ isTruncated = res.IsTruncated ?? false
397
+ continuationToken = res.NextContinuationToken
398
+ }
399
+
400
+ return {
401
+ bucket: targetBucket,
402
+ totalObjects,
403
+ totalSize,
404
+ folders,
405
+ }
406
+ }
334
407
  }