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
@@ -1,213 +1,311 @@
1
- /**
2
- * MBKBucket - Provider-Independent Storage Service
3
- * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
- * Licensed under the MIT License.
5
- */
6
-
7
- import dotenv from "dotenv";
8
- import { storageManager } from "../core/storage/storage-manager.js";
9
- import { PathStrategy } from "../core/storage/path-strategy.js";
10
- import { StorageValidationError } from "../core/storage/errors.js";
11
- import { createLogger } from "#logger";
12
- import { trimSlashes } from "#helpers";
13
-
14
- dotenv.config();
15
- const debugStorage = createLogger('storage-service');
16
-
17
- export const getAvailableStorageNames = () => storageManager.getAvailableConnectionNames();
18
- export const getAvailableBucketNames = getAvailableStorageNames;
19
-
20
- export function resolveStorageName(name) {
21
- return storageManager.resolveConnectionName(name);
22
- }
23
- export const resolveBucketName = resolveStorageName;
24
-
25
- export const getAppName = () => PathStrategy.getAppName();
26
- export const validateKeySafety = (str, isKey = false) => PathStrategy.validateSafety(str, isKey);
27
- export const ensureKeyHasAppPrefix = (key = '') => PathStrategy.applyKeyPrefix(key);
28
- export const ensurePrefix = (prefix = '') => PathStrategy.applyPrefix(prefix);
29
-
30
- export function getStorageProvider(connectionName) {
31
- return storageManager.getProvider(connectionName);
32
- }
33
-
34
- export function getStorageConfig(connectionName) {
35
- return storageManager.getConnectionConfig(connectionName);
36
- }
37
-
38
- // ---------------------------------------------------------------------------
39
- // Health Checks
40
- // ---------------------------------------------------------------------------
41
-
42
- export async function checkHealth(connectionName) {
43
- return storageManager.checkHealth(connectionName);
44
- }
45
-
46
- export async function runHealthCheck(connectionName) {
47
- try {
48
- const health = await checkHealth(connectionName);
49
- if (health.status === 'healthy') {
50
- debugStorage('Connected to storage: %s (%sms)', health.bucket || health.name || 'default', health.responseTime);
51
- } else {
52
- console.error('[mbkbucket] Storage connection failed:', health.error);
53
- }
54
- return health;
55
- } catch (err) {
56
- console.error("[mbkbucket] Storage connection test error:", err.message);
57
- }
58
- }
59
-
60
- // ---------------------------------------------------------------------------
61
- // Unified Storage Operations
62
- // ---------------------------------------------------------------------------
63
-
64
- export async function uploadFile(key, fileBuffer, contentType, options = {}) {
65
- if (!key || !fileBuffer) throw new StorageValidationError('Key and file buffer are required');
66
- const keyToUpload = ensureKeyHasAppPrefix(key);
67
- const provider = getStorageProvider(options.bucketName || options.connectionName);
68
- return provider.uploadFile(keyToUpload, fileBuffer, contentType, options);
69
- }
70
-
71
- export async function downloadFile(key, options = {}) {
72
- if (!key) throw new StorageValidationError('Key is required');
73
- const keyToDownload = ensureKeyHasAppPrefix(key);
74
- const provider = getStorageProvider(options.bucketName || options.connectionName);
75
- return provider.downloadFile(keyToDownload, options);
76
- }
77
-
78
- export async function deleteFile(key, connectionName) {
79
- if (!key) throw new StorageValidationError('Key is required');
80
- const keyToDelete = ensureKeyHasAppPrefix(key);
81
- const provider = getStorageProvider(connectionName);
82
- return provider.deleteFile(keyToDelete);
83
- }
84
-
85
- export async function deleteFiles(keys, connectionName) {
86
- if (!keys || !Array.isArray(keys) || !keys.length) {
87
- throw new StorageValidationError('Keys array is required and must not be empty');
88
- }
89
- const keysToDelete = keys.map(k => ensureKeyHasAppPrefix(k));
90
- const provider = getStorageProvider(connectionName);
91
- return provider.deleteFiles(keysToDelete);
92
- }
93
-
94
- export async function deleteFolder(prefix, connectionName) {
95
- if (!prefix) throw new StorageValidationError('Prefix is required');
96
- const effectivePrefix = `${ensurePrefix(trimSlashes(prefix))}/`;
97
- const provider = getStorageProvider(connectionName);
98
- return provider.deleteFolder(effectivePrefix);
99
- }
100
-
101
- export async function createFolder(prefix, connectionName, options = {}) {
102
- const effectivePrefix = ensurePrefix(prefix);
103
- const provider = getStorageProvider(connectionName);
104
- return provider.createFolder(effectivePrefix, options);
105
- }
106
-
107
- export async function listfiles(prefix = '', options = {}) {
108
- const effectivePrefix = ensurePrefix(prefix);
109
- const provider = getStorageProvider(options.bucketName || options.connectionName);
110
- return provider.listFiles(effectivePrefix, options);
111
- }
112
-
113
- export async function getFileMetadata(key, connectionName) {
114
- if (!key) throw new StorageValidationError('Key is required');
115
- const keyToCheck = ensureKeyHasAppPrefix(key);
116
- const provider = getStorageProvider(connectionName);
117
- return provider.getFileMetadata(keyToCheck);
118
- }
119
-
120
- export async function fileExists(key, connectionName) {
121
- try {
122
- const meta = await getFileMetadata(key, connectionName);
123
- return Boolean(meta?.exists);
124
- } catch {
125
- return false;
126
- }
127
- }
128
-
129
- export async function getFileSize(key, connectionName) {
130
- try {
131
- const meta = await getFileMetadata(key, connectionName);
132
- return meta?.exists ? (meta.ContentLength ?? null) : null;
133
- } catch {
134
- return null;
135
- }
136
- }
137
-
138
- export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, connectionName) {
139
- const keyToUse = ensureKeyHasAppPrefix(key);
140
- const provider = getStorageProvider(connectionName);
141
- return provider.generateSignedUrl(keyToUse, operation, expiresIn);
142
- }
143
-
144
- export async function copyFile(sourceKey, destKey, options = {}, connectionName) {
145
- const src = ensureKeyHasAppPrefix(sourceKey);
146
- const dst = ensureKeyHasAppPrefix(destKey);
147
- const provider = getStorageProvider(connectionName);
148
- return provider.copyFile(src, dst, options);
149
- }
150
-
151
- export async function moveFile(sourceKey, destKey, options = {}, connectionName) {
152
- const src = ensureKeyHasAppPrefix(sourceKey);
153
- const dst = ensureKeyHasAppPrefix(destKey);
154
- const provider = getStorageProvider(connectionName);
155
- return provider.moveFile(src, dst, options);
156
- }
157
-
158
- // ---------------------------------------------------------------------------
159
- // Multipart Upload Helpers (For S3 / compatible providers)
160
- // ---------------------------------------------------------------------------
161
-
162
- export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}, connectionName) {
163
- const keyToUse = ensureKeyHasAppPrefix(key);
164
- const provider = getStorageProvider(connectionName);
165
- if (typeof provider.createMultipartUpload !== 'function') {
166
- throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
167
- }
168
- return provider.createMultipartUpload(keyToUse, contentType, metadata);
169
- }
170
-
171
- export async function uploadPart(key, uploadId, partNumber, buffer, connectionName) {
172
- const keyToUse = ensureKeyHasAppPrefix(key);
173
- const provider = getStorageProvider(connectionName);
174
- if (typeof provider.uploadPart !== 'function') {
175
- throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
176
- }
177
- return provider.uploadPart(keyToUse, uploadId, partNumber, buffer);
178
- }
179
-
180
- export async function completeMultipartUpload(key, uploadId, parts, connectionName) {
181
- const keyToUse = ensureKeyHasAppPrefix(key);
182
- const provider = getStorageProvider(connectionName);
183
- if (typeof provider.completeMultipartUpload !== 'function') {
184
- throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
185
- }
186
- return provider.completeMultipartUpload(keyToUse, uploadId, parts);
187
- }
188
-
189
- export async function abortMultipartUpload(key, uploadId, connectionName) {
190
- const keyToUse = ensureKeyHasAppPrefix(key);
191
- const provider = getStorageProvider(connectionName);
192
- if (typeof provider.abortMultipartUpload !== 'function') {
193
- throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
194
- }
195
- return provider.abortMultipartUpload(keyToUse, uploadId);
196
- }
197
-
198
- export async function listIncompleteMultipartUploads(prefix = '', connectionName) {
199
- const effectivePrefix = ensurePrefix(prefix || '');
200
- const provider = getStorageProvider(connectionName);
201
- if (typeof provider.listIncompleteMultipartUploads !== 'function') {
202
- return [];
203
- }
204
- return provider.listIncompleteMultipartUploads(effectivePrefix);
205
- }
206
-
207
- export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefix = '', connectionName) {
208
- const provider = getStorageProvider(connectionName);
209
- if (typeof provider.cleanupIncompleteMultipartUploads !== 'function') {
210
- return { abortedCount: 0, uploads: [] };
211
- }
212
- return provider.cleanupIncompleteMultipartUploads(olderThanDays, prefix);
213
- }
1
+ /**
2
+ * MBKBucket - Provider-Independent Storage Service
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import dotenv from "dotenv";
8
+ import { storageManager } from "../core/storage/storage-manager.js";
9
+ import { PathStrategy } from "../core/storage/path-strategy.js";
10
+ import { StorageValidationError } from "../core/storage/errors.js";
11
+ import { metricsRegistry } from "../core/metrics.js";
12
+ import { createLogger } from "#logger";
13
+ import { trimSlashes } from "#helpers";
14
+
15
+ dotenv.config();
16
+ const debugStorage = createLogger('storage-service');
17
+
18
+ export const getAvailableStorageNames = () => storageManager.getAvailableConnectionNames();
19
+ export const getAvailableBucketNames = getAvailableStorageNames;
20
+
21
+ export function resolveStorageName(name) {
22
+ return storageManager.resolveConnectionName(name);
23
+ }
24
+ export const resolveBucketName = resolveStorageName;
25
+
26
+ export const getAppName = () => PathStrategy.getAppName();
27
+ export const validateKeySafety = (str, isKey = false) => PathStrategy.validateSafety(str, isKey);
28
+ export const ensureKeyHasAppPrefix = (key = '') => PathStrategy.applyKeyPrefix(key);
29
+ export const ensurePrefix = (prefix = '') => PathStrategy.applyPrefix(prefix);
30
+
31
+ export function getStorageProvider(connectionName) {
32
+ return storageManager.getProvider(connectionName);
33
+ }
34
+
35
+ export function getStorageConfig(connectionName) {
36
+ return storageManager.getConnectionConfig(connectionName);
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Health Checks
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export async function checkHealth(connectionName) {
44
+ return storageManager.checkHealth(connectionName);
45
+ }
46
+
47
+ export async function runHealthCheck(connectionName) {
48
+ try {
49
+ const health = await checkHealth(connectionName);
50
+ if (health.status === 'healthy') {
51
+ debugStorage('Connected to storage: %s (%sms)', health.bucket || health.name || 'default', health.responseTime);
52
+ } else {
53
+ console.error('[mbkbucket] Storage connection failed:', health.error);
54
+ }
55
+ return health;
56
+ } catch (err) {
57
+ console.error("[mbkbucket] Storage connection test error:", err.message);
58
+ }
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Unified Storage Operations
63
+ // ---------------------------------------------------------------------------
64
+
65
+ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
66
+ if (!key || !fileBuffer) throw new StorageValidationError('Key and file buffer are required');
67
+ const keyToUpload = ensureKeyHasAppPrefix(key);
68
+ const connName = options.bucketName || options.connectionName;
69
+ const provider = getStorageProvider(connName);
70
+ const start = Date.now();
71
+ try {
72
+ const result = await provider.uploadFile(keyToUpload, fileBuffer, contentType, options);
73
+ const duration = Date.now() - start;
74
+ const size = Buffer.isBuffer(fileBuffer) ? fileBuffer.length : (result.size || 0);
75
+ metricsRegistry.recordOperation('upload', { provider: provider.type, status: 'success', durationMs: duration, bytes: size });
76
+ storageManager.emit('file:uploaded', { key: keyToUpload, provider: provider.type, connectionName: connName, size, timestamp: new Date().toISOString() });
77
+ return result;
78
+ } catch (err) {
79
+ metricsRegistry.recordOperation('upload', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
80
+ metricsRegistry.recordError('upload', provider?.type || 'unknown', err.constructor?.name || 'Error');
81
+ throw err;
82
+ }
83
+ }
84
+
85
+ export async function downloadFile(key, options = {}) {
86
+ if (!key) throw new StorageValidationError('Key is required');
87
+ const keyToDownload = ensureKeyHasAppPrefix(key);
88
+ const connName = options.bucketName || options.connectionName;
89
+ const provider = getStorageProvider(connName);
90
+ const start = Date.now();
91
+ try {
92
+ const result = await provider.downloadFile(keyToDownload, options);
93
+ metricsRegistry.recordOperation('download', { provider: provider.type, status: 'success', durationMs: Date.now() - start, bytes: result.ContentLength || 0 });
94
+ return result;
95
+ } catch (err) {
96
+ metricsRegistry.recordOperation('download', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
97
+ metricsRegistry.recordError('download', provider?.type || 'unknown', err.constructor?.name || 'Error');
98
+ throw err;
99
+ }
100
+ }
101
+
102
+ export async function deleteFile(key, connectionName) {
103
+ if (!key) throw new StorageValidationError('Key is required');
104
+ const keyToDelete = ensureKeyHasAppPrefix(key);
105
+ const provider = getStorageProvider(connectionName);
106
+ const start = Date.now();
107
+ try {
108
+ const result = await provider.deleteFile(keyToDelete);
109
+ metricsRegistry.recordOperation('delete', { provider: provider.type, status: 'success', durationMs: Date.now() - start });
110
+ storageManager.emit('file:deleted', { key: keyToDelete, provider: provider.type, connectionName, timestamp: new Date().toISOString() });
111
+ return result;
112
+ } catch (err) {
113
+ metricsRegistry.recordOperation('delete', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
114
+ metricsRegistry.recordError('delete', provider?.type || 'unknown', err.constructor?.name || 'Error');
115
+ throw err;
116
+ }
117
+ }
118
+
119
+ export async function deleteFiles(keys, connectionName) {
120
+ if (!keys || !Array.isArray(keys) || !keys.length) {
121
+ throw new StorageValidationError('Keys array is required and must not be empty');
122
+ }
123
+ const keysToDelete = keys.map(k => ensureKeyHasAppPrefix(k));
124
+ const provider = getStorageProvider(connectionName);
125
+ const start = Date.now();
126
+ try {
127
+ const result = await provider.deleteFiles(keysToDelete);
128
+ metricsRegistry.recordOperation('deleteBatch', { provider: provider.type, status: 'success', durationMs: Date.now() - start });
129
+ storageManager.emit('files:deleted', { keys: keysToDelete, provider: provider.type, connectionName, timestamp: new Date().toISOString() });
130
+ return result;
131
+ } catch (err) {
132
+ metricsRegistry.recordOperation('deleteBatch', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
133
+ metricsRegistry.recordError('deleteBatch', provider?.type || 'unknown', err.constructor?.name || 'Error');
134
+ throw err;
135
+ }
136
+ }
137
+
138
+ export async function deleteFolder(prefix, connectionName) {
139
+ if (!prefix) throw new StorageValidationError('Prefix is required');
140
+ const effectivePrefix = `${ensurePrefix(trimSlashes(prefix))}/`;
141
+ const provider = getStorageProvider(connectionName);
142
+ const start = Date.now();
143
+ try {
144
+ const result = await provider.deleteFolder(effectivePrefix);
145
+ metricsRegistry.recordOperation('deleteFolder', { provider: provider.type, status: 'success', durationMs: Date.now() - start });
146
+ storageManager.emit('folder:deleted', { prefix: effectivePrefix, provider: provider.type, connectionName, timestamp: new Date().toISOString() });
147
+ return result;
148
+ } catch (err) {
149
+ metricsRegistry.recordOperation('deleteFolder', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
150
+ metricsRegistry.recordError('deleteFolder', provider?.type || 'unknown', err.constructor?.name || 'Error');
151
+ throw err;
152
+ }
153
+ }
154
+
155
+ export async function createFolder(prefix, connectionName, options = {}) {
156
+ const effectivePrefix = ensurePrefix(prefix);
157
+ const provider = getStorageProvider(connectionName);
158
+ const start = Date.now();
159
+ try {
160
+ const result = await provider.createFolder(effectivePrefix, options);
161
+ metricsRegistry.recordOperation('createFolder', { provider: provider.type, status: 'success', durationMs: Date.now() - start });
162
+ storageManager.emit('folder:created', { prefix: effectivePrefix, provider: provider.type, connectionName, timestamp: new Date().toISOString() });
163
+ return result;
164
+ } catch (err) {
165
+ metricsRegistry.recordOperation('createFolder', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
166
+ metricsRegistry.recordError('createFolder', provider?.type || 'unknown', err.constructor?.name || 'Error');
167
+ throw err;
168
+ }
169
+ }
170
+
171
+ export async function listfiles(prefix = '', options = {}) {
172
+ const effectivePrefix = ensurePrefix(prefix);
173
+ const connName = options.bucketName || options.connectionName;
174
+ const provider = getStorageProvider(connName);
175
+ const start = Date.now();
176
+ try {
177
+ const result = await provider.listFiles(effectivePrefix, options);
178
+ metricsRegistry.recordOperation('list', { provider: provider.type, status: 'success', durationMs: Date.now() - start });
179
+ return result;
180
+ } catch (err) {
181
+ metricsRegistry.recordOperation('list', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
182
+ metricsRegistry.recordError('list', provider?.type || 'unknown', err.constructor?.name || 'Error');
183
+ throw err;
184
+ }
185
+ }
186
+
187
+ export async function getFileMetadata(key, connectionName) {
188
+ if (!key) throw new StorageValidationError('Key is required');
189
+ const keyToCheck = ensureKeyHasAppPrefix(key);
190
+ const provider = getStorageProvider(connectionName);
191
+ const start = Date.now();
192
+ try {
193
+ const result = await provider.getFileMetadata(keyToCheck);
194
+ metricsRegistry.recordOperation('metadata', { provider: provider.type, status: 'success', durationMs: Date.now() - start });
195
+ return result;
196
+ } catch (err) {
197
+ metricsRegistry.recordOperation('metadata', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
198
+ metricsRegistry.recordError('metadata', provider?.type || 'unknown', err.constructor?.name || 'Error');
199
+ throw err;
200
+ }
201
+ }
202
+
203
+ export async function fileExists(key, connectionName) {
204
+ try {
205
+ const meta = await getFileMetadata(key, connectionName);
206
+ return Boolean(meta?.exists);
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ export async function getFileSize(key, connectionName) {
213
+ try {
214
+ const meta = await getFileMetadata(key, connectionName);
215
+ return meta?.exists ? (meta.ContentLength ?? null) : null;
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+
221
+ export async function copyFile(sourceKey, destKey, options = {}, connectionName) {
222
+ const src = ensureKeyHasAppPrefix(sourceKey);
223
+ const dst = ensureKeyHasAppPrefix(destKey);
224
+ const provider = getStorageProvider(connectionName);
225
+ const start = Date.now();
226
+ try {
227
+ const result = await provider.copyFile(src, dst, options);
228
+ metricsRegistry.recordOperation('copy', { provider: provider.type, status: 'success', durationMs: Date.now() - start });
229
+ storageManager.emit('file:copied', { sourceKey: src, destKey: dst, provider: provider.type, connectionName, timestamp: new Date().toISOString() });
230
+ return result;
231
+ } catch (err) {
232
+ metricsRegistry.recordOperation('copy', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
233
+ metricsRegistry.recordError('copy', provider?.type || 'unknown', err.constructor?.name || 'Error');
234
+ throw err;
235
+ }
236
+ }
237
+
238
+ export async function moveFile(sourceKey, destKey, options = {}, connectionName) {
239
+ const src = ensureKeyHasAppPrefix(sourceKey);
240
+ const dst = ensureKeyHasAppPrefix(destKey);
241
+ const provider = getStorageProvider(connectionName);
242
+ const start = Date.now();
243
+ try {
244
+ const result = await provider.moveFile(src, dst, options);
245
+ metricsRegistry.recordOperation('move', { provider: provider.type, status: 'success', durationMs: Date.now() - start });
246
+ storageManager.emit('file:moved', { sourceKey: src, destKey: dst, provider: provider.type, connectionName, timestamp: new Date().toISOString() });
247
+ return result;
248
+ } catch (err) {
249
+ metricsRegistry.recordOperation('move', { provider: provider?.type || 'unknown', status: 'error', durationMs: Date.now() - start });
250
+ metricsRegistry.recordError('move', provider?.type || 'unknown', err.constructor?.name || 'Error');
251
+ throw err;
252
+ }
253
+ }
254
+
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Multipart Upload Helpers (For S3 / compatible providers)
258
+ // ---------------------------------------------------------------------------
259
+
260
+ export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}, connectionName) {
261
+ const keyToUse = ensureKeyHasAppPrefix(key);
262
+ const provider = getStorageProvider(connectionName);
263
+ if (typeof provider.createMultipartUpload !== 'function') {
264
+ throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
265
+ }
266
+ return provider.createMultipartUpload(keyToUse, contentType, metadata);
267
+ }
268
+
269
+ export async function uploadPart(key, uploadId, partNumber, buffer, connectionName) {
270
+ const keyToUse = ensureKeyHasAppPrefix(key);
271
+ const provider = getStorageProvider(connectionName);
272
+ if (typeof provider.uploadPart !== 'function') {
273
+ throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
274
+ }
275
+ return provider.uploadPart(keyToUse, uploadId, partNumber, buffer);
276
+ }
277
+
278
+ export async function completeMultipartUpload(key, uploadId, parts, connectionName) {
279
+ const keyToUse = ensureKeyHasAppPrefix(key);
280
+ const provider = getStorageProvider(connectionName);
281
+ if (typeof provider.completeMultipartUpload !== 'function') {
282
+ throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
283
+ }
284
+ return provider.completeMultipartUpload(keyToUse, uploadId, parts);
285
+ }
286
+
287
+ export async function abortMultipartUpload(key, uploadId, connectionName) {
288
+ const keyToUse = ensureKeyHasAppPrefix(key);
289
+ const provider = getStorageProvider(connectionName);
290
+ if (typeof provider.abortMultipartUpload !== 'function') {
291
+ throw new Error(`Multipart upload is not supported by provider ${provider.type}`);
292
+ }
293
+ return provider.abortMultipartUpload(keyToUse, uploadId);
294
+ }
295
+
296
+ export async function listIncompleteMultipartUploads(prefix = '', connectionName) {
297
+ const effectivePrefix = ensurePrefix(prefix || '');
298
+ const provider = getStorageProvider(connectionName);
299
+ if (typeof provider.listIncompleteMultipartUploads !== 'function') {
300
+ return [];
301
+ }
302
+ return provider.listIncompleteMultipartUploads(effectivePrefix);
303
+ }
304
+
305
+ export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefix = '', connectionName) {
306
+ const provider = getStorageProvider(connectionName);
307
+ if (typeof provider.cleanupIncompleteMultipartUploads !== 'function') {
308
+ return { abortedCount: 0, uploads: [] };
309
+ }
310
+ return provider.cleanupIncompleteMultipartUploads(olderThanDays, prefix);
311
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * MBKBucket - Incomplete Multipart Upload Cleanup Scheduler
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import { cleanupIncompleteMultipartUploads, getAvailableStorageNames } from "../services/storage.service.js";
8
+ import { createLogger } from "#logger";
9
+
10
+ const debugCleanup = createLogger('cleanup-scheduler');
11
+
12
+ export class CleanupScheduler {
13
+ constructor() {
14
+ this._timer = null;
15
+ this._running = false;
16
+ this._lastRun = null;
17
+ this._lastStats = null;
18
+ }
19
+
20
+ async runOnce({ olderThanDays = 7, connectionNames = null } = {}) {
21
+ const targets = connectionNames && Array.isArray(connectionNames) && connectionNames.length > 0
22
+ ? connectionNames
23
+ : getAvailableStorageNames();
24
+
25
+ const summary = {
26
+ startedAt: new Date().toISOString(),
27
+ connectionsProcessed: 0,
28
+ totalAborted: 0,
29
+ errors: []
30
+ };
31
+
32
+ for (const name of targets) {
33
+ try {
34
+ const result = await cleanupIncompleteMultipartUploads(olderThanDays, '', name);
35
+ summary.connectionsProcessed++;
36
+ summary.totalAborted += (result.abortedCount || 0);
37
+ } catch (err) {
38
+ debugCleanup('Cleanup error for connection %s: %s', name, err.message);
39
+ summary.errors.push({ connection: name, error: err.message });
40
+ }
41
+ }
42
+
43
+ summary.finishedAt = new Date().toISOString();
44
+ this._lastRun = summary.finishedAt;
45
+ this._lastStats = summary;
46
+ debugCleanup('Cleanup cycle completed: %d uploads aborted across %d connections', summary.totalAborted, summary.connectionsProcessed);
47
+ return summary;
48
+ }
49
+
50
+ start({ intervalHours = 24, olderThanDays = 7, connectionNames = null } = {}) {
51
+ if (this._running) return;
52
+ this._running = true;
53
+
54
+ const intervalMs = Math.max(60000, Number(intervalHours) * 3600 * 1000);
55
+ debugCleanup('Starting cleanup scheduler every %s hour(s)', intervalHours);
56
+
57
+ // Run first pass asynchronously
58
+ this.runOnce({ olderThanDays, connectionNames }).catch(err => {
59
+ debugCleanup('Initial cleanup run error: %s', err.message);
60
+ });
61
+
62
+ this._timer = setInterval(() => {
63
+ this.runOnce({ olderThanDays, connectionNames }).catch(err => {
64
+ debugCleanup('Scheduled cleanup run error: %s', err.message);
65
+ });
66
+ }, intervalMs);
67
+
68
+ if (this._timer.unref) {
69
+ this._timer.unref();
70
+ }
71
+ }
72
+
73
+ stop() {
74
+ if (this._timer) {
75
+ clearInterval(this._timer);
76
+ this._timer = null;
77
+ }
78
+ this._running = false;
79
+ debugCleanup('Cleanup scheduler stopped');
80
+ }
81
+
82
+ getStatus() {
83
+ return {
84
+ running: this._running,
85
+ lastRun: this._lastRun,
86
+ lastStats: this._lastStats
87
+ };
88
+ }
89
+ }
90
+
91
+ export const cleanupScheduler = new CleanupScheduler();