mbkbucket 4.0.1 → 4.2.0

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 (35) hide show
  1. package/.env.example +3 -2
  2. package/README.md +96 -44
  3. package/index.js +4 -1
  4. package/lib/cli.js +1 -1
  5. package/lib/public/bucketadmin.js +255 -33
  6. package/lib/public/mbkbucket-helper-demo.html +0 -58
  7. package/lib/public/mbkbucket-helper.js +1 -9
  8. package/lib/src/app.js +10 -24
  9. package/lib/src/config/validation.js +6 -1
  10. package/lib/src/controllers/api.controller.js +34 -6
  11. package/lib/src/controllers/dashboard.controller.js +2 -2
  12. package/lib/src/controllers/info.controller.js +15 -14
  13. package/lib/src/controllers/view.controller.js +24 -22
  14. package/lib/src/core/metrics.js +134 -0
  15. package/lib/src/core/storage/capabilities.js +58 -0
  16. package/lib/src/core/storage/errors.js +0 -5
  17. package/lib/src/core/storage/gdrive-auth.js +0 -36
  18. package/lib/src/core/storage/gdrive-provider.js +103 -44
  19. package/lib/src/core/storage/index.js +4 -1
  20. package/lib/src/core/storage/local-provider.js +508 -0
  21. package/lib/src/core/storage/models.js +91 -50
  22. package/lib/src/core/storage/path-strategy.js +22 -2
  23. package/lib/src/core/storage/s3-provider.js +19 -38
  24. package/lib/src/core/storage/storage-manager.js +76 -53
  25. package/lib/src/core/storage/storage-provider.js +49 -120
  26. package/lib/src/middleware/error-handler.js +15 -52
  27. package/lib/src/middleware/rate-limiter.js +1 -1
  28. package/lib/src/routes/api.routes.js +4 -1
  29. package/lib/src/services/s3.service.js +2 -2
  30. package/lib/src/services/storage.service.js +118 -20
  31. package/lib/src/utils/cleanup-scheduler.js +91 -0
  32. package/lib/src/utils/helpers.js +8 -51
  33. package/package.json +4 -4
  34. package/views/bucket.hbs +0 -5
  35. package/views/bucketadmin_preview_modal.hbs +3 -3
@@ -6,11 +6,13 @@
6
6
 
7
7
  import { Readable } from "node:stream";
8
8
  import { StorageProvider } from "./storage-provider.js";
9
+ import { StorageCapabilities } from "./capabilities.js";
9
10
  import { StorageFile, StorageFolder, StorageListResult } from "./models.js";
10
11
  import { StorageNotFoundError, StorageConflictError, StorageAccessDeniedError, StorageValidationError, StorageConfigError, StorageError } from "./errors.js";
11
12
  import { refreshGoogleAccessToken, getServiceAccountAccessToken } from "./gdrive-auth.js";
12
13
  import { createLogger } from "#logger";
13
- import { nowIso, trimSlashes, trimLeadingSlashes, getBaseName, getFolderPath } from "#helpers";
14
+ import { nowIso, getFolderPath } from "#helpers";
15
+ import { trimSlashes, trimLeadingSlashes, getBaseName } from "mbkutils";
14
16
 
15
17
  const debugGDrive = createLogger('gdrive-provider');
16
18
 
@@ -19,19 +21,49 @@ function escapeGdriveQuery(str = '') {
19
21
  }
20
22
 
21
23
  export class GoogleDriveStorageProvider extends StorageProvider {
22
- /**
23
- * @param {string} name - Connection identifier
24
- * @param {object} config - Configuration object
25
- */
26
24
  constructor(name, config = {}) {
27
25
  super(name, config);
28
26
  this.rootFolderId = config.folder_id || config.root_folder_id || 'root';
29
27
  this._accessToken = config.access_token || null;
30
28
  this._tokenExpiry = config.expiry_date || 0;
31
29
  this._folderCache = new Map();
32
- this._folderCache.set('', this.rootFolderId);
33
- this._folderCache.set('.', this.rootFolderId);
34
- this._folderCache.set('/', this.rootFolderId);
30
+ this._setFolderCache('', this.rootFolderId);
31
+ this._setFolderCache('.', this.rootFolderId);
32
+ this._setFolderCache('/', this.rootFolderId);
33
+ }
34
+
35
+ _setFolderCache(path, id) {
36
+ if (this._folderCache.size >= 500) {
37
+ const oldestKey = this._folderCache.keys().next().value;
38
+ this._folderCache.delete(oldestKey);
39
+ }
40
+ this._folderCache.set(path, { id, expires: Date.now() + 300000 });
41
+ }
42
+
43
+ _getFolderCache(path) {
44
+ const entry = this._folderCache.get(path);
45
+ if (!entry) return null;
46
+ if (typeof entry === 'object' && entry.expires && entry.expires < Date.now()) {
47
+ this._folderCache.delete(path);
48
+ return null;
49
+ }
50
+ return typeof entry === 'object' && entry.id ? entry.id : entry;
51
+ }
52
+
53
+ _invalidateFolderCache(prefix = '') {
54
+ if (!prefix) {
55
+ this._folderCache.clear();
56
+ this._setFolderCache('', this.rootFolderId);
57
+ this._setFolderCache('.', this.rootFolderId);
58
+ this._setFolderCache('/', this.rootFolderId);
59
+ return;
60
+ }
61
+ this._folderCache.delete(prefix);
62
+ for (const k of this._folderCache.keys()) {
63
+ if (k.startsWith(`${prefix}/`)) {
64
+ this._folderCache.delete(k);
65
+ }
66
+ }
35
67
  }
36
68
 
37
69
  get type() {
@@ -39,16 +71,19 @@ export class GoogleDriveStorageProvider extends StorageProvider {
39
71
  }
40
72
 
41
73
  get capabilities() {
42
- return {
74
+ return new StorageCapabilities({
43
75
  multipart: false,
44
- presignedUrls: false,
76
+ resumable: false,
45
77
  nativeFolders: true,
46
78
  ranges: true,
47
79
  copy: true,
48
80
  move: true,
49
81
  search: true,
50
- directDownloadUrl: true
51
- };
82
+ directDownloadUrl: true,
83
+ versioning: false,
84
+ publicUrls: true,
85
+ metadata: true
86
+ });
52
87
  }
53
88
 
54
89
  async getAccessToken() {
@@ -102,29 +137,45 @@ export class GoogleDriveStorageProvider extends StorageProvider {
102
137
  }
103
138
 
104
139
  async _fetch(url, options = {}) {
105
- const token = await this.getAccessToken();
106
- const headers = new Headers(options.headers || {});
107
- headers.set('Authorization', `Bearer ${token}`);
108
-
109
- let response = await fetch(url, { ...options, headers });
110
-
111
- // Handle 401 token expiration retry once
112
- if (response.status === 401 && (this.config.refresh_token || this.config.client_email)) {
113
- debugGDrive('Received 401 from Google Drive API, retrying with refreshed token...');
114
- this._accessToken = null;
115
- this._tokenExpiry = 0;
116
- const newToken = await this.getAccessToken();
117
- headers.set('Authorization', `Bearer ${newToken}`);
118
- response = await fetch(url, { ...options, headers });
119
- }
140
+ const maxRetries = options.maxRetries ?? 3;
141
+ let attempt = 0;
142
+
143
+ while (attempt <= maxRetries) {
144
+ const token = await this.getAccessToken();
145
+ const headers = new Headers(options.headers || {});
146
+ headers.set('Authorization', `Bearer ${token}`);
147
+
148
+ let response = await fetch(url, { ...options, headers });
149
+
150
+ // Handle 401 token expiration retry once
151
+ if (response.status === 401 && attempt === 0 && (this.config.refresh_token || this.config.client_email)) {
152
+ debugGDrive('Received 401 from Google Drive API, retrying with refreshed token...');
153
+ this._accessToken = null;
154
+ this._tokenExpiry = 0;
155
+ const newToken = await this.getAccessToken();
156
+ headers.set('Authorization', `Bearer ${newToken}`);
157
+ response = await fetch(url, { ...options, headers });
158
+ }
159
+
160
+ // Handle 429 Too Many Requests or 503/500 temporary service errors with exponential backoff
161
+ if ((response.status === 429 || response.status === 503 || response.status === 500) && attempt < maxRetries) {
162
+ attempt++;
163
+ const retryAfter = Number(response.headers.get('retry-after')) || 0;
164
+ const delayMs = retryAfter > 0 ? retryAfter * 1000 : Math.min(300 * (2 ** (attempt - 1)) + Math.random() * 150, 4000);
165
+ debugGDrive('Google Drive API rate limit/error (%s). Retrying attempt %s in %sms...', response.status, attempt, Math.round(delayMs));
166
+ await new Promise(r => setTimeout(r, delayMs));
167
+ continue;
168
+ }
120
169
 
121
- return response;
170
+ return response;
171
+ }
122
172
  }
123
173
 
124
174
  async _resolvePathToFolderId(path = '') {
125
175
  const cleanPath = trimSlashes(path);
126
176
  if (!cleanPath) return this.rootFolderId;
127
- if (this._folderCache.has(cleanPath)) return this._folderCache.get(cleanPath);
177
+ const cached = this._getFolderCache(cleanPath);
178
+ if (cached) return cached;
128
179
 
129
180
  const segments = cleanPath.split('/').filter(Boolean);
130
181
  let currentParentId = this.rootFolderId;
@@ -132,8 +183,9 @@ export class GoogleDriveStorageProvider extends StorageProvider {
132
183
 
133
184
  for (const segment of segments) {
134
185
  currentPath = currentPath ? `${currentPath}/${segment}` : segment;
135
- if (this._folderCache.has(currentPath)) {
136
- currentParentId = this._folderCache.get(currentPath);
186
+ const segCached = this._getFolderCache(currentPath);
187
+ if (segCached) {
188
+ currentParentId = segCached;
137
189
  continue;
138
190
  }
139
191
 
@@ -151,7 +203,7 @@ export class GoogleDriveStorageProvider extends StorageProvider {
151
203
  }
152
204
 
153
205
  currentParentId = data.files[0].id;
154
- this._folderCache.set(currentPath, currentParentId);
206
+ this._setFolderCache(currentPath, currentParentId);
155
207
  }
156
208
 
157
209
  return currentParentId;
@@ -160,7 +212,8 @@ export class GoogleDriveStorageProvider extends StorageProvider {
160
212
  async _ensureFolderPath(path = '') {
161
213
  const cleanPath = trimSlashes(path);
162
214
  if (!cleanPath) return this.rootFolderId;
163
- if (this._folderCache.has(cleanPath)) return this._folderCache.get(cleanPath);
215
+ const cached = this._getFolderCache(cleanPath);
216
+ if (cached) return cached;
164
217
 
165
218
  const segments = cleanPath.split('/').filter(Boolean);
166
219
  let currentParentId = this.rootFolderId;
@@ -168,8 +221,9 @@ export class GoogleDriveStorageProvider extends StorageProvider {
168
221
 
169
222
  for (const segment of segments) {
170
223
  currentPath = currentPath ? `${currentPath}/${segment}` : segment;
171
- if (this._folderCache.has(currentPath)) {
172
- currentParentId = this._folderCache.get(currentPath);
224
+ const segCached = this._getFolderCache(currentPath);
225
+ if (segCached) {
226
+ currentParentId = segCached;
173
227
  continue;
174
228
  }
175
229
 
@@ -202,7 +256,7 @@ export class GoogleDriveStorageProvider extends StorageProvider {
202
256
  currentParentId = createData.id;
203
257
  }
204
258
 
205
- this._folderCache.set(currentPath, currentParentId);
259
+ this._setFolderCache(currentPath, currentParentId);
206
260
  }
207
261
 
208
262
  return currentParentId;
@@ -304,7 +358,11 @@ export class GoogleDriveStorageProvider extends StorageProvider {
304
358
  for (const item of (data.files || [])) {
305
359
  if (item.mimeType === 'application/vnd.google-apps.folder') {
306
360
  const folderPrefix = `${prefixPath}${item.name}/`;
307
- commonPrefixes.push(new StorageFolder(folderPrefix, item.id));
361
+ commonPrefixes.push(new StorageFolder({
362
+ prefix: folderPrefix,
363
+ id: item.id,
364
+ provider: 'gdrive'
365
+ }));
308
366
  this._folderCache.set(trimSlashes(folderPrefix), item.id);
309
367
  } else {
310
368
  const itemKey = `${prefixPath}${item.name}`;
@@ -312,12 +370,16 @@ export class GoogleDriveStorageProvider extends StorageProvider {
312
370
  key: itemKey,
313
371
  size: parseInt(item.size || 0, 10),
314
372
  lastModified: item.modifiedTime || item.createdTime || Date.now(),
373
+ createdAt: item.createdTime,
315
374
  etag: item.md5Checksum ? `"${item.md5Checksum}"` : `"${item.id}"`,
316
375
  storageClass: 'GOOGLE_DRIVE',
376
+ provider: 'gdrive',
317
377
  id: item.id,
378
+ nativeId: item.id,
318
379
  mimeType: item.mimeType,
319
380
  webViewLink: item.webViewLink,
320
- webContentLink: item.webContentLink
381
+ webContentLink: item.webContentLink,
382
+ metadata: item.properties || {}
321
383
  }));
322
384
  }
323
385
  }
@@ -528,12 +590,7 @@ export class GoogleDriveStorageProvider extends StorageProvider {
528
590
  throw new StorageError(`Failed to delete Google Drive folder: ${data.error?.message || res.statusText}`);
529
591
  }
530
592
 
531
- this._folderCache.delete(cleanPrefix);
532
- for (const [k] of this._folderCache.entries()) {
533
- if (k.startsWith(`${cleanPrefix}/`)) {
534
- this._folderCache.delete(k);
535
- }
536
- }
593
+ this._invalidateFolderCache(cleanPrefix);
537
594
 
538
595
  return { deletedCount: 1, prefix: cleanPrefix, deletedAt: nowIso() };
539
596
  } catch (error) {
@@ -587,6 +644,7 @@ export class GoogleDriveStorageProvider extends StorageProvider {
587
644
  }
588
645
 
589
646
  async copyFile(sourceKey, destKey, _options = {}) {
647
+ this.assertCapability('copy', 'copyFile');
590
648
  try {
591
649
  const srcClean = trimLeadingSlashes(sourceKey);
592
650
  const dstClean = trimLeadingSlashes(destKey);
@@ -622,6 +680,7 @@ export class GoogleDriveStorageProvider extends StorageProvider {
622
680
  }
623
681
 
624
682
  async moveFile(sourceKey, destKey, _options = {}) {
683
+ this.assertCapability('move', 'moveFile');
625
684
  try {
626
685
  const srcClean = trimLeadingSlashes(sourceKey);
627
686
  const dstClean = trimLeadingSlashes(destKey);
@@ -5,10 +5,13 @@
5
5
  */
6
6
 
7
7
  export { StorageProvider } from "./storage-provider.js";
8
+ export { StorageCapabilities } from "./capabilities.js";
8
9
  export { S3StorageProvider } from "./s3-provider.js";
9
10
  export { GoogleDriveStorageProvider } from "./gdrive-provider.js";
10
- export { StorageFile, StorageFolder, StorageListResult } from "./models.js";
11
+ export { LocalStorageProvider } from "./local-provider.js";
12
+ export { StorageItem, StorageFile, StorageFolder, StorageListResult } from "./models.js";
11
13
  export { StorageError, StorageNotFoundError, StorageAccessDeniedError, StorageConflictError, StorageValidationError, StorageConfigError, StorageQuotaExceededError, StorageUnsupportedOperationError } from "./errors.js";
12
14
  export { PathStrategy } from "./path-strategy.js";
13
15
  export { getGoogleAuthUrl, exchangeCodeForTokens, refreshGoogleAccessToken, getServiceAccountAccessToken, GOOGLE_DRIVE_SCOPES } from "./gdrive-auth.js";
14
16
  export { StorageManager, storageManager } from "./storage-manager.js";
17
+