mbkbucket 4.1.0 → 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.
@@ -1,223 +1,226 @@
1
- /**
2
- * MBKBucket - Storage Manager & Multi-Provider Connection Registry
3
- * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
- * Licensed under the MIT License.
5
- */
6
-
7
- import { EventEmitter } from "events";
8
- import dotenv from "dotenv";
9
- import { mbkautheVar } from "mbkauthe";
10
- import { S3StorageProvider } from "./s3-provider.js";
11
- import { GoogleDriveStorageProvider } from "./gdrive-provider.js";
12
- import { LocalStorageProvider } from "./local-provider.js";
13
- import { StorageConfigError, StorageValidationError } from "./errors.js";
14
- import { createLogger } from "#logger";
15
-
16
- dotenv.config();
17
- const debugStorage = createLogger('storage-manager');
18
-
19
- function parseJsonEnv(envVar, fallback = {}) {
20
- if (!envVar) return fallback;
21
- try {
22
- return JSON.parse(envVar);
23
- } catch (e) {
24
- console.error(`❌ Error parsing JSON for envVar: ${String(envVar).slice(0, 200)} - ${e.message}`);
25
- return fallback;
26
- }
27
- }
28
-
29
- export class StorageManager extends EventEmitter {
30
- constructor() {
31
- super();
32
- this._providers = new Map();
33
- this._factories = new Map();
34
- this._configs = new Map();
35
- this._defaultConnectionName = null;
36
-
37
- this.registerDriver('s3', (name, cfg) => new S3StorageProvider(name, cfg));
38
- this.registerDriver('r2', (name, cfg) => new S3StorageProvider(name, cfg));
39
- this.registerDriver('minio', (name, cfg) => new S3StorageProvider(name, cfg));
40
- this.registerDriver('idrive', (name, cfg) => new S3StorageProvider(name, cfg));
41
- this.registerDriver('gdrive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
42
- this.registerDriver('google-drive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
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));
47
-
48
- this.reloadFromEnv();
49
- }
50
-
51
-
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
- }
59
- this._factories.set(String(type).toLowerCase(), factory);
60
- }
61
-
62
- registerProviderFactory(type, factory) {
63
- this.registerDriver(type, factory);
64
- }
65
-
66
- hasDriver(type) {
67
- return this._factories.has(String(type).toLowerCase());
68
- }
69
-
70
- getDriverTypes() {
71
- return Array.from(this._factories.keys());
72
- }
73
-
74
- reloadFromEnv() {
75
- this._providers.clear();
76
- this._configs.clear();
77
-
78
- const envSource = process.env.StorageConnection || process.env.BucketConnection;
79
- const rawConfigs = parseJsonEnv(envSource, {});
80
-
81
- for (const [name, config] of Object.entries(rawConfigs)) {
82
- if (config && typeof config === 'object' && !Array.isArray(config)) {
83
- this.registerConnection(name, config);
84
- }
85
- }
86
- }
87
-
88
- registerConnection(name, config) {
89
- const cleanName = String(name || '').trim();
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
-
95
- this._configs.set(cleanName, Object.freeze({ ...config }));
96
- this._providers.delete(cleanName);
97
- }
98
-
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
-
109
- getAvailableConnectionNames() {
110
- return Array.from(this._configs.keys());
111
- }
112
-
113
- setDefaultConnectionName(name) {
114
- this._defaultConnectionName = name ? String(name).trim() : null;
115
- }
116
-
117
- getDefaultConnectionName() {
118
- if (this._defaultConnectionName && this._configs.has(this._defaultConnectionName)) {
119
- return this._defaultConnectionName;
120
- }
121
- const configuredDefault = mbkautheVar?.bucket;
122
- if (configuredDefault && this._configs.has(configuredDefault)) {
123
- return configuredDefault;
124
- }
125
- const available = this.getAvailableConnectionNames();
126
- return available.length > 0 ? available[0] : null;
127
- }
128
-
129
- resolveConnectionName(name) {
130
- const raw = typeof name === 'string' ? name.trim() : name;
131
- const candidate = !raw ? this.getDefaultConnectionName() : String(raw);
132
-
133
- const available = this.getAvailableConnectionNames();
134
- if (!candidate) {
135
- throw new StorageValidationError('No storage connection selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
136
- }
137
- if (available.length === 0) {
138
- throw new StorageConfigError('BucketConnection environment variable is not set or empty. Please configure it with your storage connections.');
139
- }
140
- if (!this._configs.has(candidate)) {
141
- throw new StorageValidationError(`Storage connection '${candidate}' not found in BucketConnection. Available connections: ${available.join(', ')}`);
142
- }
143
- return candidate;
144
- }
145
-
146
- getConnectionConfig(name) {
147
- const resolved = this.resolveConnectionName(name);
148
- return this._configs.get(resolved);
149
- }
150
-
151
- inferProviderType(config = {}) {
152
- if (config?.type) return String(config.type).toLowerCase();
153
- if (config?.provider) return String(config.provider).toLowerCase();
154
- if (config?.basePath || config?.rootPath || config?.directory) {
155
- return 'local';
156
- }
157
- if (config?.client_id || config?.refresh_token || config?.service_account_key || config?.folder_id || config?.service_account_email) {
158
- return 'gdrive';
159
- }
160
- return 's3';
161
- }
162
-
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
-
181
- getProvider(name) {
182
- const resolved = this.resolveConnectionName(name);
183
- if (this._providers.has(resolved)) {
184
- return this._providers.get(resolved);
185
- }
186
-
187
- const config = this.getConnectionConfig(resolved);
188
- const providerType = this.inferProviderType(config);
189
- const factory = this._factories.get(providerType);
190
-
191
- if (!factory) {
192
- throw new StorageConfigError(`Unsupported storage provider type '${providerType}' for connection '${resolved}'. Registered drivers: ${Array.from(this._factories.keys()).join(', ')}`);
193
- }
194
-
195
- debugStorage('Instantiating provider "%s" (type: %s)', resolved, providerType);
196
- const provider = factory(resolved, config);
197
- this._providers.set(resolved, provider);
198
- this.emit('provider:instantiated', { name: resolved, type: providerType });
199
- return provider;
200
- }
201
-
202
- async checkHealth(name) {
203
- try {
204
- const provider = this.getProvider(name);
205
- return await provider.checkHealth();
206
- } catch (error) {
207
- let bucket = 'unknown';
208
- try {
209
- const resolved = this.resolveConnectionName(name);
210
- bucket = this.getConnectionConfig(resolved)?.BUCKET_NAME || resolved;
211
- } catch {}
212
- return {
213
- status: 'unhealthy',
214
- error: error.message,
215
- bucket,
216
- checkedAt: new Date().toISOString()
217
- };
218
- }
219
- }
220
- }
221
-
222
- export const storageManager = new StorageManager();
223
-
1
+ /**
2
+ * MBKBucket - Storage Manager & Multi-Provider Connection Registry
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import { EventEmitter } from "events";
8
+ import dotenv from "dotenv";
9
+ import { mbkautheVar } from "mbkauthe";
10
+ import { S3StorageProvider } from "./s3-provider.js";
11
+ import { GoogleDriveStorageProvider } from "./gdrive-provider.js";
12
+ import { LocalStorageProvider } from "./local-provider.js";
13
+ import { StorageConfigError, StorageValidationError } from "./errors.js";
14
+ import { createLogger } from "#logger";
15
+
16
+ dotenv.config();
17
+ const debugStorage = createLogger('storage-manager');
18
+
19
+ function parseJsonEnv(envVar, fallback = {}) {
20
+ if (!envVar) return fallback;
21
+ try {
22
+ return JSON.parse(envVar);
23
+ } catch (e) {
24
+ console.error(`❌ Error parsing JSON for envVar: ${String(envVar).slice(0, 200)} - ${e.message}`);
25
+ return fallback;
26
+ }
27
+ }
28
+
29
+ export class StorageManager extends EventEmitter {
30
+ constructor() {
31
+ super();
32
+ this._providers = new Map();
33
+ this._factories = new Map();
34
+ this._configs = new Map();
35
+ this._defaultConnectionName = null;
36
+
37
+ this.registerDriver('s3', (name, cfg) => new S3StorageProvider(name, cfg));
38
+ this.registerDriver('r2', (name, cfg) => new S3StorageProvider(name, cfg));
39
+ this.registerDriver('minio', (name, cfg) => new S3StorageProvider(name, cfg));
40
+ this.registerDriver('idrive', (name, cfg) => new S3StorageProvider(name, cfg));
41
+ this.registerDriver('gdrive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
42
+ this.registerDriver('google-drive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
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));
47
+
48
+ this.reloadFromEnv();
49
+ }
50
+
51
+
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
+ }
59
+ this._factories.set(String(type).toLowerCase(), factory);
60
+ }
61
+
62
+ registerProviderFactory(type, factory) {
63
+ this.registerDriver(type, factory);
64
+ }
65
+
66
+ hasDriver(type) {
67
+ return this._factories.has(String(type).toLowerCase());
68
+ }
69
+
70
+ getDriverTypes() {
71
+ return Array.from(this._factories.keys());
72
+ }
73
+
74
+ reloadFromEnv() {
75
+ this._providers.clear();
76
+ this._configs.clear();
77
+
78
+ const envSource = process.env.StorageConnection || process.env.BucketConnection;
79
+ const rawConfigs = parseJsonEnv(envSource, {});
80
+
81
+ for (const [name, config] of Object.entries(rawConfigs)) {
82
+ if (config && typeof config === 'object' && !Array.isArray(config)) {
83
+ this.registerConnection(name, config);
84
+ }
85
+ }
86
+ }
87
+
88
+ registerConnection(name, config) {
89
+ const cleanName = String(name || '').trim();
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
+
95
+ this._configs.set(cleanName, Object.freeze({ ...config }));
96
+ this._providers.delete(cleanName);
97
+ }
98
+
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
+
109
+ getAvailableConnectionNames() {
110
+ return Array.from(this._configs.keys());
111
+ }
112
+
113
+ setDefaultConnectionName(name) {
114
+ this._defaultConnectionName = name ? String(name).trim() : null;
115
+ }
116
+
117
+ getDefaultConnectionName() {
118
+ if (this._defaultConnectionName && this._configs.has(this._defaultConnectionName)) {
119
+ return this._defaultConnectionName;
120
+ }
121
+ const configuredDefault = mbkautheVar?.bucket;
122
+ if (configuredDefault && this._configs.has(configuredDefault)) {
123
+ return configuredDefault;
124
+ }
125
+ const available = this.getAvailableConnectionNames();
126
+ return available.length > 0 ? available[0] : null;
127
+ }
128
+
129
+ resolveConnectionName(name) {
130
+ const raw = typeof name === 'string' ? name.trim() : name;
131
+ const candidate = !raw ? this.getDefaultConnectionName() : String(raw);
132
+
133
+ const available = this.getAvailableConnectionNames();
134
+ if (!candidate) {
135
+ if (available.length === 0) {
136
+ throw new StorageConfigError('BucketConnection environment variable is not set or empty. Please configure it with your storage connections.');
137
+ }
138
+ throw new StorageValidationError('No storage connection selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
139
+ }
140
+ if (!this._configs.has(candidate)) {
141
+ throw new StorageValidationError(`Storage connection '${candidate}' not found in BucketConnection. Available connections: ${available.join(', ') || 'none'}`);
142
+ }
143
+ if (available.length === 0) {
144
+ throw new StorageConfigError('BucketConnection environment variable is not set or empty. Please configure it with your storage connections.');
145
+ }
146
+ return candidate;
147
+ }
148
+
149
+ getConnectionConfig(name) {
150
+ const resolved = this.resolveConnectionName(name);
151
+ return this._configs.get(resolved);
152
+ }
153
+
154
+ inferProviderType(config = {}) {
155
+ if (config?.type) return String(config.type).toLowerCase();
156
+ if (config?.provider) return String(config.provider).toLowerCase();
157
+ if (config?.basePath || config?.rootPath || config?.directory) {
158
+ return 'local';
159
+ }
160
+ if (config?.client_id || config?.refresh_token || config?.service_account_key || config?.folder_id || config?.service_account_email) {
161
+ return 'gdrive';
162
+ }
163
+ return 's3';
164
+ }
165
+
166
+ clearCache() {
167
+ this._providers.clear();
168
+ }
169
+
170
+ async dispose() {
171
+ for (const [name, provider] of this._providers.entries()) {
172
+ try {
173
+ if (typeof provider.dispose === 'function') {
174
+ await provider.dispose();
175
+ }
176
+ } catch (err) {
177
+ debugStorage('Error disposing provider "%s": %s', name, err.message);
178
+ }
179
+ }
180
+ this._providers.clear();
181
+ this.removeAllListeners();
182
+ }
183
+
184
+ getProvider(name) {
185
+ const resolved = this.resolveConnectionName(name);
186
+ if (this._providers.has(resolved)) {
187
+ return this._providers.get(resolved);
188
+ }
189
+
190
+ const config = this.getConnectionConfig(resolved);
191
+ const providerType = this.inferProviderType(config);
192
+ const factory = this._factories.get(providerType);
193
+
194
+ if (!factory) {
195
+ throw new StorageConfigError(`Unsupported storage provider type '${providerType}' for connection '${resolved}'. Registered drivers: ${Array.from(this._factories.keys()).join(', ')}`);
196
+ }
197
+
198
+ debugStorage('Instantiating provider "%s" (type: %s)', resolved, providerType);
199
+ const provider = factory(resolved, config);
200
+ this._providers.set(resolved, provider);
201
+ this.emit('provider:instantiated', { name: resolved, type: providerType });
202
+ return provider;
203
+ }
204
+
205
+ async checkHealth(name) {
206
+ try {
207
+ const provider = this.getProvider(name);
208
+ return await provider.checkHealth();
209
+ } catch (error) {
210
+ let bucket = 'unknown';
211
+ try {
212
+ const resolved = this.resolveConnectionName(name);
213
+ bucket = this.getConnectionConfig(resolved)?.BUCKET_NAME || resolved;
214
+ } catch {}
215
+ return {
216
+ status: 'unhealthy',
217
+ error: error.message,
218
+ bucket,
219
+ checkedAt: new Date().toISOString()
220
+ };
221
+ }
222
+ }
223
+ }
224
+
225
+ export const storageManager = new StorageManager();
226
+