mbkbucket 4.0.1 → 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.
- package/.env.example +3 -2
- package/README.md +96 -44
- package/index.js +4 -1
- package/lib/public/bucketadmin.js +255 -33
- package/lib/public/mbkbucket-helper-demo.html +0 -58
- package/lib/public/mbkbucket-helper.js +1 -9
- package/lib/src/config/validation.js +6 -1
- package/lib/src/controllers/api.controller.js +32 -5
- package/lib/src/controllers/info.controller.js +13 -13
- package/lib/src/controllers/view.controller.js +22 -21
- package/lib/src/core/metrics.js +134 -0
- package/lib/src/core/storage/capabilities.js +58 -0
- package/lib/src/core/storage/errors.js +57 -62
- package/lib/src/core/storage/gdrive-auth.js +157 -193
- package/lib/src/core/storage/gdrive-provider.js +714 -656
- package/lib/src/core/storage/index.js +17 -14
- package/lib/src/core/storage/local-provider.js +507 -0
- package/lib/src/core/storage/models.js +120 -80
- package/lib/src/core/storage/path-strategy.js +78 -65
- package/lib/src/core/storage/s3-provider.js +471 -491
- package/lib/src/core/storage/storage-manager.js +223 -203
- package/lib/src/core/storage/storage-provider.js +146 -217
- package/lib/src/routes/api.routes.js +4 -1
- package/lib/src/services/s3.service.js +2 -2
- package/lib/src/services/storage.service.js +117 -19
- package/lib/src/utils/cleanup-scheduler.js +91 -0
- package/lib/src/utils/helpers.js +6 -6
- package/package.json +2 -3
- package/views/bucket.hbs +0 -5
- package/views/bucketadmin_preview_modal.hbs +3 -3
|
@@ -1,217 +1,146 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MBKBucket - Core Storage Provider Interface
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
async
|
|
89
|
-
throw new StorageUnsupportedOperationError(`
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
async
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
* @param {object} [options={}]
|
|
148
|
-
* @returns {Promise<boolean>}
|
|
149
|
-
*/
|
|
150
|
-
async fileExists(key, options = {}) {
|
|
151
|
-
try {
|
|
152
|
-
const meta = await this.getFileMetadata(key, options);
|
|
153
|
-
return Boolean(meta?.exists);
|
|
154
|
-
} catch {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Get file size in bytes.
|
|
161
|
-
* @param {string} key
|
|
162
|
-
* @param {object} [options={}]
|
|
163
|
-
* @returns {Promise<number|null>}
|
|
164
|
-
*/
|
|
165
|
-
async getFileSize(key, options = {}) {
|
|
166
|
-
try {
|
|
167
|
-
const meta = await this.getFileMetadata(key, options);
|
|
168
|
-
return meta?.exists ? (meta.ContentLength ?? null) : null;
|
|
169
|
-
} catch {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
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
|
-
async copyFile(sourceKey, destKey, options = {}) {
|
|
194
|
-
throw new StorageUnsupportedOperationError(`copyFile() is not supported by provider ${this.type}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
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
|
-
async moveFile(sourceKey, destKey, options = {}) {
|
|
205
|
-
await this.copyFile(sourceKey, destKey, options);
|
|
206
|
-
await this.deleteFile(sourceKey, options);
|
|
207
|
-
return { sourceKey, destKey, movedAt: new Date().toISOString() };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Run health check.
|
|
212
|
-
* @returns {Promise<{ status: 'healthy'|'unhealthy', responseTime?: number, error?: string, [key: string]: any }>}
|
|
213
|
-
*/
|
|
214
|
-
async checkHealth() {
|
|
215
|
-
throw new StorageUnsupportedOperationError(`checkHealth() not implemented by ${this.constructor.name}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MBKBucket - Core Storage Provider Interface
|
|
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
|
+
import { StorageCapabilities } from "./capabilities.js";
|
|
9
|
+
|
|
10
|
+
export class StorageProvider {
|
|
11
|
+
constructor(name, config = {}) {
|
|
12
|
+
if (new.target === StorageProvider) {
|
|
13
|
+
throw new TypeError("Cannot construct StorageProvider instances directly");
|
|
14
|
+
}
|
|
15
|
+
this.name = String(name);
|
|
16
|
+
this.config = Object.freeze({ ...config });
|
|
17
|
+
this.rootPrefix = config.rootPrefix || config.basePath || '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get type() {
|
|
21
|
+
return 'generic';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get capabilities() {
|
|
25
|
+
return new StorageCapabilities({
|
|
26
|
+
multipart: false,
|
|
27
|
+
nativeFolders: false,
|
|
28
|
+
ranges: true,
|
|
29
|
+
copy: false,
|
|
30
|
+
move: false,
|
|
31
|
+
search: false,
|
|
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}'`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async listFiles(prefix = '', options = {}) {
|
|
48
|
+
throw new StorageUnsupportedOperationError(`listFiles() not implemented by ${this.constructor.name}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
52
|
+
throw new StorageUnsupportedOperationError(`uploadFile() not implemented by ${this.constructor.name}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async downloadFile(key, options = {}) {
|
|
56
|
+
throw new StorageUnsupportedOperationError(`downloadFile() not implemented by ${this.constructor.name}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async deleteFile(key, options = {}) {
|
|
60
|
+
throw new StorageUnsupportedOperationError(`deleteFile() not implemented by ${this.constructor.name}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async deleteFiles(keys, options = {}) {
|
|
64
|
+
const concurrency = Math.max(1, Math.min(Number(options.concurrency) || 8, 32));
|
|
65
|
+
let deletedCount = 0;
|
|
66
|
+
const errors = [];
|
|
67
|
+
const results = [];
|
|
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
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { results, deletedCount, errors, deletedAt: new Date().toISOString() };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async deleteFolder(prefix, options = {}) {
|
|
89
|
+
throw new StorageUnsupportedOperationError(`deleteFolder() not implemented by ${this.constructor.name}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async createFolder(key, options = {}) {
|
|
93
|
+
throw new StorageUnsupportedOperationError(`createFolder() not implemented by ${this.constructor.name}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getFileMetadata(key, options = {}) {
|
|
97
|
+
throw new StorageUnsupportedOperationError(`getFileMetadata() not implemented by ${this.constructor.name}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async fileExists(key, options = {}) {
|
|
101
|
+
try {
|
|
102
|
+
const meta = await this.getFileMetadata(key, options);
|
|
103
|
+
return Boolean(meta?.exists);
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getFileSize(key, options = {}) {
|
|
110
|
+
try {
|
|
111
|
+
const meta = await this.getFileMetadata(key, options);
|
|
112
|
+
return meta?.exists ? (meta.ContentLength ?? null) : null;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async copyFile(sourceKey, destKey, options = {}) {
|
|
119
|
+
this.assertCapability('copy', 'copyFile');
|
|
120
|
+
throw new StorageUnsupportedOperationError(`copyFile() is not supported by provider ${this.type}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async moveFile(sourceKey, destKey, options = {}) {
|
|
124
|
+
this.assertCapability('move', 'moveFile');
|
|
125
|
+
await this.copyFile(sourceKey, destKey, options);
|
|
126
|
+
await this.deleteFile(sourceKey, options);
|
|
127
|
+
return { sourceKey, destKey, movedAt: new Date().toISOString() };
|
|
128
|
+
}
|
|
129
|
+
|
|
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
|
+
|
|
142
|
+
async checkHealth() {
|
|
143
|
+
throw new StorageUnsupportedOperationError(`checkHealth() not implemented by ${this.constructor.name}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
@@ -2,7 +2,7 @@ import express from "express";
|
|
|
2
2
|
import { sessRole } from "mbkauthe";
|
|
3
3
|
import multer from "multer";
|
|
4
4
|
import { requireBucketApi } from "../middleware/bucket-resolver.js";
|
|
5
|
-
import { cleanupUploads, listIncompleteUploads, listFiles, uploadSingleFile, initiateMultipartUpload, uploadChunk, completeUpload, abortUpload, createFolder, deleteItems, downloadFileHandler, moveItem, copyItem, getHealthStatus, getFileInfo } from "../controllers/api.controller.js";
|
|
5
|
+
import { cleanupUploads, listIncompleteUploads, listFiles, uploadSingleFile, initiateMultipartUpload, uploadChunk, completeUpload, abortUpload, createFolder, deleteItems, downloadFileHandler, moveItem, copyItem, getHealthStatus, getFileInfo, getMetricsHandler } from "../controllers/api.controller.js";
|
|
6
6
|
|
|
7
7
|
const MAX_CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
8
8
|
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: MAX_CHUNK_SIZE } });
|
|
@@ -38,6 +38,8 @@ export function createApiRoutes({
|
|
|
38
38
|
apiRouter.get('/api/files', viewAuth, listFiles);
|
|
39
39
|
apiRouter.get('/api/health', viewAuth, getHealthStatus);
|
|
40
40
|
apiRouter.get('/api/file-info/*key', viewAuth, getFileInfo);
|
|
41
|
+
apiRouter.get('/api/metrics', viewAuth, getMetricsHandler);
|
|
42
|
+
apiRouter.get('/metrics', viewAuth, getMetricsHandler);
|
|
41
43
|
apiRouter.post('/upload', uploadAuth, upload.single('file'), uploadSingleFile);
|
|
42
44
|
apiRouter.post('/upload-init', uploadAuth, initiateMultipartUpload);
|
|
43
45
|
apiRouter.post('/upload-chunk', uploadAuth, uploadChunkMiddleware, uploadChunk);
|
|
@@ -52,6 +54,7 @@ export function createApiRoutes({
|
|
|
52
54
|
return apiRouter;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
|
|
55
58
|
const router = createApiRoutes();
|
|
56
59
|
|
|
57
60
|
export default router;
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { storageManager } from "../core/storage/storage-manager.js";
|
|
10
|
-
import { getAvailableBucketNames, resolveBucketName, getAppName, ensureKeyHasAppPrefix, ensurePrefix, uploadFile, downloadFile, deleteFile, deleteFiles, deleteFolder, listfiles, getFileMetadata, fileExists, getFileSize,
|
|
10
|
+
import { getAvailableBucketNames, resolveBucketName, getAppName, ensureKeyHasAppPrefix, ensurePrefix, uploadFile, downloadFile, deleteFile, deleteFiles, deleteFolder, listfiles, getFileMetadata, fileExists, getFileSize, createMultipartUpload, uploadPart, completeMultipartUpload, abortMultipartUpload, listIncompleteMultipartUploads, cleanupIncompleteMultipartUploads, checkHealth, runHealthCheck } from "./storage.service.js";
|
|
11
11
|
|
|
12
|
-
export { getAvailableBucketNames, resolveBucketName, getAppName, ensureKeyHasAppPrefix, ensurePrefix, uploadFile, downloadFile, deleteFile, deleteFiles, deleteFolder, listfiles, getFileMetadata, fileExists, getFileSize,
|
|
12
|
+
export { getAvailableBucketNames, resolveBucketName, getAppName, ensureKeyHasAppPrefix, ensurePrefix, uploadFile, downloadFile, deleteFile, deleteFiles, deleteFolder, listfiles, getFileMetadata, fileExists, getFileSize, createMultipartUpload, uploadPart, completeMultipartUpload, abortMultipartUpload, listIncompleteMultipartUploads, cleanupIncompleteMultipartUploads, checkHealth, runHealthCheck };
|
|
13
13
|
|
|
14
14
|
export function getBucketConfig(bucketName) {
|
|
15
15
|
return storageManager.getConnectionConfig(bucketName);
|
|
@@ -8,6 +8,7 @@ import dotenv from "dotenv";
|
|
|
8
8
|
import { storageManager } from "../core/storage/storage-manager.js";
|
|
9
9
|
import { PathStrategy } from "../core/storage/path-strategy.js";
|
|
10
10
|
import { StorageValidationError } from "../core/storage/errors.js";
|
|
11
|
+
import { metricsRegistry } from "../core/metrics.js";
|
|
11
12
|
import { createLogger } from "#logger";
|
|
12
13
|
import { trimSlashes } from "#helpers";
|
|
13
14
|
|
|
@@ -64,22 +65,55 @@ export async function runHealthCheck(connectionName) {
|
|
|
64
65
|
export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
65
66
|
if (!key || !fileBuffer) throw new StorageValidationError('Key and file buffer are required');
|
|
66
67
|
const keyToUpload = ensureKeyHasAppPrefix(key);
|
|
67
|
-
const
|
|
68
|
-
|
|
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
|
+
}
|
|
69
83
|
}
|
|
70
84
|
|
|
71
85
|
export async function downloadFile(key, options = {}) {
|
|
72
86
|
if (!key) throw new StorageValidationError('Key is required');
|
|
73
87
|
const keyToDownload = ensureKeyHasAppPrefix(key);
|
|
74
|
-
const
|
|
75
|
-
|
|
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
|
+
}
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
export async function deleteFile(key, connectionName) {
|
|
79
103
|
if (!key) throw new StorageValidationError('Key is required');
|
|
80
104
|
const keyToDelete = ensureKeyHasAppPrefix(key);
|
|
81
105
|
const provider = getStorageProvider(connectionName);
|
|
82
|
-
|
|
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
|
+
}
|
|
83
117
|
}
|
|
84
118
|
|
|
85
119
|
export async function deleteFiles(keys, connectionName) {
|
|
@@ -88,33 +122,82 @@ export async function deleteFiles(keys, connectionName) {
|
|
|
88
122
|
}
|
|
89
123
|
const keysToDelete = keys.map(k => ensureKeyHasAppPrefix(k));
|
|
90
124
|
const provider = getStorageProvider(connectionName);
|
|
91
|
-
|
|
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
|
+
}
|
|
92
136
|
}
|
|
93
137
|
|
|
94
138
|
export async function deleteFolder(prefix, connectionName) {
|
|
95
139
|
if (!prefix) throw new StorageValidationError('Prefix is required');
|
|
96
140
|
const effectivePrefix = `${ensurePrefix(trimSlashes(prefix))}/`;
|
|
97
141
|
const provider = getStorageProvider(connectionName);
|
|
98
|
-
|
|
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
|
+
}
|
|
99
153
|
}
|
|
100
154
|
|
|
101
155
|
export async function createFolder(prefix, connectionName, options = {}) {
|
|
102
156
|
const effectivePrefix = ensurePrefix(prefix);
|
|
103
157
|
const provider = getStorageProvider(connectionName);
|
|
104
|
-
|
|
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
|
+
}
|
|
105
169
|
}
|
|
106
170
|
|
|
107
171
|
export async function listfiles(prefix = '', options = {}) {
|
|
108
172
|
const effectivePrefix = ensurePrefix(prefix);
|
|
109
|
-
const
|
|
110
|
-
|
|
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
|
+
}
|
|
111
185
|
}
|
|
112
186
|
|
|
113
187
|
export async function getFileMetadata(key, connectionName) {
|
|
114
188
|
if (!key) throw new StorageValidationError('Key is required');
|
|
115
189
|
const keyToCheck = ensureKeyHasAppPrefix(key);
|
|
116
190
|
const provider = getStorageProvider(connectionName);
|
|
117
|
-
|
|
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
|
+
}
|
|
118
201
|
}
|
|
119
202
|
|
|
120
203
|
export async function fileExists(key, connectionName) {
|
|
@@ -135,26 +218,41 @@ export async function getFileSize(key, connectionName) {
|
|
|
135
218
|
}
|
|
136
219
|
}
|
|
137
220
|
|
|
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
221
|
export async function copyFile(sourceKey, destKey, options = {}, connectionName) {
|
|
145
222
|
const src = ensureKeyHasAppPrefix(sourceKey);
|
|
146
223
|
const dst = ensureKeyHasAppPrefix(destKey);
|
|
147
224
|
const provider = getStorageProvider(connectionName);
|
|
148
|
-
|
|
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
|
+
}
|
|
149
236
|
}
|
|
150
237
|
|
|
151
238
|
export async function moveFile(sourceKey, destKey, options = {}, connectionName) {
|
|
152
239
|
const src = ensureKeyHasAppPrefix(sourceKey);
|
|
153
240
|
const dst = ensureKeyHasAppPrefix(destKey);
|
|
154
241
|
const provider = getStorageProvider(connectionName);
|
|
155
|
-
|
|
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
|
+
}
|
|
156
253
|
}
|
|
157
254
|
|
|
255
|
+
|
|
158
256
|
// ---------------------------------------------------------------------------
|
|
159
257
|
// Multipart Upload Helpers (For S3 / compatible providers)
|
|
160
258
|
// ---------------------------------------------------------------------------
|