mbkbucket 3.8.2 → 4.0.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 +15 -14
- package/README.md +561 -377
- package/index.js +32 -30
- package/lib/cli.js +154 -565
- package/lib/public/bucketadmin.css +2042 -1708
- package/lib/public/bucketadmin.js +1622 -1286
- package/lib/public/mbkbucket-helper-demo.html +4 -24
- package/lib/public/mbkbucket-helper.css +28 -34
- package/lib/public/mbkbucket-helper.js +4 -3
- package/lib/src/config/validation.js +86 -73
- package/lib/src/controllers/api.controller.js +459 -410
- package/lib/src/controllers/dashboard.controller.js +69 -49
- package/lib/src/controllers/info.controller.js +34 -3
- package/lib/src/controllers/view.controller.js +233 -231
- package/lib/src/core/storage/errors.js +62 -0
- package/lib/src/core/storage/gdrive-auth.js +193 -0
- package/lib/src/core/storage/gdrive-provider.js +656 -0
- package/lib/src/core/storage/index.js +14 -0
- package/lib/src/core/storage/models.js +80 -0
- package/lib/src/core/storage/path-strategy.js +65 -0
- package/lib/src/core/storage/s3-provider.js +491 -0
- package/lib/src/core/storage/storage-manager.js +203 -0
- package/lib/src/core/storage/storage-provider.js +217 -0
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/routes/api.routes.js +5 -1
- package/lib/src/routes/index.js +60 -60
- package/lib/src/routes/view.routes.js +28 -25
- package/lib/src/services/s3.service.js +42 -594
- package/lib/src/services/storage.service.js +213 -0
- package/lib/src/utils/errors.js +56 -46
- package/package.json +11 -12
- package/views/bucket.hbs +47 -71
- package/views/bucketadmin_move_modal.hbs +35 -0
- package/views/bucketadmin_preview_modal.hbs +108 -40
- package/views/bucketadmincontent.hbs +193 -144
- package/views/mbkbucket_info.hbs +134 -232
- package/.mbkbucket.config.example.json +0 -14
- package/lib/src/cli/auth-helper.js +0 -141
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import { sessRole } from "mbkauthe";
|
|
3
|
-
import { mbkbucketVar } from "../config/index.js";
|
|
4
|
-
import { requireBucketView } from "../middleware/bucket-resolver.js";
|
|
5
|
-
import { pviewRateLimit } from "../middleware/rate-limiter.js";
|
|
6
|
-
import { pviewSecurity } from "../middleware/security.js";
|
|
7
|
-
import { viewFile, playerPage, publicView } from "../controllers/view.controller.js";
|
|
8
|
-
|
|
9
|
-
export function createViewRoutes({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
viewRouter.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { sessRole } from "mbkauthe";
|
|
3
|
+
import { mbkbucketVar } from "../config/index.js";
|
|
4
|
+
import { requireBucketView } from "../middleware/bucket-resolver.js";
|
|
5
|
+
import { pviewRateLimit } from "../middleware/rate-limiter.js";
|
|
6
|
+
import { pviewSecurity } from "../middleware/security.js";
|
|
7
|
+
import { viewFile, playerPage, publicView } from "../controllers/view.controller.js";
|
|
8
|
+
|
|
9
|
+
export function createViewRoutes({
|
|
10
|
+
authorization = sessRole('superadmin'),
|
|
11
|
+
publiViewEnabled = mbkbucketVar?.publiView_enabled
|
|
12
|
+
} = {}) {
|
|
13
|
+
const viewRouter = express.Router();
|
|
14
|
+
|
|
15
|
+
viewRouter.use(requireBucketView);
|
|
16
|
+
viewRouter.get('/player/*key', authorization, playerPage);
|
|
17
|
+
viewRouter.get('/view/*key', authorization, viewFile);
|
|
18
|
+
|
|
19
|
+
if (publiViewEnabled ?? mbkbucketVar?.publiView_enabled) {
|
|
20
|
+
viewRouter.get('/p_view/*key', pviewRateLimit, pviewSecurity, publicView);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return viewRouter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const router = createViewRoutes();
|
|
27
|
+
|
|
28
|
+
export default router;
|
|
@@ -1,594 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
return candidate;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function getAppName() {
|
|
48
|
-
const app = mbkautheVar?.APP_NAME;
|
|
49
|
-
return typeof app === 'string' && app.trim() ? app.trim() : '';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const isRootModeApp = appName => ['portal', 'mbkbucket'].includes(String(appName || '').toLowerCase());
|
|
53
|
-
|
|
54
|
-
function validateKeySafety(str, isKey = false) {
|
|
55
|
-
if (isKey && !str) throw new Error('Key is required');
|
|
56
|
-
if (str && (str.includes('../') || str.includes('..\\') || /\.\.[\\/]/.test(str))) {
|
|
57
|
-
throw new Error(`Path traversal detected: invalid ${isKey ? 'key' : 'prefix'}`);
|
|
58
|
-
}
|
|
59
|
-
if (str && /[\x00-\x1f\x7f]/.test(str)) {
|
|
60
|
-
throw new Error(`Invalid characters in ${isKey ? 'key' : 'prefix'}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function ensureKeyHasAppPrefix(key = '') {
|
|
65
|
-
if (Array.isArray(key)) key = key.join('/');
|
|
66
|
-
validateKeySafety(key, true);
|
|
67
|
-
|
|
68
|
-
const app = getAppName();
|
|
69
|
-
const cleaned = trimLeadingSlashes(key);
|
|
70
|
-
if (isRootModeApp(app)) return cleaned;
|
|
71
|
-
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
72
|
-
|
|
73
|
-
const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
|
|
74
|
-
return cleaned.startsWith(`${app}/`) ? cleaned.replace(appPrefixRegex, `${app}/`) : `${app}/${cleaned}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function ensurePrefix(prefix = '') {
|
|
78
|
-
if (Array.isArray(prefix)) prefix = prefix.join('/');
|
|
79
|
-
validateKeySafety(prefix, false);
|
|
80
|
-
|
|
81
|
-
const app = getAppName();
|
|
82
|
-
const p = trimLeadingSlashes(prefix);
|
|
83
|
-
if (isRootModeApp(app)) return p;
|
|
84
|
-
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
85
|
-
if (!p || p === app || p === `${app}/`) return p || app;
|
|
86
|
-
|
|
87
|
-
return p.startsWith(`${app}/`)
|
|
88
|
-
? p.replace(new RegExp(`^(?:${escapeRegExp(app)}\/)+`), `${app}/`)
|
|
89
|
-
: `${app}/${p}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// S3 Client & Config Cache
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
const _clientCache = new Map();
|
|
96
|
-
const _configCache = new Map();
|
|
97
|
-
|
|
98
|
-
export function getBucketConfig(bucketName) {
|
|
99
|
-
const resolved = resolveBucketName(bucketName);
|
|
100
|
-
if (_configCache.has(resolved)) return _configCache.get(resolved);
|
|
101
|
-
|
|
102
|
-
if (!hasBucketConfigs) {
|
|
103
|
-
console.error('[mbkbucket] ❌ BucketConnection is not configured or failed to parse');
|
|
104
|
-
throw new Error('BucketConnection environment variable is not set or empty. Please configure it with your bucket connections.');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const cfg = allBucketConfigs[resolved];
|
|
108
|
-
if (!cfg) throw new Error(`Bucket '${resolved}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
|
|
109
|
-
|
|
110
|
-
_configCache.set(resolved, cfg);
|
|
111
|
-
return cfg;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function getBucketClient(bucketName) {
|
|
115
|
-
const resolved = resolveBucketName(bucketName);
|
|
116
|
-
if (_clientCache.has(resolved)) return _clientCache.get(resolved);
|
|
117
|
-
|
|
118
|
-
const config = getBucketConfig(resolved);
|
|
119
|
-
const client = new S3Client({
|
|
120
|
-
region: config.region || 'auto',
|
|
121
|
-
endpoint: config.ENDPOINT,
|
|
122
|
-
credentials: {
|
|
123
|
-
accessKeyId: config.ACCESS_KEY_ID,
|
|
124
|
-
secretAccessKey: config.SECRET_ACCESS_KEY,
|
|
125
|
-
},
|
|
126
|
-
maxAttempts: 3,
|
|
127
|
-
retryMode: 'adaptive',
|
|
128
|
-
requestTimeout: 120000,
|
|
129
|
-
requestHandler: {
|
|
130
|
-
connectionTimeout: 5000,
|
|
131
|
-
socketTimeout: 120000,
|
|
132
|
-
maxSockets: 50,
|
|
133
|
-
keepAlive: true,
|
|
134
|
-
keepAliveMsecs: 1000,
|
|
135
|
-
},
|
|
136
|
-
useAccelerateEndpoint: false,
|
|
137
|
-
forcePathStyle: true,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
_clientCache.set(resolved, client);
|
|
141
|
-
return client;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function getBucketClientAndConfig(bucketName) {
|
|
145
|
-
const resolved = resolveBucketName(bucketName);
|
|
146
|
-
return {
|
|
147
|
-
client: getBucketClient(resolved),
|
|
148
|
-
config: getBucketConfig(resolved),
|
|
149
|
-
bucketName: resolved
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let _defaultClient = null;
|
|
154
|
-
export const bucketClient = new Proxy({}, {
|
|
155
|
-
get(_, prop) {
|
|
156
|
-
_defaultClient ??= getBucketClient();
|
|
157
|
-
const val = _defaultClient[prop];
|
|
158
|
-
return typeof val === 'function' ? val.bind(_defaultClient) : val;
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
// Health Checks
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
|
|
166
|
-
export async function checkHealth() {
|
|
167
|
-
try {
|
|
168
|
-
const startTime = Date.now();
|
|
169
|
-
const { client, config } = getBucketClientAndConfig();
|
|
170
|
-
const { BUCKET_NAME: bucket, region = 'auto' } = config;
|
|
171
|
-
|
|
172
|
-
await client.send(new ListObjectsV2Command({ Bucket: bucket, MaxKeys: 1 }));
|
|
173
|
-
return {
|
|
174
|
-
status: 'healthy',
|
|
175
|
-
responseTime: Date.now() - startTime,
|
|
176
|
-
bucket,
|
|
177
|
-
region,
|
|
178
|
-
checkedAt: nowIso()
|
|
179
|
-
};
|
|
180
|
-
} catch (error) {
|
|
181
|
-
let bucket = 'unknown';
|
|
182
|
-
try { bucket = getBucketConfig().BUCKET_NAME; } catch {}
|
|
183
|
-
return { status: 'unhealthy', error: error.message, bucket, checkedAt: nowIso() };
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export async function runHealthCheck() {
|
|
188
|
-
try {
|
|
189
|
-
const health = await checkHealth();
|
|
190
|
-
if (health.status === 'healthy') {
|
|
191
|
-
debugS3('Connected to bucket: %s (%sms)', health.bucket, health.responseTime);
|
|
192
|
-
} else {
|
|
193
|
-
console.error('[mbkbucket] S3 connection failed:', health.error);
|
|
194
|
-
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
console.error("[mbkbucket] S3 connection test error:", err.message);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ---------------------------------------------------------------------------
|
|
201
|
-
// Core S3 Operations
|
|
202
|
-
// ---------------------------------------------------------------------------
|
|
203
|
-
|
|
204
|
-
export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
205
|
-
try {
|
|
206
|
-
if (!key || !fileBuffer) throw new Error('Key and file buffer are required');
|
|
207
|
-
|
|
208
|
-
const keyToUpload = ensureKeyHasAppPrefix(key);
|
|
209
|
-
const {
|
|
210
|
-
metadata = {},
|
|
211
|
-
cacheControl = 'public, max-age=31536000',
|
|
212
|
-
storageClass = 'STANDARD',
|
|
213
|
-
serverSideEncryption = 'AES256',
|
|
214
|
-
bucketName,
|
|
215
|
-
preventOverwrite = false
|
|
216
|
-
} = options;
|
|
217
|
-
|
|
218
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
219
|
-
debugS3('Uploading file to bucket=%s key=%s', config.BUCKET_NAME, keyToUpload);
|
|
220
|
-
|
|
221
|
-
const uploadedAt = nowIso();
|
|
222
|
-
const commandParams = {
|
|
223
|
-
Bucket: config.BUCKET_NAME,
|
|
224
|
-
Key: keyToUpload,
|
|
225
|
-
Body: fileBuffer,
|
|
226
|
-
ContentType: contentType,
|
|
227
|
-
CacheControl: cacheControl,
|
|
228
|
-
Metadata: {
|
|
229
|
-
'uploaded-at': uploadedAt,
|
|
230
|
-
'file-size': fileBuffer.length.toString(),
|
|
231
|
-
'upload-source': 'web-portal',
|
|
232
|
-
...metadata
|
|
233
|
-
},
|
|
234
|
-
ServerSideEncryption: serverSideEncryption,
|
|
235
|
-
StorageClass: storageClass,
|
|
236
|
-
...(preventOverwrite && { IfNoneMatch: '*' })
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const result = await client.send(new PutObjectCommand(commandParams));
|
|
240
|
-
return {
|
|
241
|
-
...result,
|
|
242
|
-
fileSize: fileBuffer.length,
|
|
243
|
-
key: keyToUpload,
|
|
244
|
-
contentType,
|
|
245
|
-
uploadedAt
|
|
246
|
-
};
|
|
247
|
-
} catch (error) {
|
|
248
|
-
console.error(`Upload failed for key ${key}:`, error);
|
|
249
|
-
if (error.name === 'PreconditionFailed' || error.$metadata?.httpStatusCode === 412) {
|
|
250
|
-
throw new Error('File already exists');
|
|
251
|
-
}
|
|
252
|
-
throw new Error(`Upload failed: ${error.message}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export async function downloadFile(key, options = {}) {
|
|
257
|
-
let keyToDownload;
|
|
258
|
-
try {
|
|
259
|
-
if (!key) throw new Error('Key is required');
|
|
260
|
-
keyToDownload = ensureKeyHasAppPrefix(key);
|
|
261
|
-
|
|
262
|
-
const { range, ifNoneMatch, ifModifiedSince, responseCacheControl, responseContentType, bucketName } = options;
|
|
263
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
264
|
-
|
|
265
|
-
if (range) debugS3('Range request for %s: %s', keyToDownload, range);
|
|
266
|
-
|
|
267
|
-
const result = await client.send(new GetObjectCommand({
|
|
268
|
-
Bucket: config.BUCKET_NAME,
|
|
269
|
-
Key: keyToDownload,
|
|
270
|
-
...(range && { Range: range }),
|
|
271
|
-
...(ifNoneMatch && { IfNoneMatch: ifNoneMatch }),
|
|
272
|
-
...(ifModifiedSince && { IfModifiedSince: ifModifiedSince }),
|
|
273
|
-
...(responseCacheControl && { ResponseCacheControl: responseCacheControl }),
|
|
274
|
-
...(responseContentType && { ResponseContentType: responseContentType }),
|
|
275
|
-
}));
|
|
276
|
-
|
|
277
|
-
return { ...result, key: keyToDownload };
|
|
278
|
-
} catch (error) {
|
|
279
|
-
if (error?.$metadata?.httpStatusCode === 304 || error?.name === '304' || error?.name === 'NotModified') {
|
|
280
|
-
debugS3('File not modified (304) for key %s', keyToDownload);
|
|
281
|
-
return { notModified: true, key: keyToDownload };
|
|
282
|
-
}
|
|
283
|
-
console.error(`Download failed for key ${key}:`, error);
|
|
284
|
-
if (error.name === 'NoSuchKey') throw new Error(`File not found: ${key}`);
|
|
285
|
-
if (error.name === 'AccessDenied') throw new Error(`Access denied for file: ${key}`);
|
|
286
|
-
throw new Error(`Download failed: ${error.message}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export async function deleteFile(key, bucketName) {
|
|
291
|
-
try {
|
|
292
|
-
if (!key) throw new Error('Key is required');
|
|
293
|
-
const keyToDelete = ensureKeyHasAppPrefix(key);
|
|
294
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
295
|
-
|
|
296
|
-
const result = await client.send(new DeleteObjectCommand({
|
|
297
|
-
Bucket: config.BUCKET_NAME,
|
|
298
|
-
Key: keyToDelete,
|
|
299
|
-
}));
|
|
300
|
-
|
|
301
|
-
return { ...result, key: keyToDelete, deletedAt: nowIso() };
|
|
302
|
-
} catch (error) {
|
|
303
|
-
console.error('Delete failed:', error);
|
|
304
|
-
throw new Error(`Delete failed: ${error.message}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export async function deleteFiles(keys, bucketName) {
|
|
309
|
-
try {
|
|
310
|
-
if (!keys || !Array.isArray(keys) || !keys.length) {
|
|
311
|
-
throw new Error('Keys array is required and must not be empty');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
315
|
-
const maxBatchSize = 1000;
|
|
316
|
-
const results = [];
|
|
317
|
-
let deletedCount = 0;
|
|
318
|
-
const errors = [];
|
|
319
|
-
|
|
320
|
-
for (let i = 0; i < keys.length; i += maxBatchSize) {
|
|
321
|
-
const batch = keys.slice(i, i + maxBatchSize).map(k => ({ Key: ensureKeyHasAppPrefix(k) }));
|
|
322
|
-
const result = await client.send(new DeleteObjectsCommand({
|
|
323
|
-
Bucket: config.BUCKET_NAME,
|
|
324
|
-
Delete: { Objects: batch, Quiet: false }
|
|
325
|
-
}));
|
|
326
|
-
|
|
327
|
-
results.push(result);
|
|
328
|
-
deletedCount += result.Deleted?.length || 0;
|
|
329
|
-
if (result.Errors?.length) errors.push(...result.Errors);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return { results, deletedCount, errors, deletedAt: nowIso() };
|
|
333
|
-
} catch (error) {
|
|
334
|
-
console.error('Batch delete failed:', error);
|
|
335
|
-
throw new Error(`Batch delete failed: ${error.message}`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export async function deleteFolder(prefix, bucketName) {
|
|
340
|
-
try {
|
|
341
|
-
if (!prefix) throw new Error('Prefix is required');
|
|
342
|
-
const effectivePrefix = `${ensurePrefix(trimSlashes(prefix))}/`;
|
|
343
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
344
|
-
|
|
345
|
-
const keysToDelete = [];
|
|
346
|
-
let continuationToken = null;
|
|
347
|
-
|
|
348
|
-
do {
|
|
349
|
-
const resp = await client.send(new ListObjectsV2Command({
|
|
350
|
-
Bucket: config.BUCKET_NAME,
|
|
351
|
-
Prefix: effectivePrefix,
|
|
352
|
-
ContinuationToken: continuationToken,
|
|
353
|
-
MaxKeys: 1000
|
|
354
|
-
}));
|
|
355
|
-
(resp.Contents || []).forEach(obj => { if (obj.Key) keysToDelete.push(obj.Key); });
|
|
356
|
-
continuationToken = resp.IsTruncated ? resp.NextContinuationToken : null;
|
|
357
|
-
} while (continuationToken);
|
|
358
|
-
|
|
359
|
-
if (!keysToDelete.length) {
|
|
360
|
-
const exists = await fileExists(effectivePrefix, bucketName);
|
|
361
|
-
if (exists) {
|
|
362
|
-
await deleteFile(effectivePrefix, bucketName);
|
|
363
|
-
return { deletedCount: 1, deletedAt: nowIso(), prefix: effectivePrefix };
|
|
364
|
-
}
|
|
365
|
-
return { deletedCount: 0, deletedAt: nowIso(), prefix: effectivePrefix };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const result = await deleteFiles(keysToDelete, bucketName);
|
|
369
|
-
return { ...result, prefix: effectivePrefix, deletedAt: nowIso() };
|
|
370
|
-
} catch (error) {
|
|
371
|
-
console.error('Delete folder failed:', error);
|
|
372
|
-
throw new Error(`Delete folder failed: ${error.message}`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export async function listfiles(prefix = '', options = {}) {
|
|
377
|
-
try {
|
|
378
|
-
const {
|
|
379
|
-
maxKeys = 1000,
|
|
380
|
-
continuationToken = null,
|
|
381
|
-
delimiter = null,
|
|
382
|
-
fetchOwner = false,
|
|
383
|
-
startAfter = null,
|
|
384
|
-
bucketName
|
|
385
|
-
} = options;
|
|
386
|
-
|
|
387
|
-
const effectivePrefix = ensurePrefix(prefix);
|
|
388
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
389
|
-
|
|
390
|
-
const result = await client.send(new ListObjectsV2Command({
|
|
391
|
-
Bucket: config.BUCKET_NAME,
|
|
392
|
-
Prefix: effectivePrefix,
|
|
393
|
-
MaxKeys: maxKeys,
|
|
394
|
-
...(continuationToken && { ContinuationToken: continuationToken }),
|
|
395
|
-
...(delimiter && { Delimiter: delimiter }),
|
|
396
|
-
...(fetchOwner && { FetchOwner: fetchOwner }),
|
|
397
|
-
...(startAfter && { StartAfter: startAfter }),
|
|
398
|
-
}));
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
...result,
|
|
402
|
-
requestedAt: nowIso(),
|
|
403
|
-
totalFiles: result.KeyCount || 0,
|
|
404
|
-
hasMore: result.IsTruncated || false,
|
|
405
|
-
nextToken: result.NextContinuationToken || null
|
|
406
|
-
};
|
|
407
|
-
} catch (error) {
|
|
408
|
-
console.error('List files failed:', error);
|
|
409
|
-
throw new Error(`List files failed: ${error.message}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export async function getFileMetadata(key, bucketName) {
|
|
414
|
-
try {
|
|
415
|
-
if (!key) throw new Error('Key is required');
|
|
416
|
-
const keyToCheck = ensureKeyHasAppPrefix(key);
|
|
417
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
418
|
-
|
|
419
|
-
const result = await client.send(new HeadObjectCommand({
|
|
420
|
-
Bucket: config.BUCKET_NAME,
|
|
421
|
-
Key: keyToCheck,
|
|
422
|
-
}));
|
|
423
|
-
|
|
424
|
-
return { ...result, key: keyToCheck, exists: true, queriedAt: nowIso() };
|
|
425
|
-
} catch (error) {
|
|
426
|
-
const queriedAt = nowIso();
|
|
427
|
-
if (error.name === 'NotFound' || error.name === 'NoSuchKey') {
|
|
428
|
-
return { key, exists: false, queriedAt };
|
|
429
|
-
}
|
|
430
|
-
console.error(`Get metadata failed for key ${key}:`, error);
|
|
431
|
-
throw new Error(`Get metadata failed: ${error.message}`);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
export async function fileExists(key, bucketName) {
|
|
436
|
-
try {
|
|
437
|
-
const meta = await getFileMetadata(key, bucketName);
|
|
438
|
-
return !!meta.exists;
|
|
439
|
-
} catch {
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
export async function getFileSize(key, bucketName) {
|
|
445
|
-
try {
|
|
446
|
-
const meta = await getFileMetadata(key, bucketName);
|
|
447
|
-
return meta.exists ? meta.ContentLength : null;
|
|
448
|
-
} catch {
|
|
449
|
-
return null;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// ---------------------------------------------------------------------------
|
|
454
|
-
// Multipart Upload Helpers
|
|
455
|
-
// ---------------------------------------------------------------------------
|
|
456
|
-
|
|
457
|
-
export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}, bucketName) {
|
|
458
|
-
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
459
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
460
|
-
const result = await client.send(new CreateMultipartUploadCommand({
|
|
461
|
-
Bucket: config.BUCKET_NAME,
|
|
462
|
-
Key: keyToUse,
|
|
463
|
-
ContentType: contentType,
|
|
464
|
-
Metadata: {
|
|
465
|
-
'upload-source': 'web-portal',
|
|
466
|
-
'uploaded-at': nowIso(),
|
|
467
|
-
...metadata
|
|
468
|
-
}
|
|
469
|
-
}));
|
|
470
|
-
return { uploadId: result.UploadId, key: keyToUse };
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
export async function uploadPart(key, uploadId, partNumber, buffer, bucketName) {
|
|
474
|
-
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
475
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
476
|
-
const result = await client.send(new UploadPartCommand({
|
|
477
|
-
Bucket: config.BUCKET_NAME,
|
|
478
|
-
Key: keyToUse,
|
|
479
|
-
UploadId: uploadId,
|
|
480
|
-
PartNumber: partNumber,
|
|
481
|
-
Body: buffer,
|
|
482
|
-
ContentLength: buffer.length
|
|
483
|
-
}));
|
|
484
|
-
return { ETag: result.ETag, partNumber };
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
export async function completeMultipartUpload(key, uploadId, parts, bucketName) {
|
|
488
|
-
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
489
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
490
|
-
const sorted = [...parts].sort((a, b) => a.partNumber - b.partNumber);
|
|
491
|
-
await client.send(new CompleteMultipartUploadCommand({
|
|
492
|
-
Bucket: config.BUCKET_NAME,
|
|
493
|
-
Key: keyToUse,
|
|
494
|
-
UploadId: uploadId,
|
|
495
|
-
MultipartUpload: {
|
|
496
|
-
Parts: sorted.map(p => ({ PartNumber: p.partNumber, ETag: p.ETag }))
|
|
497
|
-
}
|
|
498
|
-
}));
|
|
499
|
-
return { key: keyToUse };
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export async function abortMultipartUpload(key, uploadId, bucketName) {
|
|
503
|
-
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
504
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
505
|
-
await client.send(new AbortMultipartUploadCommand({
|
|
506
|
-
Bucket: config.BUCKET_NAME,
|
|
507
|
-
Key: keyToUse,
|
|
508
|
-
UploadId: uploadId
|
|
509
|
-
}));
|
|
510
|
-
return { key: keyToUse, abortedAt: nowIso() };
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, bucketName) {
|
|
514
|
-
try {
|
|
515
|
-
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
516
|
-
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
517
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
518
|
-
const bucket = config.BUCKET_NAME;
|
|
519
|
-
|
|
520
|
-
const command = operation === 'getObject'
|
|
521
|
-
? new GetObjectCommand({ Bucket: bucket, Key: keyToUse })
|
|
522
|
-
: operation === 'putObject'
|
|
523
|
-
? new PutObjectCommand({ Bucket: bucket, Key: keyToUse })
|
|
524
|
-
: null;
|
|
525
|
-
|
|
526
|
-
if (!command) throw new Error(`Unsupported operation: ${operation}`);
|
|
527
|
-
|
|
528
|
-
const signedUrl = await getSignedUrl(client, command, { expiresIn });
|
|
529
|
-
const now = Date.now();
|
|
530
|
-
return {
|
|
531
|
-
url: signedUrl,
|
|
532
|
-
key,
|
|
533
|
-
operation,
|
|
534
|
-
expiresIn,
|
|
535
|
-
expiresAt: new Date(now + expiresIn * 1000).toISOString(),
|
|
536
|
-
generatedAt: new Date(now).toISOString()
|
|
537
|
-
};
|
|
538
|
-
} catch (error) {
|
|
539
|
-
console.error(`Generate signed URL failed for key ${key}:`, error);
|
|
540
|
-
throw new Error(`Generate signed URL failed: ${error.message}`);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export async function listIncompleteMultipartUploads(prefix = '', bucketName) {
|
|
545
|
-
try {
|
|
546
|
-
const effectivePrefix = ensurePrefix(prefix || '');
|
|
547
|
-
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
548
|
-
const result = await client.send(new ListMultipartUploadsCommand({
|
|
549
|
-
Bucket: config.BUCKET_NAME,
|
|
550
|
-
Prefix: effectivePrefix
|
|
551
|
-
}));
|
|
552
|
-
return result.Uploads || [];
|
|
553
|
-
} catch (error) {
|
|
554
|
-
console.error('Failed to list incomplete multipart uploads:', error);
|
|
555
|
-
throw new Error(`Failed to list incomplete uploads: ${error.message}`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefix = '', bucketName) {
|
|
560
|
-
try {
|
|
561
|
-
const uploads = await listIncompleteMultipartUploads(prefix, bucketName);
|
|
562
|
-
if (!uploads.length) {
|
|
563
|
-
debugS3('No incomplete multipart uploads found');
|
|
564
|
-
return { abortedCount: 0, uploads: [] };
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const cutoffDate = new Date();
|
|
568
|
-
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
|
|
569
|
-
|
|
570
|
-
const uploadsToAbort = uploads.filter(u => u.Initiated && new Date(u.Initiated) < cutoffDate);
|
|
571
|
-
if (!uploadsToAbort.length) {
|
|
572
|
-
debugS3('No incomplete uploads older than %s days', olderThanDays);
|
|
573
|
-
return { abortedCount: 0, uploads: [] };
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
debugS3('Found %s incomplete uploads to clean up', uploadsToAbort.length);
|
|
577
|
-
const aborted = [];
|
|
578
|
-
|
|
579
|
-
for (const upload of uploadsToAbort) {
|
|
580
|
-
try {
|
|
581
|
-
await abortMultipartUpload(upload.Key, upload.UploadId, bucketName);
|
|
582
|
-
aborted.push({ key: upload.Key, uploadId: upload.UploadId, initiated: upload.Initiated });
|
|
583
|
-
debugS3('Aborted incomplete upload: %s (initiated: %s)', upload.Key, upload.Initiated);
|
|
584
|
-
} catch (err) {
|
|
585
|
-
console.error(`[mbkbucket] Failed to abort upload ${upload.Key}:`, err);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
return { abortedCount: aborted.length, uploads: aborted, cleanedAt: nowIso() };
|
|
590
|
-
} catch (error) {
|
|
591
|
-
console.error('Cleanup of incomplete multipart uploads failed:', error);
|
|
592
|
-
throw new Error(`Cleanup failed: ${error.message}`);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MBKBucket - S3 Service Compatibility Layer
|
|
3
|
+
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*
|
|
6
|
+
* Provides backwards-compatible S3 APIs delegating to the unified storage layer.
|
|
7
|
+
*/
|
|
8
|
+
|
|
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, generateSignedUrl, createMultipartUpload, uploadPart, completeMultipartUpload, abortMultipartUpload, listIncompleteMultipartUploads, cleanupIncompleteMultipartUploads, checkHealth, runHealthCheck } from "./storage.service.js";
|
|
11
|
+
|
|
12
|
+
export { getAvailableBucketNames, resolveBucketName, getAppName, ensureKeyHasAppPrefix, ensurePrefix, uploadFile, downloadFile, deleteFile, deleteFiles, deleteFolder, listfiles, getFileMetadata, fileExists, getFileSize, generateSignedUrl, createMultipartUpload, uploadPart, completeMultipartUpload, abortMultipartUpload, listIncompleteMultipartUploads, cleanupIncompleteMultipartUploads, checkHealth, runHealthCheck };
|
|
13
|
+
|
|
14
|
+
export function getBucketConfig(bucketName) {
|
|
15
|
+
return storageManager.getConnectionConfig(bucketName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getBucketClient(bucketName) {
|
|
19
|
+
const provider = storageManager.getProvider(bucketName);
|
|
20
|
+
if (provider.client) {
|
|
21
|
+
return provider.client;
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`Connection '${provider.name}' is of type '${provider.type}' and does not use an S3Client.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getBucketClientAndConfig(bucketName) {
|
|
27
|
+
const resolved = resolveBucketName(bucketName);
|
|
28
|
+
return {
|
|
29
|
+
client: getBucketClient(resolved),
|
|
30
|
+
config: getBucketConfig(resolved),
|
|
31
|
+
bucketName: resolved
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let _defaultClient = null;
|
|
36
|
+
export const bucketClient = new Proxy({}, {
|
|
37
|
+
get(_, prop) {
|
|
38
|
+
_defaultClient ??= getBucketClient();
|
|
39
|
+
const val = _defaultClient[prop];
|
|
40
|
+
return typeof val === 'function' ? val.bind(_defaultClient) : val;
|
|
41
|
+
}
|
|
42
|
+
});
|