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.
Files changed (38) hide show
  1. package/.env.example +15 -14
  2. package/README.md +561 -377
  3. package/index.js +32 -30
  4. package/lib/cli.js +154 -565
  5. package/lib/public/bucketadmin.css +2042 -1708
  6. package/lib/public/bucketadmin.js +1622 -1286
  7. package/lib/public/mbkbucket-helper-demo.html +4 -24
  8. package/lib/public/mbkbucket-helper.css +28 -34
  9. package/lib/public/mbkbucket-helper.js +4 -3
  10. package/lib/src/config/validation.js +86 -73
  11. package/lib/src/controllers/api.controller.js +459 -410
  12. package/lib/src/controllers/dashboard.controller.js +69 -49
  13. package/lib/src/controllers/info.controller.js +34 -3
  14. package/lib/src/controllers/view.controller.js +233 -231
  15. package/lib/src/core/storage/errors.js +62 -0
  16. package/lib/src/core/storage/gdrive-auth.js +193 -0
  17. package/lib/src/core/storage/gdrive-provider.js +656 -0
  18. package/lib/src/core/storage/index.js +14 -0
  19. package/lib/src/core/storage/models.js +80 -0
  20. package/lib/src/core/storage/path-strategy.js +65 -0
  21. package/lib/src/core/storage/s3-provider.js +491 -0
  22. package/lib/src/core/storage/storage-manager.js +203 -0
  23. package/lib/src/core/storage/storage-provider.js +217 -0
  24. package/lib/src/middleware/bucket-resolver.js +37 -37
  25. package/lib/src/routes/api.routes.js +5 -1
  26. package/lib/src/routes/index.js +60 -60
  27. package/lib/src/routes/view.routes.js +28 -25
  28. package/lib/src/services/s3.service.js +42 -594
  29. package/lib/src/services/storage.service.js +213 -0
  30. package/lib/src/utils/errors.js +56 -46
  31. package/package.json +11 -12
  32. package/views/bucket.hbs +47 -71
  33. package/views/bucketadmin_move_modal.hbs +35 -0
  34. package/views/bucketadmin_preview_modal.hbs +108 -40
  35. package/views/bucketadmincontent.hbs +193 -144
  36. package/views/mbkbucket_info.hbs +134 -232
  37. package/.mbkbucket.config.example.json +0 -14
  38. 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({ authorization = sessRole('superadmin') } = {}) {
10
- const viewRouter = express.Router();
11
-
12
- viewRouter.use(requireBucketView);
13
- viewRouter.get('/player/*key', authorization, playerPage);
14
- viewRouter.get('/view/*key', authorization, viewFile);
15
-
16
- if (mbkbucketVar?.publiView_enabled) {
17
- viewRouter.get('/p_view/*key', pviewRateLimit, pviewSecurity, publicView);
18
- }
19
-
20
- return viewRouter;
21
- }
22
-
23
- const router = createViewRoutes();
24
-
25
- export default router;
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
- import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, ListMultipartUploadsCommand } from '@aws-sdk/client-s3';
2
- import dotenv from "dotenv";
3
- import { mbkautheVar } from "mbkauthe";
4
- import { createLogger } from "#logger";
5
- import { nowIso, trimSlashes, trimLeadingSlashes } from "#helpers";
6
-
7
- dotenv.config();
8
- const debugS3 = createLogger('s3');
9
-
10
- function parseJsonEnv(envVar, fallback = {}) {
11
- if (!envVar) return fallback;
12
- try {
13
- return JSON.parse(envVar);
14
- } catch (e) {
15
- console.error(`❌ Error parsing JSON for envVar: ${String(envVar).slice(0, 200)} - ${e.message}`);
16
- console.error(`💡 Tip: Check that inner objects are NOT quoted as strings. Use {"key":{"field":"value"}} not {"key":"{"field":"value"}"}`);
17
- return fallback;
18
- }
19
- }
20
-
21
- const allBucketConfigs = parseJsonEnv(process.env.BucketConnection, {});
22
- const availableBucketNames = Object.freeze(Object.keys(allBucketConfigs || {}));
23
- const hasBucketConfigs = availableBucketNames.length > 0;
24
-
25
- const escapeRegExp = value => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
-
27
- export const getAvailableBucketNames = () => availableBucketNames;
28
-
29
- const getDefaultBucketName = () => mbkautheVar?.bucket || (availableBucketNames.length > 0 ? availableBucketNames[0] : null);
30
-
31
- export function resolveBucketName(bucketName) {
32
- const raw = typeof bucketName === 'string' ? bucketName.trim() : bucketName;
33
- const candidate = !raw ? getDefaultBucketName() : String(raw);
34
-
35
- if (!candidate) {
36
- throw new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
37
- }
38
- if (!hasBucketConfigs) {
39
- throw new Error('BucketConnection environment variable is not set or empty. Please configure it with your bucket connections.');
40
- }
41
- if (!allBucketConfigs[candidate]) {
42
- throw new Error(`Bucket '${candidate}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
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
+ });