mbkbucket 4.0.0 → 4.1.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 (40) hide show
  1. package/.env.example +16 -15
  2. package/README.md +613 -561
  3. package/index.js +36 -33
  4. package/lib/cli.js +239 -239
  5. package/lib/public/bucketadmin.css +2042 -2042
  6. package/lib/public/bucketadmin.js +1844 -1622
  7. package/lib/public/mbkbucket-helper-demo.html +0 -58
  8. package/lib/public/mbkbucket-helper.js +1 -9
  9. package/lib/src/config/validation.js +91 -86
  10. package/lib/src/controllers/api.controller.js +486 -459
  11. package/lib/src/controllers/dashboard.controller.js +69 -69
  12. package/lib/src/controllers/info.controller.js +13 -13
  13. package/lib/src/controllers/view.controller.js +234 -233
  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 +101 -43
  19. package/lib/src/core/storage/index.js +4 -1
  20. package/lib/src/core/storage/local-provider.js +507 -0
  21. package/lib/src/core/storage/models.js +90 -50
  22. package/lib/src/core/storage/path-strategy.js +14 -1
  23. package/lib/src/core/storage/s3-provider.js +17 -37
  24. package/lib/src/core/storage/storage-manager.js +70 -50
  25. package/lib/src/core/storage/storage-provider.js +49 -120
  26. package/lib/src/middleware/bucket-resolver.js +37 -37
  27. package/lib/src/routes/api.routes.js +4 -1
  28. package/lib/src/routes/index.js +60 -60
  29. package/lib/src/routes/view.routes.js +28 -28
  30. package/lib/src/services/s3.service.js +42 -42
  31. package/lib/src/services/storage.service.js +311 -213
  32. package/lib/src/utils/cleanup-scheduler.js +91 -0
  33. package/lib/src/utils/errors.js +56 -56
  34. package/lib/src/utils/helpers.js +6 -6
  35. package/package.json +3 -5
  36. package/views/bucket.hbs +0 -5
  37. package/views/bucketadmin_move_modal.hbs +35 -35
  38. package/views/bucketadmin_preview_modal.hbs +108 -108
  39. package/views/bucketadmincontent.hbs +193 -193
  40. package/views/mbkbucket_info.hbs +134 -134
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, ListMultipartUploadsCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
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, StorageError } from './errors.js';
11
12
  import { createLogger } from "#logger";
@@ -14,10 +15,6 @@ import { nowIso, trimSlashes } from "#helpers";
14
15
  const debugS3 = createLogger('s3-provider');
15
16
 
16
17
  export class S3StorageProvider extends StorageProvider {
17
- /**
18
- * @param {string} name - Connection name
19
- * @param {object} config - S3 configuration (BUCKET_NAME, ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, region)
20
- */
21
18
  constructor(name, config = {}) {
22
19
  super(name, config);
23
20
  this.bucket = config.BUCKET_NAME;
@@ -49,16 +46,19 @@ export class S3StorageProvider extends StorageProvider {
49
46
  }
50
47
 
51
48
  get capabilities() {
52
- return {
49
+ return new StorageCapabilities({
53
50
  multipart: true,
54
- presignedUrls: true,
51
+ resumable: true,
55
52
  nativeFolders: false,
56
53
  ranges: true,
57
54
  copy: true,
58
55
  move: true,
59
56
  search: false,
60
- directDownloadUrl: false
61
- };
57
+ directDownloadUrl: false,
58
+ versioning: false,
59
+ publicUrls: false,
60
+ metadata: true
61
+ });
62
62
  }
63
63
 
64
64
  async checkHealth() {
@@ -279,7 +279,8 @@ export class S3StorageProvider extends StorageProvider {
279
279
  size: item.Size,
280
280
  lastModified: item.LastModified,
281
281
  etag: item.ETag,
282
- storageClass: item.StorageClass || 'STANDARD'
282
+ storageClass: item.StorageClass || 'STANDARD',
283
+ provider: 's3'
283
284
  }));
284
285
 
285
286
  const commonPrefixes = (result.CommonPrefixes || []).map(p => new StorageFolder(p.Prefix));
@@ -340,6 +341,7 @@ export class S3StorageProvider extends StorageProvider {
340
341
  }
341
342
 
342
343
  async copyFile(sourceKey, destKey, options = {}) {
344
+ this.assertCapability('copy', 'copyFile');
343
345
  try {
344
346
  const copySource = encodeURIComponent(`${this.bucket}/${sourceKey}`);
345
347
  await this.client.send(new CopyObjectCommand({
@@ -357,39 +359,12 @@ export class S3StorageProvider extends StorageProvider {
357
359
  }
358
360
  }
359
361
 
360
- async generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, _options = {}) {
361
- try {
362
- const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
363
- const command = operation === 'getObject'
364
- ? new GetObjectCommand({ Bucket: this.bucket, Key: key })
365
- : operation === 'putObject'
366
- ? new PutObjectCommand({ Bucket: this.bucket, Key: key })
367
- : null;
368
-
369
- if (!command) throw new StorageValidationError(`Unsupported operation: ${operation}`);
370
-
371
- const signedUrl = await getSignedUrl(this.client, command, { expiresIn });
372
- const now = Date.now();
373
- return {
374
- url: signedUrl,
375
- key,
376
- operation,
377
- expiresIn,
378
- expiresAt: new Date(now + expiresIn * 1000).toISOString(),
379
- generatedAt: new Date(now).toISOString()
380
- };
381
- } catch (error) {
382
- debugS3('Generate signed URL failed for key %s: %s', key, error.message);
383
- if (error instanceof StorageError) throw error;
384
- throw new StorageError(`Generate signed URL failed: ${error.message}`);
385
- }
386
- }
387
-
388
362
  // ---------------------------------------------------------------------------
389
363
  // S3 Multipart Upload Operations
390
364
  // ---------------------------------------------------------------------------
391
365
 
392
366
  async createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}) {
367
+ this.assertCapability('multipart', 'createMultipartUpload');
393
368
  const result = await this.client.send(new CreateMultipartUploadCommand({
394
369
  Bucket: this.bucket,
395
370
  Key: key,
@@ -404,6 +379,7 @@ export class S3StorageProvider extends StorageProvider {
404
379
  }
405
380
 
406
381
  async uploadPart(key, uploadId, partNumber, buffer) {
382
+ this.assertCapability('multipart', 'uploadPart');
407
383
  const result = await this.client.send(new UploadPartCommand({
408
384
  Bucket: this.bucket,
409
385
  Key: key,
@@ -416,6 +392,7 @@ export class S3StorageProvider extends StorageProvider {
416
392
  }
417
393
 
418
394
  async completeMultipartUpload(key, uploadId, parts) {
395
+ this.assertCapability('multipart', 'completeMultipartUpload');
419
396
  const sorted = [...parts].sort((a, b) => a.partNumber - b.partNumber);
420
397
  await this.client.send(new CompleteMultipartUploadCommand({
421
398
  Bucket: this.bucket,
@@ -429,6 +406,7 @@ export class S3StorageProvider extends StorageProvider {
429
406
  }
430
407
 
431
408
  async abortMultipartUpload(key, uploadId) {
409
+ this.assertCapability('multipart', 'abortMultipartUpload');
432
410
  await this.client.send(new AbortMultipartUploadCommand({
433
411
  Bucket: this.bucket,
434
412
  Key: key,
@@ -438,6 +416,7 @@ export class S3StorageProvider extends StorageProvider {
438
416
  }
439
417
 
440
418
  async listIncompleteMultipartUploads(prefix = '') {
419
+ this.assertCapability('multipart', 'listIncompleteMultipartUploads');
441
420
  try {
442
421
  const result = await this.client.send(new ListMultipartUploadsCommand({
443
422
  Bucket: this.bucket,
@@ -452,6 +431,7 @@ export class S3StorageProvider extends StorageProvider {
452
431
  }
453
432
 
454
433
  async cleanupIncompleteMultipartUploads(olderThanDays = 7, prefix = '') {
434
+ this.assertCapability('multipart', 'cleanupIncompleteMultipartUploads');
455
435
  try {
456
436
  const uploads = await this.listIncompleteMultipartUploads(prefix);
457
437
  if (!uploads.length) {
@@ -1,13 +1,15 @@
1
1
  /**
2
- * MBKBucket - Storage Manager & Provider Registry
2
+ * MBKBucket - Storage Manager & Multi-Provider Connection Registry
3
3
  * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
4
  * Licensed under the MIT License.
5
5
  */
6
6
 
7
+ import { EventEmitter } from "events";
7
8
  import dotenv from "dotenv";
8
9
  import { mbkautheVar } from "mbkauthe";
9
10
  import { S3StorageProvider } from "./s3-provider.js";
10
11
  import { GoogleDriveStorageProvider } from "./gdrive-provider.js";
12
+ import { LocalStorageProvider } from "./local-provider.js";
11
13
  import { StorageConfigError, StorageValidationError } from "./errors.js";
12
14
  import { createLogger } from "#logger";
13
15
 
@@ -24,29 +26,36 @@ function parseJsonEnv(envVar, fallback = {}) {
24
26
  }
25
27
  }
26
28
 
27
- export class StorageManager {
29
+ export class StorageManager extends EventEmitter {
28
30
  constructor() {
31
+ super();
29
32
  this._providers = new Map();
30
33
  this._factories = new Map();
31
34
  this._configs = new Map();
35
+ this._defaultConnectionName = null;
32
36
 
33
- // Register built-in provider drivers
34
37
  this.registerDriver('s3', (name, cfg) => new S3StorageProvider(name, cfg));
35
38
  this.registerDriver('r2', (name, cfg) => new S3StorageProvider(name, cfg));
36
39
  this.registerDriver('minio', (name, cfg) => new S3StorageProvider(name, cfg));
40
+ this.registerDriver('idrive', (name, cfg) => new S3StorageProvider(name, cfg));
37
41
  this.registerDriver('gdrive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
38
42
  this.registerDriver('google-drive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
39
43
  this.registerDriver('googledrive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
44
+ this.registerDriver('local', (name, cfg) => new LocalStorageProvider(name, cfg));
45
+ this.registerDriver('fs', (name, cfg) => new LocalStorageProvider(name, cfg));
46
+ this.registerDriver('filesystem', (name, cfg) => new LocalStorageProvider(name, cfg));
40
47
 
41
48
  this.reloadFromEnv();
42
49
  }
43
50
 
44
- /**
45
- * Register a custom provider driver.
46
- * @param {string} type
47
- * @param {(name: string, config: object) => import('./storage-provider.js').StorageProvider} factory
48
- */
51
+
49
52
  registerDriver(type, factory) {
53
+ if (!type || typeof type !== 'string') {
54
+ throw new StorageValidationError("Driver type must be a non-empty string");
55
+ }
56
+ if (typeof factory !== 'function') {
57
+ throw new StorageValidationError("Driver factory must be a function");
58
+ }
50
59
  this._factories.set(String(type).toLowerCase(), factory);
51
60
  }
52
61
 
@@ -54,9 +63,14 @@ export class StorageManager {
54
63
  this.registerDriver(type, factory);
55
64
  }
56
65
 
57
- /**
58
- * Reload all connections from environment variables.
59
- */
66
+ hasDriver(type) {
67
+ return this._factories.has(String(type).toLowerCase());
68
+ }
69
+
70
+ getDriverTypes() {
71
+ return Array.from(this._factories.keys());
72
+ }
73
+
60
74
  reloadFromEnv() {
61
75
  this._providers.clear();
62
76
  this._configs.clear();
@@ -71,31 +85,39 @@ export class StorageManager {
71
85
  }
72
86
  }
73
87
 
74
- /**
75
- * Register a named storage connection.
76
- * @param {string} name
77
- * @param {object} config
78
- */
79
88
  registerConnection(name, config) {
80
- const cleanName = String(name).trim();
89
+ const cleanName = String(name || '').trim();
81
90
  if (!cleanName) throw new StorageValidationError("Connection name cannot be empty");
91
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
92
+ throw new StorageValidationError(`Connection configuration for '${cleanName}' must be an object`);
93
+ }
94
+
82
95
  this._configs.set(cleanName, Object.freeze({ ...config }));
83
96
  this._providers.delete(cleanName);
84
97
  }
85
98
 
86
- /**
87
- * Get all registered connection names.
88
- * @returns {string[]}
89
- */
99
+ unregisterConnection(name) {
100
+ const cleanName = String(name || '').trim();
101
+ this._configs.delete(cleanName);
102
+ this._providers.delete(cleanName);
103
+ }
104
+
105
+ hasConnection(name) {
106
+ return this._configs.has(String(name || '').trim());
107
+ }
108
+
90
109
  getAvailableConnectionNames() {
91
110
  return Array.from(this._configs.keys());
92
111
  }
93
112
 
94
- /**
95
- * Get the default connection name.
96
- * @returns {string|null}
97
- */
113
+ setDefaultConnectionName(name) {
114
+ this._defaultConnectionName = name ? String(name).trim() : null;
115
+ }
116
+
98
117
  getDefaultConnectionName() {
118
+ if (this._defaultConnectionName && this._configs.has(this._defaultConnectionName)) {
119
+ return this._defaultConnectionName;
120
+ }
99
121
  const configuredDefault = mbkautheVar?.bucket;
100
122
  if (configuredDefault && this._configs.has(configuredDefault)) {
101
123
  return configuredDefault;
@@ -104,11 +126,6 @@ export class StorageManager {
104
126
  return available.length > 0 ? available[0] : null;
105
127
  }
106
128
 
107
- /**
108
- * Resolve a candidate connection name or fall back to default.
109
- * @param {string} [name]
110
- * @returns {string}
111
- */
112
129
  resolveConnectionName(name) {
113
130
  const raw = typeof name === 'string' ? name.trim() : name;
114
131
  const candidate = !raw ? this.getDefaultConnectionName() : String(raw);
@@ -126,35 +143,41 @@ export class StorageManager {
126
143
  return candidate;
127
144
  }
128
145
 
129
- /**
130
- * Get connection configuration object.
131
- * @param {string} [name]
132
- * @returns {object}
133
- */
134
146
  getConnectionConfig(name) {
135
147
  const resolved = this.resolveConnectionName(name);
136
148
  return this._configs.get(resolved);
137
149
  }
138
150
 
139
- /**
140
- * Determine the provider type from configuration.
141
- * @param {object} config
142
- * @returns {string}
143
- */
144
151
  inferProviderType(config = {}) {
145
152
  if (config?.type) return String(config.type).toLowerCase();
146
153
  if (config?.provider) return String(config.provider).toLowerCase();
154
+ if (config?.basePath || config?.rootPath || config?.directory) {
155
+ return 'local';
156
+ }
147
157
  if (config?.client_id || config?.refresh_token || config?.service_account_key || config?.folder_id || config?.service_account_email) {
148
158
  return 'gdrive';
149
159
  }
150
160
  return 's3';
151
161
  }
152
162
 
153
- /**
154
- * Get or instantiate the StorageProvider for a connection.
155
- * @param {string} [name]
156
- * @returns {import('./storage-provider.js').StorageProvider}
157
- */
163
+ clearCache() {
164
+ this._providers.clear();
165
+ }
166
+
167
+ async dispose() {
168
+ for (const [name, provider] of this._providers.entries()) {
169
+ try {
170
+ if (typeof provider.dispose === 'function') {
171
+ await provider.dispose();
172
+ }
173
+ } catch (err) {
174
+ debugStorage('Error disposing provider "%s": %s', name, err.message);
175
+ }
176
+ }
177
+ this._providers.clear();
178
+ this.removeAllListeners();
179
+ }
180
+
158
181
  getProvider(name) {
159
182
  const resolved = this.resolveConnectionName(name);
160
183
  if (this._providers.has(resolved)) {
@@ -172,14 +195,10 @@ export class StorageManager {
172
195
  debugStorage('Instantiating provider "%s" (type: %s)', resolved, providerType);
173
196
  const provider = factory(resolved, config);
174
197
  this._providers.set(resolved, provider);
198
+ this.emit('provider:instantiated', { name: resolved, type: providerType });
175
199
  return provider;
176
200
  }
177
201
 
178
- /**
179
- * Check health of the default or specified connection.
180
- * @param {string} [name]
181
- * @returns {Promise<object>}
182
- */
183
202
  async checkHealth(name) {
184
203
  try {
185
204
  const provider = this.getProvider(name);
@@ -201,3 +220,4 @@ export class StorageManager {
201
220
  }
202
221
 
203
222
  export const storageManager = new StorageManager();
223
+
@@ -5,148 +5,98 @@
5
5
  */
6
6
 
7
7
  import { StorageUnsupportedOperationError } from "./errors.js";
8
+ import { StorageCapabilities } from "./capabilities.js";
8
9
 
9
- /**
10
- * Abstract base class for storage providers (S3, Cloudflare R2, Google Drive, Azure, etc.)
11
- */
12
10
  export class StorageProvider {
13
- /**
14
- * @param {string} name - Connection/Bucket name identifier
15
- * @param {object} config - Provider-specific configuration
16
- */
17
11
  constructor(name, config = {}) {
18
12
  if (new.target === StorageProvider) {
19
13
  throw new TypeError("Cannot construct StorageProvider instances directly");
20
14
  }
21
- this.name = name;
15
+ this.name = String(name);
22
16
  this.config = Object.freeze({ ...config });
17
+ this.rootPrefix = config.rootPrefix || config.basePath || '';
23
18
  }
24
19
 
25
- /**
26
- * Provider type identifier (e.g. 's3', 'gdrive', 'r2')
27
- * @returns {string}
28
- */
29
20
  get type() {
30
21
  return 'generic';
31
22
  }
32
23
 
33
- /**
34
- * Explicit provider capabilities.
35
- * @returns {object}
36
- */
37
24
  get capabilities() {
38
- return {
25
+ return new StorageCapabilities({
39
26
  multipart: false,
40
- presignedUrls: false,
41
27
  nativeFolders: false,
42
28
  ranges: true,
43
29
  copy: false,
44
30
  move: false,
45
31
  search: false,
46
- directDownloadUrl: false
47
- };
32
+ directDownloadUrl: false,
33
+ versioning: false,
34
+ publicUrls: false,
35
+ metadata: true
36
+ });
37
+ }
38
+
39
+ supports(capabilityName) {
40
+ return this.capabilities.has(capabilityName);
41
+ }
42
+
43
+ assertCapability(capabilityName, operationName) {
44
+ this.capabilities.assert(capabilityName, operationName, `provider '${this.type}'`);
48
45
  }
49
46
 
50
- /**
51
- * List files and folders at the given prefix/path.
52
- * @param {string} [prefix='']
53
- * @param {object} [options={}]
54
- * @returns {Promise<import('./models.js').StorageListResult>}
55
- */
56
47
  async listFiles(prefix = '', options = {}) {
57
48
  throw new StorageUnsupportedOperationError(`listFiles() not implemented by ${this.constructor.name}`);
58
49
  }
59
50
 
60
- /**
61
- * Upload a file.
62
- * @param {string} key
63
- * @param {Buffer|Uint8Array|import('node:stream').Readable} fileBuffer
64
- * @param {string} contentType
65
- * @param {object} [options={}]
66
- * @returns {Promise<{ key: string, fileSize: number, contentType: string, uploadedAt: string, [key: string]: any }>}
67
- */
68
51
  async uploadFile(key, fileBuffer, contentType, options = {}) {
69
52
  throw new StorageUnsupportedOperationError(`uploadFile() not implemented by ${this.constructor.name}`);
70
53
  }
71
54
 
72
- /**
73
- * Download a file.
74
- * @param {string} key
75
- * @param {object} [options={}]
76
- * @returns {Promise<{ Body: import('node:stream').Readable, ContentType?: string, ContentLength?: number, ETag?: string, LastModified?: Date, notModified?: boolean, key: string }>}
77
- */
78
55
  async downloadFile(key, options = {}) {
79
56
  throw new StorageUnsupportedOperationError(`downloadFile() not implemented by ${this.constructor.name}`);
80
57
  }
81
58
 
82
- /**
83
- * Delete a single file.
84
- * @param {string} key
85
- * @param {object} [options={}]
86
- * @returns {Promise<{ key: string, deletedAt: string, [key: string]: any }>}
87
- */
88
59
  async deleteFile(key, options = {}) {
89
60
  throw new StorageUnsupportedOperationError(`deleteFile() not implemented by ${this.constructor.name}`);
90
61
  }
91
62
 
92
- /**
93
- * Delete multiple files.
94
- * @param {string[]} keys
95
- * @param {object} [options={}]
96
- * @returns {Promise<{ results: any[], deletedCount: number, errors: any[], deletedAt: string }>}
97
- */
98
63
  async deleteFiles(keys, options = {}) {
64
+ const concurrency = Math.max(1, Math.min(Number(options.concurrency) || 8, 32));
99
65
  let deletedCount = 0;
100
66
  const errors = [];
101
67
  const results = [];
102
- for (const key of keys) {
103
- try {
104
- const res = await this.deleteFile(key, options);
105
- results.push(res);
106
- deletedCount++;
107
- } catch (err) {
108
- errors.push({ key, error: err.message });
109
- }
68
+
69
+ for (let i = 0; i < keys.length; i += concurrency) {
70
+ const batch = keys.slice(i, i + concurrency);
71
+ const batchResults = await Promise.allSettled(
72
+ batch.map(key => this.deleteFile(key, options))
73
+ );
74
+ batchResults.forEach((outcome, idx) => {
75
+ const key = batch[idx];
76
+ if (outcome.status === 'fulfilled') {
77
+ results.push(outcome.value);
78
+ deletedCount++;
79
+ } else {
80
+ errors.push({ key, error: outcome.reason?.message || 'Delete failed' });
81
+ }
82
+ });
110
83
  }
84
+
111
85
  return { results, deletedCount, errors, deletedAt: new Date().toISOString() };
112
86
  }
113
87
 
114
- /**
115
- * Delete a folder and its contents.
116
- * @param {string} prefix
117
- * @param {object} [options={}]
118
- * @returns {Promise<{ deletedCount: number, prefix: string, deletedAt: string }>}
119
- */
120
88
  async deleteFolder(prefix, options = {}) {
121
89
  throw new StorageUnsupportedOperationError(`deleteFolder() not implemented by ${this.constructor.name}`);
122
90
  }
123
91
 
124
- /**
125
- * Create a folder.
126
- * @param {string} key
127
- * @param {object} [options={}]
128
- * @returns {Promise<{ key: string, success: boolean, [key: string]: any }>}
129
- */
130
92
  async createFolder(key, options = {}) {
131
93
  throw new StorageUnsupportedOperationError(`createFolder() not implemented by ${this.constructor.name}`);
132
94
  }
133
95
 
134
- /**
135
- * Get file metadata.
136
- * @param {string} key
137
- * @param {object} [options={}]
138
- * @returns {Promise<{ key: string, exists: boolean, ContentLength?: number, ContentType?: string, LastModified?: Date, ETag?: string, Metadata?: object, queriedAt: string }>}
139
- */
140
96
  async getFileMetadata(key, options = {}) {
141
97
  throw new StorageUnsupportedOperationError(`getFileMetadata() not implemented by ${this.constructor.name}`);
142
98
  }
143
99
 
144
- /**
145
- * Check if a file exists.
146
- * @param {string} key
147
- * @param {object} [options={}]
148
- * @returns {Promise<boolean>}
149
- */
150
100
  async fileExists(key, options = {}) {
151
101
  try {
152
102
  const meta = await this.getFileMetadata(key, options);
@@ -156,12 +106,6 @@ export class StorageProvider {
156
106
  }
157
107
  }
158
108
 
159
- /**
160
- * Get file size in bytes.
161
- * @param {string} key
162
- * @param {object} [options={}]
163
- * @returns {Promise<number|null>}
164
- */
165
109
  async getFileSize(key, options = {}) {
166
110
  try {
167
111
  const meta = await this.getFileMetadata(key, options);
@@ -171,47 +115,32 @@ export class StorageProvider {
171
115
  }
172
116
  }
173
117
 
174
- /**
175
- * Generate a signed or temporary access URL if supported.
176
- * @param {string} key
177
- * @param {'getObject'|'putObject'} [operation='getObject']
178
- * @param {number} [expiresIn=3600]
179
- * @param {object} [options={}]
180
- * @returns {Promise<{ url: string, key: string, operation: string, expiresIn: number, expiresAt: string, generatedAt: string }>}
181
- */
182
- async generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, options = {}) {
183
- throw new StorageUnsupportedOperationError(`generateSignedUrl() is not supported by provider ${this.type}`);
184
- }
185
-
186
- /**
187
- * Copy a file.
188
- * @param {string} sourceKey
189
- * @param {string} destKey
190
- * @param {object} [options={}]
191
- * @returns {Promise<{ sourceKey: string, destKey: string, copiedAt: string }>}
192
- */
193
118
  async copyFile(sourceKey, destKey, options = {}) {
119
+ this.assertCapability('copy', 'copyFile');
194
120
  throw new StorageUnsupportedOperationError(`copyFile() is not supported by provider ${this.type}`);
195
121
  }
196
122
 
197
- /**
198
- * Move a file.
199
- * @param {string} sourceKey
200
- * @param {string} destKey
201
- * @param {object} [options={}]
202
- * @returns {Promise<{ sourceKey: string, destKey: string, movedAt: string }>}
203
- */
204
123
  async moveFile(sourceKey, destKey, options = {}) {
124
+ this.assertCapability('move', 'moveFile');
205
125
  await this.copyFile(sourceKey, destKey, options);
206
126
  await this.deleteFile(sourceKey, options);
207
127
  return { sourceKey, destKey, movedAt: new Date().toISOString() };
208
128
  }
209
129
 
210
- /**
211
- * Run health check.
212
- * @returns {Promise<{ status: 'healthy'|'unhealthy', responseTime?: number, error?: string, [key: string]: any }>}
213
- */
130
+ async initialize() {
131
+ return this;
132
+ }
133
+
134
+ async dispose() {
135
+ return this;
136
+ }
137
+
138
+ async ping() {
139
+ return this.checkHealth();
140
+ }
141
+
214
142
  async checkHealth() {
215
143
  throw new StorageUnsupportedOperationError(`checkHealth() not implemented by ${this.constructor.name}`);
216
144
  }
217
145
  }
146
+
@@ -1,37 +1,37 @@
1
- import { resolveStorageName } from "../services/storage.service.js";
2
- import { sendApiError, sendViewError } from "../utils/errors.js";
3
-
4
- const NO_BUCKET_MSG = 'No storage connection selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.';
5
-
6
- /**
7
- * Resolves `?bucket=<name>` query param and attaches `req.activeBucket` and `req.bucketResolveError`.
8
- */
9
- export function bucketResolver(req, _res, next) {
10
- try {
11
- req.activeBucket = resolveStorageName(req.query.bucket);
12
- req.bucketResolveError = null;
13
- } catch (err) {
14
- req.activeBucket = null;
15
- req.bucketResolveError = err;
16
- }
17
- next();
18
- }
19
-
20
- /**
21
- * Guard middleware for API routes — returns 400/503 JSON if bucket is invalid.
22
- */
23
- export function requireBucketApi(req, res, next) {
24
- if (req.bucketResolveError) return sendApiError(res, req.bucketResolveError, 'Invalid storage connection');
25
- if (!req.activeBucket) return sendApiError(res, new Error(NO_BUCKET_MSG));
26
- next();
27
- }
28
-
29
- /**
30
- * Guard middleware for view routes — returns 400/503 JSON if bucket is invalid (skips /p_view/...).
31
- */
32
- export function requireBucketView(req, res, next) {
33
- if (req.path.includes('/p_view/') || req.url.startsWith('/p_view/') || req.originalUrl?.includes('/p_view/')) return next();
34
- if (req.bucketResolveError) return sendViewError(res, req.bucketResolveError, 'Invalid storage connection');
35
- if (!req.activeBucket) return sendViewError(res, new Error(NO_BUCKET_MSG));
36
- next();
37
- }
1
+ import { resolveStorageName } from "../services/storage.service.js";
2
+ import { sendApiError, sendViewError } from "../utils/errors.js";
3
+
4
+ const NO_BUCKET_MSG = 'No storage connection selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.';
5
+
6
+ /**
7
+ * Resolves `?bucket=<name>` query param and attaches `req.activeBucket` and `req.bucketResolveError`.
8
+ */
9
+ export function bucketResolver(req, _res, next) {
10
+ try {
11
+ req.activeBucket = resolveStorageName(req.query.bucket);
12
+ req.bucketResolveError = null;
13
+ } catch (err) {
14
+ req.activeBucket = null;
15
+ req.bucketResolveError = err;
16
+ }
17
+ next();
18
+ }
19
+
20
+ /**
21
+ * Guard middleware for API routes — returns 400/503 JSON if bucket is invalid.
22
+ */
23
+ export function requireBucketApi(req, res, next) {
24
+ if (req.bucketResolveError) return sendApiError(res, req.bucketResolveError, 'Invalid storage connection');
25
+ if (!req.activeBucket) return sendApiError(res, new Error(NO_BUCKET_MSG));
26
+ next();
27
+ }
28
+
29
+ /**
30
+ * Guard middleware for view routes — returns 400/503 JSON if bucket is invalid (skips /p_view/...).
31
+ */
32
+ export function requireBucketView(req, res, next) {
33
+ if (req.path.includes('/p_view/') || req.url.startsWith('/p_view/') || req.originalUrl?.includes('/p_view/')) return next();
34
+ if (req.bucketResolveError) return sendViewError(res, req.bucketResolveError, 'Invalid storage connection');
35
+ if (!req.activeBucket) return sendViewError(res, new Error(NO_BUCKET_MSG));
36
+ next();
37
+ }