mbkbucket 2.0.0 → 2.1.1

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/lib/s3.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, ListMultipartUploadsCommand } from '@aws-sdk/client-s3';
2
2
  import dotenv from "dotenv";
3
3
  import { mbkautheVar } from "mbkauthe";
4
+ import { createLogger } from "./debug.js";
4
5
 
5
6
  dotenv.config();
7
+ const debugS3 = createLogger('s3');
6
8
 
7
9
  function parseJsonEnv(envVar, fallback = {}) {
8
10
  if (!envVar) return fallback;
@@ -18,9 +20,15 @@ function parseJsonEnv(envVar, fallback = {}) {
18
20
 
19
21
  // Parse BucketConnection JSON that contains all bucket configurations
20
22
  const allBucketConfigs = parseJsonEnv(process.env.BucketConnection, {});
23
+ const availableBucketNames = Object.freeze(Object.keys(allBucketConfigs || {}));
24
+ const hasBucketConfigs = availableBucketNames.length > 0;
25
+
26
+ function escapeRegExp(value) {
27
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ }
21
29
 
22
30
  export function getAvailableBucketNames() {
23
- return Object.keys(allBucketConfigs || {});
31
+ return availableBucketNames;
24
32
  }
25
33
 
26
34
  // Helper to get default bucket name
@@ -31,9 +39,8 @@ function getDefaultBucketName() {
31
39
  }
32
40
 
33
41
  // Second priority: use first bucket in BucketConnection
34
- const buckets = Object.keys(allBucketConfigs);
35
- if (buckets.length > 0) {
36
- return buckets[0];
42
+ if (availableBucketNames.length > 0) {
43
+ return availableBucketNames[0];
37
44
  }
38
45
 
39
46
  // Fallback: return null (will cause error in getBucketConfig)
@@ -49,12 +56,12 @@ export function resolveBucketName(bucketName) {
49
56
  throw new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
50
57
  }
51
58
 
52
- if (!allBucketConfigs || Object.keys(allBucketConfigs).length === 0) {
59
+ if (!hasBucketConfigs) {
53
60
  throw new Error('BucketConnection environment variable is not set or empty. Please configure it with your bucket connections.');
54
61
  }
55
62
 
56
63
  if (!allBucketConfigs[candidate]) {
57
- throw new Error(`Bucket '${candidate}' not found in BucketConnection. Available buckets: ${Object.keys(allBucketConfigs).join(', ')}`);
64
+ throw new Error(`Bucket '${candidate}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
58
65
  }
59
66
 
60
67
  return candidate;
@@ -69,7 +76,9 @@ export function getAppName() {
69
76
  }
70
77
 
71
78
  function isRootModeApp(appName) {
72
- return String(appName || '').trim().toLowerCase() === 'portal';
79
+ const app = String(appName || '').toLowerCase();
80
+ const isRootApp = app === 'portal' || app === 'mbkbucket';
81
+ return isRootApp;
73
82
  }
74
83
 
75
84
  export function ensureKeyHasAppPrefix(key = '') {
@@ -96,7 +105,7 @@ export function ensureKeyHasAppPrefix(key = '') {
96
105
  if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
97
106
 
98
107
  // Collapse repeated app prefixes (app/app/... -> app/...)
99
- const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
108
+ const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
100
109
  if (!cleaned.startsWith(`${app}/`)) {
101
110
  cleaned = `${app}/${cleaned}`;
102
111
  } else {
@@ -137,7 +146,7 @@ export function ensurePrefix(prefix = '') {
137
146
 
138
147
  if (p.startsWith(`${app}/`)) {
139
148
  // collapse repeated app/ sequences
140
- const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
149
+ const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
141
150
  return p.replace(appPrefixRegex, `${app}/`);
142
151
  }
143
152
  return `${app}/${p}`;
@@ -157,7 +166,7 @@ export function getBucketConfig(bucketName) {
157
166
  if (_configCache.has(cacheKey)) return _configCache.get(cacheKey);
158
167
 
159
168
  // Look up bucket config from BucketConnection JSON
160
- if (!allBucketConfigs || Object.keys(allBucketConfigs).length === 0) {
169
+ if (!hasBucketConfigs) {
161
170
  console.error(`[mbkbucket] ❌ BucketConnection is not configured or failed to parse`);
162
171
  console.error(`[mbkbucket] Please set BucketConnection in your .env file with this format:`);
163
172
  console.error(`[mbkbucket] BucketConnection={"r2":{"BUCKET_NAME":"...","ACCESS_KEY_ID":"...","SECRET_ACCESS_KEY":"...","ENDPOINT":"https://..."}}`);
@@ -170,7 +179,7 @@ export function getBucketConfig(bucketName) {
170
179
  const cfg = allBucketConfigs[bucketName];
171
180
 
172
181
  if (!cfg) {
173
- throw new Error(`Bucket '${bucketName}' not found in BucketConnection. Available buckets: ${Object.keys(allBucketConfigs).join(', ')}`);
182
+ throw new Error(`Bucket '${bucketName}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
174
183
  }
175
184
 
176
185
  _configCache.set(cacheKey, cfg);
@@ -250,10 +259,10 @@ async function checkHealth() {
250
259
  // Quick diagnostics at startup
251
260
  (async () => {
252
261
  try {
253
- console.log('[mbkbucket] mbkautheVar snapshot:', { APP_NAME: mbkautheVar?.APP_NAME, bucket: mbkautheVar?.bucket });
262
+ debugS3('mbkautheVar snapshot: %O', { APP_NAME: mbkautheVar?.APP_NAME, bucket: mbkautheVar?.bucket });
254
263
  const health = await checkHealth();
255
264
  if (health.status === 'healthy') {
256
- console.log(`[mbkbucket] Connected to bucket: ${health.bucket} (${health.responseTime}ms)`);
265
+ debugS3('Connected to bucket: %s (%sms)', health.bucket, health.responseTime);
257
266
  } else {
258
267
  console.error(`[mbkbucket] S3 connection failed:`, health.error);
259
268
  }
@@ -286,11 +295,12 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
286
295
  const config = getBucketConfig(bucketName);
287
296
  const bucket = config.BUCKET_NAME;
288
297
 
289
- console.log(`[mbkbucket] Uploading file to bucket=${bucket} key=${keyToUpload}`);
298
+ debugS3('Uploading file to bucket=%s key=%s', bucket, keyToUpload);
290
299
 
291
300
  // Add default metadata (supports metadata)
301
+ const uploadedAt = new Date().toISOString();
292
302
  const defaultMetadata = {
293
- 'uploaded-at': new Date().toISOString(),
303
+ 'uploaded-at': uploadedAt,
294
304
  'file-size': fileBuffer.length.toString(),
295
305
  'upload-source': 'web-portal',
296
306
  ...metadata
@@ -321,7 +331,7 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
321
331
  fileSize: fileBuffer.length,
322
332
  key: keyToUpload,
323
333
  contentType,
324
- uploadedAt: new Date().toISOString()
334
+ uploadedAt
325
335
  };
326
336
  } catch (error) {
327
337
  console.error(`Upload failed for key ${key}:`, error);
@@ -361,7 +371,7 @@ export async function downloadFile(key, options = {}) {
361
371
 
362
372
  // Log range requests for debugging
363
373
  if (range) {
364
- console.log(`[mbkbucket] Range request for ${keyToDownload}: ${range}`);
374
+ debugS3('Range request for %s: %s', keyToDownload, range);
365
375
  }
366
376
 
367
377
  const command = new GetObjectCommand({
@@ -374,9 +384,7 @@ export async function downloadFile(key, options = {}) {
374
384
  ...(responseContentType && { ResponseContentType: responseContentType }),
375
385
  });
376
386
 
377
- const startTime = Date.now();
378
387
  const result = await client.send(command);
379
- const downloadTime = Date.now() - startTime;
380
388
 
381
389
  return {
382
390
  ...result,
@@ -446,6 +454,8 @@ export async function deleteFiles(keys, bucketName) {
446
454
  // S3 allows max 1000 objects per delete request
447
455
  const maxBatchSize = 1000;
448
456
  const results = [];
457
+ let deletedCount = 0;
458
+ const errors = [];
449
459
 
450
460
  for (let i = 0; i < keys.length; i += maxBatchSize) {
451
461
  const batch = keys.slice(i, i + maxBatchSize).map(k => ({ Key: ensureKeyHasAppPrefix(k) }));
@@ -460,12 +470,14 @@ export async function deleteFiles(keys, bucketName) {
460
470
 
461
471
  const result = await client.send(command);
462
472
  results.push(result);
473
+ deletedCount += result.Deleted?.length || 0;
474
+ if (result.Errors?.length) errors.push(...result.Errors);
463
475
  }
464
476
 
465
477
  return {
466
478
  results,
467
- deletedCount: results.reduce((acc, result) => acc + (result.Deleted?.length || 0), 0),
468
- errors: results.reduce((acc, result) => acc.concat(result.Errors || []), []),
479
+ deletedCount,
480
+ errors,
469
481
  deletedAt: new Date().toISOString()
470
482
  };
471
483
  } catch (error) {
@@ -600,19 +612,21 @@ export async function getFileMetadata(key, bucketName) {
600
612
  });
601
613
 
602
614
  const result = await client.send(command);
615
+ const queriedAt = new Date().toISOString();
603
616
 
604
617
  return {
605
618
  ...result,
606
619
  key: keyToCheck,
607
620
  exists: true,
608
- queriedAt: new Date().toISOString()
621
+ queriedAt
609
622
  };
610
623
  } catch (error) {
624
+ const queriedAt = new Date().toISOString();
611
625
  if (error.name === 'NotFound' || error.name === 'NoSuchKey') {
612
626
  return {
613
627
  key,
614
628
  exists: false,
615
- queriedAt: new Date().toISOString()
629
+ queriedAt
616
630
  };
617
631
  }
618
632
 
@@ -655,13 +669,14 @@ export async function createMultipartUpload(key, contentType = 'application/octe
655
669
  const keyToUse = ensureKeyHasAppPrefix(key);
656
670
  const cfg = getBucketConfig(bucketName);
657
671
  const client = getBucketClient(bucketName);
672
+ const uploadedAt = new Date().toISOString();
658
673
  const command = new CreateMultipartUploadCommand({
659
674
  Bucket: cfg.BUCKET_NAME,
660
675
  Key: keyToUse,
661
676
  ContentType: contentType,
662
677
  Metadata: {
663
678
  'upload-source': 'web-portal',
664
- 'uploaded-at': new Date().toISOString(),
679
+ 'uploaded-at': uploadedAt,
665
680
  ...metadata
666
681
  }
667
682
  });
@@ -803,7 +818,7 @@ export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefi
803
818
  const uploads = await listIncompleteMultipartUploads(prefix, bucketName);
804
819
 
805
820
  if (!uploads.length) {
806
- console.log('[mbkbucket] No incomplete multipart uploads found');
821
+ debugS3('No incomplete multipart uploads found');
807
822
  return { abortedCount: 0, uploads: [] };
808
823
  }
809
824
 
@@ -815,22 +830,22 @@ export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefi
815
830
  });
816
831
 
817
832
  if (!uploadsToAbort.length) {
818
- console.log(`[mbkbucket] No incomplete uploads older than ${olderThanDays} days`);
833
+ debugS3('No incomplete uploads older than %s days', olderThanDays);
819
834
  return { abortedCount: 0, uploads: [] };
820
835
  }
821
836
 
822
- console.log(`[mbkbucket] Found ${uploadsToAbort.length} incomplete uploads to clean up`);
837
+ debugS3('Found %s incomplete uploads to clean up', uploadsToAbort.length);
823
838
 
824
839
  const aborted = [];
825
840
  for (const upload of uploadsToAbort) {
826
841
  try {
827
- await abortMultipartUpload(upload.Key, upload.UploadId);
842
+ await abortMultipartUpload(upload.Key, upload.UploadId, bucketName);
828
843
  aborted.push({
829
844
  key: upload.Key,
830
845
  uploadId: upload.UploadId,
831
846
  initiated: upload.Initiated
832
847
  });
833
- console.log(`[mbkbucket] Aborted incomplete upload: ${upload.Key} (initiated: ${upload.Initiated})`);
848
+ debugS3('Aborted incomplete upload: %s (initiated: %s)', upload.Key, upload.Initiated);
834
849
  } catch (err) {
835
850
  console.error(`[mbkbucket] Failed to abort upload ${upload.Key}:`, err);
836
851
  }
package/package.json CHANGED
@@ -1,32 +1,38 @@
1
1
  {
2
2
  "name": "mbkbucket",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Flexible S3/R2 bucket management library and Express router for mbktech.org applications, providing upload, download, listing, and multipart utilities with built-in configuration validation and health checks",
5
5
  "author": "Muhammad Bin Khalid",
6
6
  "homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
7
- "license": "GPL-2.0",
7
+ "license": "LGPL-3.0-only",
8
8
  "main": "index.js",
9
9
  "type": "module",
10
10
  "types": "index.d.ts",
11
11
  "scripts": {
12
- "dev": "cross-env mbkbucket_test=dev nodemon index.js",
12
+ "dev": "cross-env NODE_ENV=dev nodemon index.js",
13
13
  "test": "node --test"
14
14
  },
15
15
  "dependencies": {
16
16
  "@aws-sdk/client-s3": "^3.968.0",
17
17
  "@aws-sdk/s3-request-presigner": "^3.1000.0",
18
18
  "compression": "^1.8.0",
19
+ "debug": "^2.6.9",
19
20
  "dotenv": "^16.6.1",
20
21
  "express": "^4.22.1",
21
22
  "express-handlebars": "^8.0.3",
22
23
  "express-rate-limit": "^7.0.1",
23
- "mbkauthe": "latest",
24
+ "mbkauthe": "5.1.1",
24
25
  "multer": "^2.0.2",
25
26
  "pg": "^8.16.3"
26
27
  },
27
28
  "devDependencies": {
28
- "nodemon": "^2.0.22",
29
+ "nodemon": "^3.1.14",
29
30
  "cross-env": "^7.0.3"
31
+ },
32
+ "overrides": {
33
+ "csurf": {
34
+ "cookie": "^0.7.0"
35
+ }
30
36
  },
31
37
  "repository": {
32
38
  "type": "git",
@@ -49,4 +55,4 @@
49
55
  "mbkauthe",
50
56
  "mbktech.org"
51
57
  ]
52
- }
58
+ }
@@ -731,9 +731,18 @@
731
731
  align-items: center;
732
732
  gap: 0.5rem;
733
733
  font-size: 0.9rem;
734
+ min-width: 0;
735
+ }
736
+
737
+ .breadcrumb-nav .breadcrumb-item::before {
738
+ display: none !important;
739
+ content: none !important;
734
740
  }
735
741
 
736
742
  .breadcrumb-link {
743
+ background: transparent;
744
+ border: 0;
745
+ padding: 0;
737
746
  color: var(--primary-color-ml);
738
747
  cursor: pointer;
739
748
  transition: all 0.2s ease;
@@ -754,6 +763,8 @@
754
763
  .breadcrumb-current {
755
764
  color: var(--dark-color-ml);
756
765
  font-weight: 600;
766
+ min-width: 0;
767
+ overflow-wrap: anywhere;
757
768
  }
758
769
 
759
770
  .file-icon {
@@ -2048,6 +2059,19 @@
2048
2059
  .breadcrumb-nav {
2049
2060
  padding: 0.5rem 1rem;
2050
2061
  font-size: 0.85rem;
2062
+ gap: 0.35rem;
2063
+ overflow: visible;
2064
+ }
2065
+
2066
+ .breadcrumb-item {
2067
+ max-width: 100%;
2068
+ }
2069
+
2070
+ .breadcrumb-current,
2071
+ .breadcrumb-link {
2072
+ max-width: calc(100vw - 4rem);
2073
+ white-space: normal;
2074
+ overflow-wrap: anywhere;
2051
2075
  }
2052
2076
 
2053
2077
  .file-size {
@@ -2172,4 +2196,103 @@
2172
2196
  .dtm {
2173
2197
  display: block;
2174
2198
  }
2175
- }
2199
+ }
2200
+
2201
+ .table-cell-center {
2202
+ text-align: center;
2203
+ }
2204
+
2205
+ .table-select-col {
2206
+ width: 48px;
2207
+ text-align: center;
2208
+ }
2209
+
2210
+ .file-name-cell,
2211
+ .file-name-heading {
2212
+ padding-left: 1.5rem;
2213
+ }
2214
+
2215
+ .file-row-main {
2216
+ display: flex;
2217
+ align-items: center;
2218
+ }
2219
+
2220
+ .folder-name {
2221
+ font-weight: 700;
2222
+ }
2223
+
2224
+ .folder-leading-icon {
2225
+ margin-right: 0.5rem;
2226
+ color: #f59e0b;
2227
+ }
2228
+
2229
+ .muted-cell-value {
2230
+ color: var(--gray-color-ml);
2231
+ }
2232
+
2233
+ .path-display-icon {
2234
+ margin-right: 4px;
2235
+ opacity: 0.5;
2236
+ }
2237
+
2238
+ .copy-btn {
2239
+ color: #fff;
2240
+ }
2241
+
2242
+ .view-btn.is-disabled {
2243
+ opacity: 0.5;
2244
+ }
2245
+
2246
+ .sortable-heading {
2247
+ cursor: pointer;
2248
+ }
2249
+
2250
+ .bulk-actions-controls {
2251
+ float: right;
2252
+ }
2253
+
2254
+ .toast-icon {
2255
+ font-size: 1.25rem;
2256
+ }
2257
+
2258
+ .toast-message {
2259
+ flex: 1;
2260
+ }
2261
+
2262
+ .empty-state-error-icon {
2263
+ color: var(--danger-color-ml);
2264
+ }
2265
+
2266
+ .empty-state-folder-icon {
2267
+ font-size: 4rem;
2268
+ color: #cbd5e0;
2269
+ }
2270
+
2271
+ .upload-progress-cell {
2272
+ min-width: 220px;
2273
+ }
2274
+
2275
+ .upload-progress-track {
2276
+ height: 8px;
2277
+ margin-bottom: 6px;
2278
+ }
2279
+
2280
+ .upload-progress-bar {
2281
+ height: 8px;
2282
+ }
2283
+
2284
+ .progress-bar-empty {
2285
+ width: 0%;
2286
+ }
2287
+
2288
+ .is-hidden {
2289
+ display: none !important;
2290
+ }
2291
+
2292
+ .bucket-switcher-status:not(.is-hidden) {
2293
+ display: inline;
2294
+ }
2295
+
2296
+ .file-list-card-body {
2297
+ padding: 0;
2298
+ }