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.
- package/.env.example +3 -2
- package/README.md +96 -44
- package/index.js +4 -1
- package/lib/cli.js +1 -1
- package/lib/public/bucketadmin.js +255 -33
- package/lib/public/mbkbucket-helper-demo.html +0 -58
- package/lib/public/mbkbucket-helper.js +1 -9
- package/lib/src/app.js +10 -24
- package/lib/src/config/validation.js +6 -1
- package/lib/src/controllers/api.controller.js +34 -6
- package/lib/src/controllers/dashboard.controller.js +2 -2
- package/lib/src/controllers/info.controller.js +15 -14
- package/lib/src/controllers/view.controller.js +24 -22
- package/lib/src/core/metrics.js +134 -0
- package/lib/src/core/storage/capabilities.js +58 -0
- package/lib/src/core/storage/errors.js +0 -5
- package/lib/src/core/storage/gdrive-auth.js +0 -36
- package/lib/src/core/storage/gdrive-provider.js +103 -44
- package/lib/src/core/storage/index.js +4 -1
- package/lib/src/core/storage/local-provider.js +508 -0
- package/lib/src/core/storage/models.js +91 -50
- package/lib/src/core/storage/path-strategy.js +22 -2
- package/lib/src/core/storage/s3-provider.js +19 -38
- package/lib/src/core/storage/storage-manager.js +76 -53
- package/lib/src/core/storage/storage-provider.js +49 -120
- package/lib/src/middleware/error-handler.js +15 -52
- package/lib/src/middleware/rate-limiter.js +1 -1
- package/lib/src/routes/api.routes.js +4 -1
- package/lib/src/services/s3.service.js +2 -2
- package/lib/src/services/storage.service.js +118 -20
- package/lib/src/utils/cleanup-scheduler.js +91 -0
- package/lib/src/utils/helpers.js +8 -51
- package/package.json +4 -4
- package/views/bucket.hbs +0 -5
- 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,
|
|
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.
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
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
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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 {
|
|
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
|
+
|