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
@@ -1,9 +1,12 @@
1
1
  import { pipeline } from "stream/promises";
2
2
  import { listfiles, uploadFile, deleteFile, deleteFiles, deleteFolder, createFolder as createStorageFolder, downloadFile, getFileMetadata, fileExists, ensureKeyHasAppPrefix, ensurePrefix, createMultipartUpload, uploadPart, completeMultipartUpload, abortMultipartUpload, listIncompleteMultipartUploads, cleanupIncompleteMultipartUploads, getStorageProvider, copyFile, moveFile, checkHealth } from "../services/storage.service.js";
3
+ import { metricsRegistry } from "../core/metrics.js";
3
4
  import { sendApiError, classifyApiError } from "../utils/errors.js";
4
- import { getBaseName, isSensitiveTextType, buildKey, normalizeKeyParam, getFolderPath, destroyStream } from "../utils/helpers.js";
5
+ import { getBaseName } from "mbkutils";
6
+ import { isSensitiveTextType, buildKey, normalizeKeyParam, getFolderPath, destroyStream } from "../utils/helpers.js";
5
7
  import { createLogger } from "#logger";
6
8
 
9
+
7
10
  const debugApi = createLogger('api');
8
11
 
9
12
  function resolvePrefix(req, res, prefix) {
@@ -107,6 +110,9 @@ export async function listFiles(req, res) {
107
110
  .map(p => p.Prefix);
108
111
  nextContinuationToken = result.NextContinuationToken;
109
112
 
113
+ const provider = getStorageProvider(bucketName);
114
+ const capabilities = provider?.capabilities?.toJSON ? provider.capabilities.toJSON() : (provider?.capabilities || {});
115
+
110
116
  return res.json({
111
117
  success: true,
112
118
  files,
@@ -120,7 +126,8 @@ export async function listFiles(req, res) {
120
126
  nextContinuationToken,
121
127
  mode: 'optimized',
122
128
  totalFiles: -1,
123
- paginationType: 'continuation-token'
129
+ paginationType: 'continuation-token',
130
+ capabilities
124
131
  });
125
132
  }
126
133
 
@@ -152,6 +159,9 @@ export async function listFiles(req, res) {
152
159
  const currentPage = Math.max(1, parseInt(page, 10) || 1);
153
160
  const paginatedFiles = files.slice((currentPage - 1) * pageSize, currentPage * pageSize);
154
161
 
162
+ const provider = getStorageProvider(bucketName);
163
+ const capabilities = provider?.capabilities?.toJSON ? provider.capabilities.toJSON() : (provider?.capabilities || {});
164
+
155
165
  res.json({
156
166
  success: true,
157
167
  files: paginatedFiles,
@@ -164,7 +174,8 @@ export async function listFiles(req, res) {
164
174
  hasPrevPage: currentPage > 1,
165
175
  nextPage: currentPage + 1,
166
176
  prevPage: Math.max(1, currentPage - 1),
167
- totalFiles
177
+ totalFiles,
178
+ capabilities
168
179
  });
169
180
  } catch (error) {
170
181
  console.error("Error listing files:", error);
@@ -366,8 +377,9 @@ export async function downloadFileHandler(req, res) {
366
377
  if (aborted) return;
367
378
 
368
379
  const result = await downloadFile(keyToUse, { bucketName: req.activeBucket });
380
+ const stream = result.Body || result.stream;
369
381
  if (aborted) {
370
- destroyStream(result.Body);
382
+ destroyStream(stream);
371
383
  return;
372
384
  }
373
385
 
@@ -386,14 +398,15 @@ export async function downloadFileHandler(req, res) {
386
398
  res.setHeader('Cache-Control', isSensitive ? 'private, no-store' : isLargeFile ? 'private, max-age=600, must-revalidate' : 'private, max-age=1800, must-revalidate');
387
399
 
388
400
  try {
389
- await pipeline(result.Body, res);
401
+ await pipeline(stream, res);
390
402
  } catch (pipeErr) {
391
403
  if (aborted) {
392
- destroyStream(result.Body);
404
+ destroyStream(stream);
393
405
  return;
394
406
  }
395
407
  throw pipeErr;
396
408
  }
409
+
397
410
  } catch (error) {
398
411
  if (aborted) return;
399
412
  console.error("Error downloading file:", error);
@@ -457,3 +470,18 @@ export async function getFileInfo(req, res) {
457
470
  return sendApiError(res, error, 'Failed to get file info');
458
471
  }
459
472
  }
473
+
474
+ export async function getMetricsHandler(req, res) {
475
+ try {
476
+ const format = req.query.format || (req.headers.accept?.includes('application/json') ? 'json' : 'prometheus');
477
+ if (format === 'json') {
478
+ res.setHeader('Content-Type', 'application/json');
479
+ return res.json({ success: true, ...metricsRegistry.getStats() });
480
+ }
481
+ res.setHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
482
+ return res.send(metricsRegistry.toPrometheusText());
483
+ } catch (error) {
484
+ return sendApiError(res, error, 'Failed to generate metrics');
485
+ }
486
+ }
487
+
@@ -2,8 +2,8 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { resolveStorageName, getAvailableStorageNames, getStorageProvider, getStorageConfig } from "../services/storage.service.js";
4
4
  import { storageManager } from "../core/storage/storage-manager.js";
5
- import { renderPage, mbkautheVar } from "mbkauthe";
6
- import { commonHandlebarsHelpers } from "../utils/helpers.js";
5
+ import { renderPage, commonHandlebarsHelpers } from "mbkutils";
6
+ import { mbkautheVar } from "mbkauthe";
7
7
  import { mbkbucketVar } from "../config/index.js";
8
8
 
9
9
  /**
@@ -1,9 +1,10 @@
1
- import { renderPage, mbkautheVar } from "mbkauthe";
1
+ import { renderPage } from "mbkutils";
2
+ import { mbkautheVar } from "mbkauthe";
2
3
  import { packageJson, getLatestVersion, appVersion, mbkbucketVar } from "../config/index.js";
3
4
  import { getAvailableBucketNames, getStorageProvider } from "../services/storage.service.js";
4
5
  import { storageManager } from "../core/storage/storage-manager.js";
5
6
 
6
- async function getInfoData() {
7
+ async function getInfoData(requestedBucket) {
7
8
  let latestVersion = 'unknown';
8
9
  try {
9
10
  latestVersion = (await getLatestVersion()) || 'unknown';
@@ -12,16 +13,21 @@ async function getInfoData() {
12
13
  }
13
14
 
14
15
  const buckets = getAvailableBucketNames();
15
- const defaultBucket = storageManager.getDefaultConnectionName() || (buckets[0] ?? '');
16
+ let defaultBucket = requestedBucket;
17
+ if (!defaultBucket || !buckets.includes(defaultBucket)) {
18
+ defaultBucket = storageManager.getDefaultConnectionName() || (buckets[0] ?? '');
19
+ }
20
+
16
21
  let providerType = 'S3';
22
+ let capabilities = {};
17
23
  if (defaultBucket) {
18
24
  try {
19
25
  const provider = getStorageProvider(defaultBucket);
20
26
  providerType = (provider?.type || 's3').toUpperCase();
27
+ capabilities = provider?.capabilities?.toJSON ? provider.capabilities.toJSON() : (provider?.capabilities || {});
21
28
  } catch {}
22
29
  }
23
30
 
24
- // Safe subset only - strictly prevent leaking passwords, DB connection strings, OAuth secrets, or storage credentials
25
31
  const safeMbkautheVar = {
26
32
  APP_NAME: mbkautheVar?.APP_NAME || mbkautheVar?.app_name || "mbkbucket",
27
33
  };
@@ -41,24 +47,19 @@ async function getInfoData() {
41
47
  mbkbucketVar: safeMbkbucketVar,
42
48
  defaultBucket,
43
49
  configuredBucketsCount: buckets.length,
44
- providerType
50
+ providerType,
51
+ capabilities
45
52
  };
46
53
  }
47
54
 
48
- /**
49
- * Render the mbkbucket info page (Handlebars).
50
- */
51
55
  export async function infoPage(req, res) {
52
56
  renderPage(req, res, "mbkbucket_info.hbs", false, {
53
57
  pageTitle: "MBKBucket Information",
54
58
  ogUrl: "/mbkbucket/info",
55
- ...(await getInfoData())
59
+ ...(await getInfoData(req.query?.bucket))
56
60
  });
57
61
  }
58
62
 
59
- /**
60
- * Return bucket info as JSON.
61
- */
62
- export async function infoJson(_req, res) {
63
- res.json(await getInfoData());
63
+ export async function infoJson(req, res) {
64
+ res.json(await getInfoData(req.query?.bucket));
64
65
  }
@@ -1,11 +1,13 @@
1
- import { downloadFile, getFileMetadata, ensureKeyHasAppPrefix, generateSignedUrl, resolveStorageName, getStorageProvider } from "../services/storage.service.js";
1
+ import { downloadFile, getFileMetadata, ensureKeyHasAppPrefix, resolveStorageName, getStorageProvider } from "../services/storage.service.js";
2
2
  import { mbkbucketVar } from "../config/index.js";
3
- import { escapeHtml, getBaseName, getFileExt, getMimeType, normalizeKeyParam, destroyStream, VIEWABLE_TYPES, VIEWABLE_TYPES_SORTED, STATIC_ASSET_TYPES, VIDEO_TYPES, AUDIO_TYPES, buildCacheControl, parseRangeHeader, renderPlayerPage } from "../utils/helpers.js";
3
+ import { escapeHtml, getBaseName, getFileExt } from "mbkutils";
4
+ import { getMimeType, normalizeKeyParam, destroyStream, VIEWABLE_TYPES, VIEWABLE_TYPES_SORTED, STATIC_ASSET_TYPES, VIDEO_TYPES, AUDIO_TYPES, buildCacheControl, parseRangeHeader, renderPlayerPage } from "../utils/helpers.js";
4
5
  import { createLogger } from "#logger";
5
6
 
6
7
  const debugView = createLogger('view');
7
8
 
8
9
  function attachStreamCleanup(req, res, stream, timeoutMs = 120000) {
10
+ if (!stream || typeof stream.on !== 'function') return;
9
11
  const cleanup = () => destroyStream(stream);
10
12
 
11
13
  stream.on('error', (err) => {
@@ -25,6 +27,7 @@ function attachStreamCleanup(req, res, stream, timeoutMs = 120000) {
25
27
  res.on('close', cleanup);
26
28
  }
27
29
 
30
+
28
31
  async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName, publicCache = false } = {}) {
29
32
  const fileName = getBaseName(keyToUse);
30
33
  const fileExtension = getFileExt(fileName);
@@ -48,6 +51,8 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
48
51
  const isPdf = fileExtension === 'pdf';
49
52
  const supportsRanges = isVideo || isAudio || isPdf;
50
53
  const contentType = getMimeType(fileName);
54
+ const safeAsciiName = fileName.replace(/["\\]/g, '').replace(/[^\x20-\x7E]/g, '_');
55
+ const encodedName = encodeURIComponent(fileName);
51
56
 
52
57
  // Fast path for range requests (video / audio / PDF streaming)
53
58
  if (supportsRanges && rangeHeader) {
@@ -86,21 +91,27 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
86
91
  res.setHeader('Content-Range', `bytes ${start}-${end}/${total}`);
87
92
  res.setHeader('Content-Length', chunksize);
88
93
  res.setHeader('Content-Type', contentType);
94
+ res.setHeader('Content-Disposition', `inline; filename="${safeAsciiName}"; filename*=UTF-8''${encodedName}`);
89
95
  res.setHeader('Cache-Control', buildCacheControl(contentType, { supportsRanges: true, publicCache }));
90
96
  res.setHeader('ETag', etag);
91
97
  res.setHeader('Accept-Ranges', 'bytes');
98
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
92
99
  if (noindex) res.setHeader('X-Robots-Tag', 'noindex, nofollow');
93
100
  res.setHeader('X-Content-Type-Options', 'nosniff');
94
- if (meta.LastModified) res.setHeader('Last-Modified', new Date(meta.LastModified).toUTCString());
95
-
96
- attachStreamCleanup(req, res, rangeResult.Body);
97
- return rangeResult.Body.pipe(res);
101
+ const stream = rangeResult.Body || rangeResult.stream;
102
+ attachStreamCleanup(req, res, stream);
103
+ return stream.pipe(res);
98
104
  } catch (rangeError) {
99
- console.error('Range request failed:', rangeError);
105
+ console.error('[mbkbucket] Range request failed for %s:', keyToUse, rangeError);
106
+ if (!res.headersSent) {
107
+ return res.status(500).json({ message: 'Range stream failed', error: rangeError.message });
108
+ }
109
+ return;
100
110
  }
101
111
  }
102
112
  }
103
113
 
114
+
104
115
  // Standard path: full-file download
105
116
  const downloadOptions = {
106
117
  ...(ifNoneMatch && { ifNoneMatch }),
@@ -130,13 +141,14 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
130
141
  if (ifNoneMatch === etag) return res.status(304).end();
131
142
 
132
143
  res.setHeader('Content-Type', contentType);
133
- res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
144
+ res.setHeader('Content-Disposition', `inline; filename="${safeAsciiName}"; filename*=UTF-8''${encodedName}`);
134
145
  res.setHeader('ETag', etag);
135
146
  res.setHeader('X-Content-Type-Options', 'nosniff');
136
147
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
137
148
  if (noindex) res.setHeader('X-Robots-Tag', 'noindex, nofollow');
138
149
  if (result.ContentLength) res.setHeader('Content-Length', result.ContentLength);
139
150
  if (result.LastModified) res.setHeader('Last-Modified', new Date(result.LastModified).toUTCString());
151
+
140
152
  if (fileExtension === 'html' || fileExtension === 'htm') {
141
153
  res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
142
154
  }
@@ -144,11 +156,13 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
144
156
  res.setHeader('Cache-Control', buildCacheControl(contentType, { isStaticAsset, supportsRanges, publicCache }));
145
157
  res.status(200);
146
158
 
159
+ const stream = result.Body || result.stream;
147
160
  const timeoutDuration = result.ContentLength > 50 * 1024 * 1024 ? 300000 : 120000;
148
- attachStreamCleanup(req, res, result.Body, timeoutDuration);
149
- result.Body.pipe(res);
161
+ attachStreamCleanup(req, res, stream, timeoutDuration);
162
+ stream.pipe(res);
150
163
  }
151
164
 
165
+
152
166
  export async function viewFile(req, res) {
153
167
  try {
154
168
  const key = normalizeKeyParam(req.params.key);
@@ -213,18 +227,6 @@ export async function publicView(req, res) {
213
227
  return res.status(400).json({ message: e.message });
214
228
  }
215
229
 
216
- if (!mbkbucketVar?.p_view_inline) {
217
- try {
218
- const provider = getStorageProvider(bucketName);
219
- if (provider.capabilities.presignedUrls) {
220
- const signed = await generateSignedUrl(keyToUse, 'getObject', 3600, bucketName);
221
- return res.redirect(302, signed.url);
222
- }
223
- } catch (err) {
224
- debugView('Signed URL redirection failed, falling back to inline serve: %s', err.message);
225
- }
226
- }
227
-
228
230
  await serveFileInline(req, res, keyToUse, { noindex: true, bucketName, publicCache: true });
229
231
  } catch (error) {
230
232
  console.error('[mbkbucket] p_view error:', error);
@@ -0,0 +1,134 @@
1
+ /**
2
+ * MBKBucket - Metrics & Observability Registry
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ export class MetricsRegistry {
8
+ constructor() {
9
+ this.reset();
10
+ }
11
+
12
+ reset() {
13
+ this.operations = new Map(); // key: "op:provider:status" -> count
14
+ this.durations = new Map(); // key: "op:provider" -> { totalMs, count, minMs, maxMs }
15
+ this.bytesTransferred = new Map(); // key: "direction:provider" -> bytes
16
+ this.errors = new Map(); // key: "op:provider:errorType" -> count
17
+ this.activeMultipart = 0;
18
+ this.startedAt = new Date().toISOString();
19
+ }
20
+
21
+ recordOperation(operation, { provider = 'unknown', status = 'success', durationMs = 0, bytes = 0 } = {}) {
22
+ const opKey = `${operation}:${provider}:${status}`;
23
+ this.operations.set(opKey, (this.operations.get(opKey) || 0) + 1);
24
+
25
+ if (durationMs >= 0) {
26
+ const durKey = `${operation}:${provider}`;
27
+ const existing = this.durations.get(durKey) || { totalMs: 0, count: 0, minMs: Infinity, maxMs: 0 };
28
+ existing.totalMs += durationMs;
29
+ existing.count += 1;
30
+ existing.minMs = Math.min(existing.minMs, durationMs);
31
+ existing.maxMs = Math.max(existing.maxMs, durationMs);
32
+ this.durations.set(durKey, existing);
33
+ }
34
+
35
+ if (bytes > 0) {
36
+ const direction = operation.startsWith('download') || operation.startsWith('view') ? 'outbound' : 'inbound';
37
+ const byteKey = `${direction}:${provider}`;
38
+ this.bytesTransferred.set(byteKey, (this.bytesTransferred.get(byteKey) || 0) + bytes);
39
+ }
40
+ }
41
+
42
+ recordError(operation, provider = 'unknown', errorType = 'Error') {
43
+ const errKey = `${operation}:${provider}:${errorType}`;
44
+ this.errors.set(errKey, (this.errors.get(errKey) || 0) + 1);
45
+ }
46
+
47
+ incrementActiveMultipart(delta = 1) {
48
+ this.activeMultipart = Math.max(0, this.activeMultipart + delta);
49
+ }
50
+
51
+ getStats() {
52
+ const ops = {};
53
+ for (const [key, count] of this.operations.entries()) {
54
+ ops[key] = count;
55
+ }
56
+
57
+ const durations = {};
58
+ for (const [key, d] of this.durations.entries()) {
59
+ durations[key] = {
60
+ count: d.count,
61
+ avgMs: d.count ? Math.round((d.totalMs / d.count) * 100) / 100 : 0,
62
+ minMs: d.minMs === Infinity ? 0 : d.minMs,
63
+ maxMs: d.maxMs
64
+ };
65
+ }
66
+
67
+ const bytes = {};
68
+ for (const [key, val] of this.bytesTransferred.entries()) {
69
+ bytes[key] = val;
70
+ }
71
+
72
+ const errs = {};
73
+ for (const [key, count] of this.errors.entries()) {
74
+ errs[key] = count;
75
+ }
76
+
77
+ return {
78
+ startedAt: this.startedAt,
79
+ activeMultipartUploads: this.activeMultipart,
80
+ operations: ops,
81
+ durations,
82
+ bytesTransferred: bytes,
83
+ errors: errs,
84
+ timestamp: new Date().toISOString()
85
+ };
86
+ }
87
+
88
+ toPrometheusText() {
89
+ const lines = [
90
+ '# HELP mbkbucket_operations_total Total number of storage operations executed',
91
+ '# TYPE mbkbucket_operations_total counter'
92
+ ];
93
+
94
+ for (const [key, count] of this.operations.entries()) {
95
+ const [operation, provider, status] = key.split(':');
96
+ lines.push(`mbkbucket_operations_total{operation="${operation}",provider="${provider}",status="${status}"} ${count}`);
97
+ }
98
+
99
+ lines.push('');
100
+ lines.push('# HELP mbkbucket_operation_duration_seconds Total duration of storage operations');
101
+ lines.push('# TYPE mbkbucket_operation_duration_seconds summary');
102
+ for (const [key, d] of this.durations.entries()) {
103
+ const [operation, provider] = key.split(':');
104
+ lines.push(`mbkbucket_operation_duration_seconds_count{operation="${operation}",provider="${provider}"} ${d.count}`);
105
+ lines.push(`mbkbucket_operation_duration_seconds_sum{operation="${operation}",provider="${provider}"} ${(d.totalMs / 1000).toFixed(4)}`);
106
+ }
107
+
108
+ lines.push('');
109
+ lines.push('# HELP mbkbucket_transferred_bytes_total Total bytes transferred into or out of storage');
110
+ lines.push('# TYPE mbkbucket_transferred_bytes_total counter');
111
+ for (const [key, bytes] of this.bytesTransferred.entries()) {
112
+ const [direction, provider] = key.split(':');
113
+ lines.push(`mbkbucket_transferred_bytes_total{direction="${direction}",provider="${provider}"} ${bytes}`);
114
+ }
115
+
116
+ lines.push('');
117
+ lines.push('# HELP mbkbucket_errors_total Total storage errors encountered');
118
+ lines.push('# TYPE mbkbucket_errors_total counter');
119
+ for (const [key, count] of this.errors.entries()) {
120
+ const [operation, provider, errorType] = key.split(':');
121
+ lines.push(`mbkbucket_errors_total{operation="${operation}",provider="${provider}",error_type="${errorType}"} ${count}`);
122
+ }
123
+
124
+ lines.push('');
125
+ lines.push('# HELP mbkbucket_active_multipart_uploads Current number of active multipart uploads');
126
+ lines.push('# TYPE mbkbucket_active_multipart_uploads gauge');
127
+ lines.push(`mbkbucket_active_multipart_uploads ${this.activeMultipart}`);
128
+ lines.push('');
129
+
130
+ return lines.join('\n');
131
+ }
132
+ }
133
+
134
+ export const metricsRegistry = new MetricsRegistry();
@@ -0,0 +1,58 @@
1
+ /**
2
+ * MBKBucket - Storage Provider Capabilities
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import { StorageUnsupportedOperationError } from "./errors.js";
8
+
9
+ export class StorageCapabilities {
10
+ constructor(flags = {}) {
11
+ this.multipart = Boolean(flags.multipart);
12
+ this.resumable = Boolean(flags.resumable ?? flags.multipart);
13
+ this.nativeFolders = Boolean(flags.nativeFolders);
14
+ this.ranges = Boolean(flags.ranges ?? true);
15
+ this.copy = Boolean(flags.copy);
16
+ this.move = Boolean(flags.move);
17
+ this.search = Boolean(flags.search);
18
+ this.directDownloadUrl = Boolean(flags.directDownloadUrl);
19
+ this.versioning = Boolean(flags.versioning);
20
+ this.publicUrls = Boolean(flags.publicUrls);
21
+ this.storageQuota = Boolean(flags.storageQuota);
22
+ this.deleteBatch = Boolean(flags.deleteBatch ?? true);
23
+ this.metadata = Boolean(flags.metadata ?? true);
24
+
25
+ Object.freeze(this);
26
+ }
27
+
28
+ has(capability) {
29
+ return Boolean(this[capability]);
30
+ }
31
+
32
+ assert(capability, operationName, providerType = 'storage provider') {
33
+ if (!this.has(capability)) {
34
+ const op = operationName || capability;
35
+ throw new StorageUnsupportedOperationError(
36
+ `Operation '${op}' is not supported by ${providerType} (missing capability: ${capability})`
37
+ );
38
+ }
39
+ }
40
+
41
+ toJSON() {
42
+ return {
43
+ multipart: this.multipart,
44
+ resumable: this.resumable,
45
+ nativeFolders: this.nativeFolders,
46
+ ranges: this.ranges,
47
+ copy: this.copy,
48
+ move: this.move,
49
+ search: this.search,
50
+ directDownloadUrl: this.directDownloadUrl,
51
+ versioning: this.versioning,
52
+ publicUrls: this.publicUrls,
53
+ storageQuota: this.storageQuota,
54
+ deleteBatch: this.deleteBatch,
55
+ metadata: this.metadata
56
+ };
57
+ }
58
+ }
@@ -5,11 +5,6 @@
5
5
  */
6
6
 
7
7
  export class StorageError extends Error {
8
- /**
9
- * @param {string} message
10
- * @param {number} [status=500]
11
- * @param {string} [code='INTERNAL_STORAGE_ERROR']
12
- */
13
8
  constructor(message, status = 500, code = 'INTERNAL_STORAGE_ERROR') {
14
9
  super(message);
15
10
  this.name = this.constructor.name;
@@ -15,17 +15,6 @@ export const GOOGLE_DRIVE_SCOPES = [
15
15
  'https://www.googleapis.com/auth/drive.metadata.readonly'
16
16
  ];
17
17
 
18
- /**
19
- * Generate Google OAuth 2.0 Authorization URL for user consent.
20
- * @param {object} options
21
- * @param {string} options.clientId
22
- * @param {string} options.redirectUri
23
- * @param {string} [options.state]
24
- * @param {string[]} [options.scopes]
25
- * @param {'consent'|'select_account'|'none'} [options.prompt='consent']
26
- * @param {'offline'|'online'} [options.accessType='offline']
27
- * @returns {string}
28
- */
29
18
  export function getGoogleAuthUrl({
30
19
  clientId,
31
20
  redirectUri,
@@ -51,15 +40,6 @@ export function getGoogleAuthUrl({
51
40
  return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
52
41
  }
53
42
 
54
- /**
55
- * Exchange an OAuth authorization code for tokens (access_token, refresh_token).
56
- * @param {object} options
57
- * @param {string} options.code
58
- * @param {string} options.clientId
59
- * @param {string} options.clientSecret
60
- * @param {string} options.redirectUri
61
- * @returns {Promise<{ access_token: string, refresh_token?: string, expires_in: number, token_type: string, scope: string, expiry_date: number }>}
62
- */
63
43
  export async function exchangeCodeForTokens({
64
44
  code,
65
45
  clientId,
@@ -94,14 +74,6 @@ export async function exchangeCodeForTokens({
94
74
  return { ...data, expiry_date };
95
75
  }
96
76
 
97
- /**
98
- * Refresh an expired access token using a refresh token.
99
- * @param {object} options
100
- * @param {string} options.clientId
101
- * @param {string} options.clientSecret
102
- * @param {string} options.refreshToken
103
- * @returns {Promise<{ access_token: string, expires_in: number, expiry_date: number, token_type: string }>}
104
- */
105
77
  export async function refreshGoogleAccessToken({
106
78
  clientId,
107
79
  clientSecret,
@@ -134,14 +106,6 @@ export async function refreshGoogleAccessToken({
134
106
  return { ...data, expiry_date };
135
107
  }
136
108
 
137
- /**
138
- * Generate an OAuth2 access token from a Google Service Account using JWT signing.
139
- * @param {object} options
140
- * @param {string} options.clientEmail
141
- * @param {string} options.privateKey
142
- * @param {string[]} [options.scopes]
143
- * @returns {Promise<{ access_token: string, expires_in: number, expiry_date: number }>}
144
- */
145
109
  export async function getServiceAccountAccessToken({
146
110
  clientEmail,
147
111
  privateKey,