mbkbucket 3.8.3 → 4.0.1

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 (36) hide show
  1. package/.env.example +4 -3
  2. package/README.md +375 -167
  3. package/index.js +3 -1
  4. package/lib/cli.js +239 -239
  5. package/lib/public/bucketadmin.css +855 -521
  6. package/lib/public/bucketadmin.js +367 -31
  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 +18 -5
  11. package/lib/src/controllers/api.controller.js +66 -17
  12. package/lib/src/controllers/dashboard.controller.js +30 -10
  13. package/lib/src/controllers/info.controller.js +34 -3
  14. package/lib/src/controllers/view.controller.js +16 -14
  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 +6 -6
  25. package/lib/src/routes/api.routes.js +5 -1
  26. package/lib/src/routes/index.js +2 -2
  27. package/lib/src/routes/view.routes.js +5 -2
  28. package/lib/src/services/s3.service.js +16 -568
  29. package/lib/src/services/storage.service.js +213 -0
  30. package/lib/src/utils/errors.js +15 -5
  31. package/package.json +2 -3
  32. package/views/bucket.hbs +47 -71
  33. package/views/bucketadmin_move_modal.hbs +35 -0
  34. package/views/bucketadmin_preview_modal.hbs +81 -13
  35. package/views/bucketadmincontent.hbs +56 -7
  36. package/views/mbkbucket_info.hbs +109 -207
@@ -29,33 +29,13 @@
29
29
  body {
30
30
  font-family: var(--font);
31
31
  font-size: 14px;
32
- line-height: 1.6;
32
+ line-height: 1.5;
33
33
  color: var(--text);
34
- background:
35
- radial-gradient(circle at 12% 14%, rgba(16, 185, 129, 0.1), transparent 44%),
36
- radial-gradient(circle at 88% 8%, rgba(15, 118, 110, 0.08), transparent 40%),
37
- linear-gradient(160deg, #eef7f2 0%, #f8fbf7 52%, #eaf2ef 100%);
34
+ background: var(--body-bg, #f8fafc);
38
35
  min-height: 100vh;
39
36
  overflow-x: hidden;
40
- }
41
-
42
- /* Ambient blobs */
43
- body::before, body::after {
44
- content: '';
45
- position: fixed;
46
- border-radius: 50%;
47
- pointer-events: none;
48
- z-index: 0;
49
- }
50
- body::before {
51
- width: 280px; height: 280px;
52
- top: 10vh; left: -80px;
53
- background: radial-gradient(circle, rgba(15,118,110,0.14) 0, transparent 70%);
54
- }
55
- body::after {
56
- width: 320px; height: 320px;
57
- bottom: 8vh; right: -100px;
58
- background: radial-gradient(circle, rgba(16,185,129,0.1) 0, transparent 70%);
37
+ -webkit-font-smoothing: antialiased;
38
+ -moz-osx-font-smoothing: grayscale;
59
39
  }
60
40
 
61
41
  /* ── Top nav ───────────────────────────────────────────────── */
@@ -13,55 +13,49 @@
13
13
  /* ─── Standalone design tokens (fallbacks via @property-like approach) ──────── */
14
14
  :root {
15
15
  /* Colours */
16
- --mbkh-primary: var(--primary-color-ml, #0f766e);
17
- --mbkh-primary-lt: var(--primary-light-color-ml, #d7f4ee);
18
- --mbkh-primary-dk: var(--primary-dark-color-ml, #0b4f4a);
19
- --mbkh-primary-hover: var(--primary-hover, #0d635c);
16
+ --mbkh-primary: var(--primary-color-ml, #0f766e);
17
+ --mbkh-primary-lt: var(--primary-light-color-ml, #f0fdfa);
18
+ --mbkh-primary-dk: var(--primary-dark-color-ml, #115e59);
19
+ --mbkh-primary-hover: var(--primary-hover, #134e4a);
20
20
 
21
- --mbkh-danger: var(--danger-color-ml, #dc2626);
22
- --mbkh-success: var(--success-color-ml, #16a34a);
23
- --mbkh-warning: var(--warning-color-ml, #d97706);
21
+ --mbkh-danger: var(--danger-color-ml, #ef4444);
22
+ --mbkh-success: var(--success-color-ml, #10b981);
23
+ --mbkh-warning: var(--warning-color-ml, #f59e0b);
24
24
 
25
25
  /* Backgrounds */
26
- --mbkh-body-bg: var(--body-bg, #f4f7f3);
27
- --mbkh-surface: var(--bg-surface, rgba(255,255,255,0.85));
28
- --mbkh-surface2: var(--bg-surface-secondary, rgba(244,247,243,0.85));
29
- --mbkh-surface3: var(--bg-surface-tertiary, #e2ede8);
26
+ --mbkh-body-bg: var(--body-bg, #f8fafc);
27
+ --mbkh-surface: var(--bg-surface, #ffffff);
28
+ --mbkh-surface2: var(--bg-surface-secondary, #f8fafc);
29
+ --mbkh-surface3: var(--bg-surface-tertiary, #f1f5f9);
30
30
 
31
31
  /* Borders */
32
- --mbkh-border: var(--border-color, rgba(15,118,110,0.16));
33
- --mbkh-border-lt: var(--border-color-light, rgba(15,118,110,0.08));
34
- --mbkh-border-active: rgba(15,118,110,0.45);
32
+ --mbkh-border: var(--border-color, #e2e8f0);
33
+ --mbkh-border-lt: var(--border-color-light, #f1f5f9);
34
+ --mbkh-border-active: rgba(15, 118, 110, 0.35);
35
35
 
36
36
  /* Text */
37
- --mbkh-text: var(--text, #1f2a2a);
38
- --mbkh-text-sec: var(--text-secondary, #425855);
39
- --mbkh-text-muted: var(--text-muted, #667c79);
40
- --mbkh-text-dim: var(--text-disabled, #a0b2af);
37
+ --mbkh-text: var(--text, #0f172a);
38
+ --mbkh-text-sec: var(--text-secondary, #475569);
39
+ --mbkh-text-muted: var(--text-muted, #64748b);
40
+ --mbkh-text-dim: var(--text-disabled, #94a3b8);
41
41
 
42
42
  /* Typography */
43
- --mbkh-font: var(--font, "IBM Plex Sans", "Segoe UI", sans-serif);
44
- --mbkh-font-heading: var(--font-headings, "Sora", "Segoe UI", sans-serif);
45
- --mbkh-font-mono: var(--font-mono, "JetBrains Mono", Consolas, monospace);
43
+ --mbkh-font: var(--font, "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif);
44
+ --mbkh-font-heading: var(--font-headings, "Segoe UI", -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif);
45
+ --mbkh-font-mono: var(--font-mono, "Segoe UI Mono", "Cascadia Code", Consolas, "Courier New", monospace);
46
46
 
47
47
  /* Shape */
48
- --mbkh-radius: var(--radius-lg, 14px);
48
+ --mbkh-radius: var(--radius-lg, 12px);
49
49
  --mbkh-radius-md: var(--radius, 8px);
50
50
  --mbkh-radius-sm: var(--radius-sm, 6px);
51
51
 
52
52
  /* Shadows */
53
- --mbkh-shadow: var(--shadow-lg,
54
- 0 20px 40px rgba(15,118,110,0.12),
55
- 0 4px 12px rgba(0,0,0,0.06));
56
- --mbkh-shadow-sm: var(--shadow-md,
57
- 0 8px 20px rgba(15,118,110,0.08),
58
- 0 2px 6px rgba(0,0,0,0.04));
59
- --mbkh-shadow-dropdown: var(--shadow-dropdown,
60
- 0 12px 30px rgba(15,42,42,0.14),
61
- 0 4px 10px rgba(0,0,0,0.05));
53
+ --mbkh-shadow: var(--shadow-dropdown, 0 10px 25px -5px rgba(15, 23, 42, 0.12), 0 8px 10px -6px rgba(15, 23, 42, 0.04));
54
+ --mbkh-shadow-sm: var(--shadow-sm, 0 1px 3px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04));
55
+ --mbkh-shadow-dropdown: var(--shadow-dropdown, 0 10px 25px -5px rgba(15, 23, 42, 0.12), 0 8px 10px -6px rgba(15, 23, 42, 0.04));
62
56
 
63
57
  /* Transition */
64
- --mbkh-transition: 0.15s ease-in-out;
58
+ --mbkh-transition: 0.12s ease;
65
59
  }
66
60
 
67
61
  /* ─── Reset inside helper components ───────────────────────────────────────── */
@@ -82,14 +76,14 @@
82
76
  position: fixed;
83
77
  inset: 0;
84
78
  z-index: 99990;
85
- background: rgba(15, 42, 42, 0.45);
79
+ background: rgba(15, 23, 42, 0.5);
86
80
  backdrop-filter: blur(4px);
87
81
  -webkit-backdrop-filter: blur(4px);
88
82
  display: flex;
89
83
  align-items: center;
90
84
  justify-content: center;
91
85
  padding: 16px;
92
- animation: mbkh-fade-in 0.18s ease;
86
+ animation: mbkh-fade-in 0.15s ease;
93
87
  }
94
88
 
95
89
  @keyframes mbkh-fade-in { from { opacity: 0; } to { opacity: 1; } }
@@ -151,9 +151,10 @@
151
151
  close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
152
152
  search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
153
153
  grid: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>`,
154
- list: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`,
155
- upload: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/></svg>`,
156
- plus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
154
+ list: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`,
155
+ upload: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`,
156
+ download: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
157
+ plus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
157
158
  check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
158
159
  warn: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
159
160
  info: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
@@ -1,6 +1,6 @@
1
1
  const TRUE_VALUES = new Set(['true', 't', '1', 'yes', 'y', 'on']);
2
2
  const FALSE_VALUES = new Set(['false', 'f', '0', 'no', 'n', 'off']);
3
- const REQUIRED_BUCKET_FIELDS = ['BUCKET_NAME', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT'];
3
+ const REQUIRED_S3_FIELDS = ['BUCKET_NAME', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT'];
4
4
 
5
5
  export function normalizeBooleanLike(value) {
6
6
  if (typeof value === 'boolean') return value;
@@ -62,12 +62,25 @@ export function parseAndValidateBucketConnection(rawValue) {
62
62
  throw new Error(`Bucket '${bucketName}' configuration must be an object, not a string. Remove quotes around the inner JSON object.`);
63
63
  }
64
64
 
65
- const missingFields = REQUIRED_BUCKET_FIELDS.filter(field => !config[field]);
66
- if (missingFields.length) {
67
- throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
65
+ const providerType = String(config.type || config.provider || '').toLowerCase();
66
+
67
+ if (providerType === 'gdrive' || providerType === 'google-drive' || providerType === 'googledrive') {
68
+ const hasOAuth = Boolean(config.client_id && config.client_secret && (config.refresh_token || config.access_token || config.code));
69
+ const hasServiceAccount = Boolean((config.client_email || config.service_account_email) && (config.private_key || config.service_account_key));
70
+ const hasAccessTokenOnly = Boolean(config.access_token);
71
+ const hasServiceAccountKeyObj = Boolean(config.service_account && typeof config.service_account === 'object');
72
+
73
+ if (!hasOAuth && !hasServiceAccount && !hasAccessTokenOnly && !hasServiceAccountKeyObj) {
74
+ throw new Error(`Google Drive connection '${bucketName}' requires OAuth credentials (client_id, client_secret, refresh_token) or Service Account credentials (client_email, private_key).`);
75
+ }
76
+ } else {
77
+ // Default is S3-compatible provider
78
+ const missingFields = REQUIRED_S3_FIELDS.filter(field => !config[field]);
79
+ if (missingFields.length) {
80
+ throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
81
+ }
68
82
  }
69
83
  }
70
84
 
71
85
  return parsed;
72
86
  }
73
-
@@ -1,5 +1,5 @@
1
1
  import { pipeline } from "stream/promises";
2
- import { listfiles, uploadFile, deleteFile, deleteFiles, deleteFolder, downloadFile, getFileMetadata, fileExists, ensureKeyHasAppPrefix, ensurePrefix, createMultipartUpload, uploadPart, completeMultipartUpload, abortMultipartUpload, listIncompleteMultipartUploads, cleanupIncompleteMultipartUploads } from "../services/s3.service.js";
2
+ import { listfiles, uploadFile, deleteFile, deleteFiles, deleteFolder, createFolder as createStorageFolder, downloadFile, getFileMetadata, fileExists, ensureKeyHasAppPrefix, ensurePrefix, createMultipartUpload, uploadPart, completeMultipartUpload, abortMultipartUpload, listIncompleteMultipartUploads, cleanupIncompleteMultipartUploads, getStorageProvider, copyFile, moveFile, checkHealth } from "../services/storage.service.js";
3
3
  import { sendApiError, classifyApiError } from "../utils/errors.js";
4
4
  import { getBaseName, isSensitiveTextType, buildKey, normalizeKeyParam, getFolderPath, destroyStream } from "../utils/helpers.js";
5
5
  import { createLogger } from "#logger";
@@ -18,6 +18,9 @@ function resolvePrefix(req, res, prefix) {
18
18
  async function cleanupFolderMarker(folderPath, bucketName) {
19
19
  if (!folderPath) return;
20
20
  try {
21
+ const provider = getStorageProvider(bucketName);
22
+ if (provider.capabilities.nativeFolders) return;
23
+
21
24
  const meta = await getFileMetadata(folderPath, bucketName);
22
25
  if (meta.exists && (meta.ContentLength === 0 || meta.Metadata?.marker === 'true')) {
23
26
  await deleteFile(folderPath, bucketName);
@@ -214,6 +217,14 @@ export async function uploadSingleFile(req, res) {
214
217
  export async function initiateMultipartUpload(req, res) {
215
218
  try {
216
219
  const bucketName = req.activeBucket;
220
+ const provider = getStorageProvider(bucketName);
221
+ if (!provider.capabilities.multipart) {
222
+ return res.status(501).json({
223
+ success: false,
224
+ error: `Multipart upload is not supported by provider '${provider.type}'. Use single-file direct upload.`
225
+ });
226
+ }
227
+
217
228
  const { fileName, prefix = '', contentType = 'application/octet-stream' } = req.body;
218
229
  if (!fileName) return res.status(400).json({ success: false, error: 'fileName is required' });
219
230
 
@@ -298,21 +309,8 @@ export async function createFolder(req, res) {
298
309
  ? `${cleanPrefix}/${cleanName}/`
299
310
  : `${cleanName}/`;
300
311
 
301
- if (await fileExists(folderKey, bucketName)) {
302
- return res.status(409).json({ success: false, error: 'Folder already exists' });
303
- }
304
-
305
- const existingFiles = await listfiles(folderKey, { maxKeys: 1, bucketName });
306
- if (existingFiles.Contents?.length) {
307
- return res.json({ success: true, message: 'Folder already exists with content', key: folderKey, skipMarker: true });
308
- }
309
-
310
- await uploadFile(folderKey, Buffer.alloc(0), 'application/x-empty', {
311
- metadata: { folder: 'true', marker: 'true' },
312
- bucketName
313
- });
314
-
315
- return res.json({ success: true, message: 'Folder created', key: folderKey });
312
+ const result = await createStorageFolder(folderKey, bucketName);
313
+ return res.json({ success: true, message: result.message || 'Folder created', key: result.key || folderKey });
316
314
  } catch (err) {
317
315
  console.error('Create folder failed:', err);
318
316
  return sendApiError(res, err, 'Create folder failed');
@@ -381,7 +379,7 @@ export async function downloadFileHandler(req, res) {
381
379
  res.setHeader('Content-Type', contentType);
382
380
  if (contentLength) res.setHeader('Content-Length', contentLength);
383
381
  if (result.ETag) res.setHeader('ETag', result.ETag);
384
- if (result.LastModified) res.setHeader('Last-Modified', result.LastModified.toUTCString());
382
+ if (result.LastModified) res.setHeader('Last-Modified', new Date(result.LastModified).toUTCString());
385
383
 
386
384
  const isSensitive = isSensitiveTextType(contentType);
387
385
  const isLargeFile = Number(contentLength || 0) > 50 * 1024 * 1024;
@@ -408,3 +406,54 @@ export async function downloadFileHandler(req, res) {
408
406
  }
409
407
  }
410
408
  }
409
+
410
+ export async function moveItem(req, res) {
411
+ try {
412
+ const { sourceKey, destKey } = req.body;
413
+ if (!sourceKey || !destKey) {
414
+ return res.status(400).json({ success: false, error: 'sourceKey and destKey are required' });
415
+ }
416
+
417
+ const result = await moveFile(sourceKey, destKey, {}, req.activeBucket);
418
+ res.json({ success: true, message: `Moved ${sourceKey} to ${destKey}`, ...result });
419
+ } catch (error) {
420
+ console.error('[mbkbucket] Move failed:', error);
421
+ return sendApiError(res, error, 'Move failed');
422
+ }
423
+ }
424
+
425
+ export async function copyItem(req, res) {
426
+ try {
427
+ const { sourceKey, destKey } = req.body;
428
+ if (!sourceKey || !destKey) {
429
+ return res.status(400).json({ success: false, error: 'sourceKey and destKey are required' });
430
+ }
431
+
432
+ const result = await copyFile(sourceKey, destKey, {}, req.activeBucket);
433
+ res.json({ success: true, message: `Copied ${sourceKey} to ${destKey}`, ...result });
434
+ } catch (error) {
435
+ console.error('[mbkbucket] Copy failed:', error);
436
+ return sendApiError(res, error, 'Copy failed');
437
+ }
438
+ }
439
+
440
+ export async function getHealthStatus(req, res) {
441
+ try {
442
+ const health = await checkHealth(req.activeBucket);
443
+ res.json({ success: true, ...health });
444
+ } catch (error) {
445
+ return sendApiError(res, error, 'Health check failed');
446
+ }
447
+ }
448
+
449
+ export async function getFileInfo(req, res) {
450
+ try {
451
+ const key = normalizeKeyParam(req.params.key || req.query.key);
452
+ if (!key) return res.status(400).json({ success: false, error: 'key is required' });
453
+
454
+ const meta = await getFileMetadata(key, req.activeBucket);
455
+ res.json({ success: true, ...meta });
456
+ } catch (error) {
457
+ return sendApiError(res, error, 'Failed to get file info');
458
+ }
459
+ }
@@ -1,14 +1,16 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { resolveBucketName, getAvailableBucketNames } from "../services/s3.service.js";
3
+ import { resolveStorageName, getAvailableStorageNames, getStorageProvider, getStorageConfig } from "../services/storage.service.js";
4
+ import { storageManager } from "../core/storage/storage-manager.js";
4
5
  import { renderPage, mbkautheVar } from "mbkauthe";
5
6
  import { commonHandlebarsHelpers } from "../utils/helpers.js";
7
+ import { mbkbucketVar } from "../config/index.js";
6
8
 
7
9
  /**
8
- * Render the main bucket dashboard page.
10
+ * Render the main storage/bucket dashboard page.
9
11
  */
10
12
  export function renderBucketDashboard(req, res) {
11
- const allBuckets = getAvailableBucketNames();
13
+ const allBuckets = getAvailableStorageNames();
12
14
  const configuredDefaultBucket = mbkautheVar?.bucket;
13
15
  const isPortal = mbkautheVar?.APP_NAME === "portal";
14
16
  const bucketfilePath = isPortal ? "bucketportal.hbs" : "bucket.hbs";
@@ -19,19 +21,34 @@ export function renderBucketDashboard(req, res) {
19
21
 
20
22
  let selectedBucket;
21
23
  let error = req.query.error;
24
+ let providerType = 's3';
25
+ let providerCapabilities = {};
22
26
 
23
27
  try {
24
- selectedBucket = resolveBucketName(req.query.bucket);
28
+ selectedBucket = resolveStorageName(req.query.bucket);
29
+ const provider = getStorageProvider(selectedBucket);
30
+ providerType = provider.type;
31
+ providerCapabilities = provider.capabilities;
25
32
  } catch (err) {
26
33
  selectedBucket = mbkautheVar?.bucket;
27
34
  error ||= err.message;
28
35
  }
29
36
 
30
- const bucketOptions = allBuckets.map(name => ({
31
- name,
32
- isSelected: name === selectedBucket,
33
- isDefault: name === configuredDefaultBucket
34
- }));
37
+ const bucketOptions = allBuckets.map(name => {
38
+ let type = 's3';
39
+ try {
40
+ const cfg = getStorageConfig(name);
41
+ type = storageManager.inferProviderType(cfg);
42
+ } catch {}
43
+ return {
44
+ name,
45
+ type,
46
+ isSelected: name === selectedBucket,
47
+ isDefault: name === configuredDefaultBucket
48
+ };
49
+ });
50
+
51
+ const publicViewEnabled = Boolean(mbkbucketVar?.publiView_enabled);
35
52
 
36
53
  renderPage(req, res, bucketfilePath, Boolean(portalLayout), {
37
54
  page: "Admin Bucket",
@@ -40,10 +57,13 @@ export function renderBucketDashboard(req, res) {
40
57
  bucketvar: selectedBucket,
41
58
  selectedBucket,
42
59
  bucketOptions,
60
+ providerType,
61
+ providerCapabilities,
43
62
  APP_NAME: mbkautheVar?.APP_NAME,
63
+ publicViewEnabled,
64
+ publiViewEnabled: publicViewEnabled,
44
65
  message: req.query.message,
45
66
  error,
46
67
  availableBuckets: allBuckets
47
68
  });
48
69
  }
49
-
@@ -1,5 +1,7 @@
1
1
  import { renderPage, mbkautheVar } from "mbkauthe";
2
- import { packageJson, getLatestVersion } from "../config/index.js";
2
+ import { packageJson, getLatestVersion, appVersion, mbkbucketVar } from "../config/index.js";
3
+ import { getAvailableBucketNames, getStorageProvider } from "../services/storage.service.js";
4
+ import { storageManager } from "../core/storage/storage-manager.js";
3
5
 
4
6
  async function getInfoData() {
5
7
  let latestVersion = 'unknown';
@@ -8,10 +10,38 @@ async function getInfoData() {
8
10
  } catch (err) {
9
11
  console.error("[mbkbucket] Error fetching latest version:", err);
10
12
  }
13
+
14
+ const buckets = getAvailableBucketNames();
15
+ const defaultBucket = storageManager.getDefaultConnectionName() || (buckets[0] ?? '');
16
+ let providerType = 'S3';
17
+ if (defaultBucket) {
18
+ try {
19
+ const provider = getStorageProvider(defaultBucket);
20
+ providerType = (provider?.type || 's3').toUpperCase();
21
+ } catch {}
22
+ }
23
+
24
+ // Safe subset only - strictly prevent leaking passwords, DB connection strings, OAuth secrets, or storage credentials
25
+ const safeMbkautheVar = {
26
+ APP_NAME: mbkautheVar?.APP_NAME || mbkautheVar?.app_name || "mbkbucket",
27
+ };
28
+
29
+ const safeMbkbucketVar = {
30
+ publiView_enabled: Boolean(mbkbucketVar?.publiView_enabled),
31
+ p_view_inline: Boolean(mbkbucketVar?.p_view_inline)
32
+ };
33
+
11
34
  return {
12
35
  CurrentVersion: packageJson.version,
36
+ version: packageJson.version,
13
37
  latestVersion,
14
- APP_NAME: mbkautheVar?.APP_NAME
38
+ APP_NAME: safeMbkautheVar.APP_NAME,
39
+ APP_VERSION: appVersion,
40
+ mbkautheVar: safeMbkautheVar,
41
+ mbkbucketVar: safeMbkbucketVar,
42
+ defaultBucket,
43
+ configuredBucketsCount: buckets.length,
44
+ providerType
15
45
  };
16
46
  }
17
47
 
@@ -20,7 +50,8 @@ async function getInfoData() {
20
50
  */
21
51
  export async function infoPage(req, res) {
22
52
  renderPage(req, res, "mbkbucket_info.hbs", false, {
23
- page: "MBK Bucket Info",
53
+ pageTitle: "MBKBucket Information",
54
+ ogUrl: "/mbkbucket/info",
24
55
  ...(await getInfoData())
25
56
  });
26
57
  }
@@ -1,4 +1,4 @@
1
- import { downloadFile, getFileMetadata, ensureKeyHasAppPrefix, generateSignedUrl, resolveBucketName } from "../services/s3.service.js";
1
+ import { downloadFile, getFileMetadata, ensureKeyHasAppPrefix, generateSignedUrl, resolveStorageName, getStorageProvider } from "../services/storage.service.js";
2
2
  import { mbkbucketVar } from "../config/index.js";
3
3
  import { escapeHtml, getBaseName, getFileExt, getMimeType, normalizeKeyParam, destroyStream, VIEWABLE_TYPES, VIEWABLE_TYPES_SORTED, STATIC_ASSET_TYPES, VIDEO_TYPES, AUDIO_TYPES, buildCacheControl, parseRangeHeader, renderPlayerPage } from "../utils/helpers.js";
4
4
  import { createLogger } from "#logger";
@@ -62,7 +62,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
62
62
 
63
63
  if (!meta?.exists) return res.status(404).json({ message: 'File not found', key: keyToUse });
64
64
 
65
- const etag = `"${meta.LastModified?.getTime() || Date.now()}-${meta.ContentLength || 0}"`;
65
+ const etag = `"${new Date(meta.LastModified || Date.now()).getTime()}-${meta.ContentLength || 0}"`;
66
66
  if (ifNoneMatch === etag) return res.status(304).end();
67
67
 
68
68
  const total = Number(meta.ContentLength);
@@ -91,7 +91,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
91
91
  res.setHeader('Accept-Ranges', 'bytes');
92
92
  if (noindex) res.setHeader('X-Robots-Tag', 'noindex, nofollow');
93
93
  res.setHeader('X-Content-Type-Options', 'nosniff');
94
- if (meta.LastModified) res.setHeader('Last-Modified', meta.LastModified.toUTCString());
94
+ if (meta.LastModified) res.setHeader('Last-Modified', new Date(meta.LastModified).toUTCString());
95
95
 
96
96
  attachStreamCleanup(req, res, rangeResult.Body);
97
97
  return rangeResult.Body.pipe(res);
@@ -114,10 +114,10 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
114
114
  try {
115
115
  result = await downloadFile(keyToUse, downloadOptions);
116
116
  } catch (err) {
117
- if (err.name === 'NoSuchKey' || err.name === 'NotFound') {
117
+ if (err.name === 'NoSuchKey' || err.name === 'NotFound' || err.message?.includes('File not found')) {
118
118
  return res.status(404).json({ message: 'File not found', key: keyToUse });
119
119
  }
120
- if (err.name === 'AccessDenied') {
120
+ if (err.name === 'AccessDenied' || err.message?.includes('Access denied')) {
121
121
  return res.status(403).json({ message: 'Access denied' });
122
122
  }
123
123
  console.error('[mbkbucket] serveFileInline download error:', err);
@@ -126,7 +126,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
126
126
 
127
127
  if (result?.notModified) return res.status(304).end();
128
128
 
129
- const etag = `"${result.LastModified?.getTime() || Date.now()}-${result.ContentLength || 0}"`;
129
+ const etag = `"${new Date(result.LastModified || Date.now()).getTime()}-${result.ContentLength || 0}"`;
130
130
  if (ifNoneMatch === etag) return res.status(304).end();
131
131
 
132
132
  res.setHeader('Content-Type', contentType);
@@ -136,7 +136,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
136
136
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
137
137
  if (noindex) res.setHeader('X-Robots-Tag', 'noindex, nofollow');
138
138
  if (result.ContentLength) res.setHeader('Content-Length', result.ContentLength);
139
- if (result.LastModified) res.setHeader('Last-Modified', result.LastModified.toUTCString());
139
+ if (result.LastModified) res.setHeader('Last-Modified', new Date(result.LastModified).toUTCString());
140
140
  if (fileExtension === 'html' || fileExtension === 'htm') {
141
141
  res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
142
142
  }
@@ -162,9 +162,9 @@ export async function viewFile(req, res) {
162
162
  await serveFileInline(req, res, keyToUse, { noindex: true, bucketName: req.activeBucket, publicCache: false });
163
163
  } catch (error) {
164
164
  console.error("Error viewing file:", error);
165
- if (error.name === 'NoSuchKey' || error.name === 'NotFound') {
165
+ if (error.name === 'NoSuchKey' || error.name === 'NotFound' || error.message?.includes('File not found')) {
166
166
  res.status(404).json({ message: "File not found", key: req.params.key });
167
- } else if (error.name === 'AccessDenied') {
167
+ } else if (error.name === 'AccessDenied' || error.message?.includes('Access denied')) {
168
168
  res.status(403).json({ message: "Access denied" });
169
169
  } else {
170
170
  res.status(500).json({ message: "View failed", error: error.message });
@@ -199,7 +199,7 @@ export async function playerPage(req, res) {
199
199
 
200
200
  export async function publicView(req, res) {
201
201
  try {
202
- const bucketName = resolveBucketName();
202
+ const bucketName = req.query?.bucket ? resolveStorageName(req.query.bucket) : resolveStorageName();
203
203
  const key = normalizeKeyParam(req.params.key);
204
204
 
205
205
  if (!key || key.endsWith('/')) {
@@ -215,11 +215,13 @@ export async function publicView(req, res) {
215
215
 
216
216
  if (!mbkbucketVar?.p_view_inline) {
217
217
  try {
218
- const signed = await generateSignedUrl(keyToUse, 'getObject', 3600, bucketName);
219
- return res.redirect(302, signed.url);
218
+ const provider = getStorageProvider(bucketName);
219
+ if (provider.capabilities.presignedUrls) {
220
+ const signed = await generateSignedUrl(keyToUse, 'getObject', 3600, bucketName);
221
+ return res.redirect(302, signed.url);
222
+ }
220
223
  } catch (err) {
221
- console.error('[mbkbucket] p_view signed URL error:', err);
222
- return res.status(500).json({ message: 'Failed to generate access URL' });
224
+ debugView('Signed URL redirection failed, falling back to inline serve: %s', err.message);
223
225
  }
224
226
  }
225
227
 
@@ -0,0 +1,62 @@
1
+ /**
2
+ * MBKBucket - Typed Storage Error Hierarchy
3
+ * Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ export class StorageError extends Error {
8
+ /**
9
+ * @param {string} message
10
+ * @param {number} [status=500]
11
+ * @param {string} [code='INTERNAL_STORAGE_ERROR']
12
+ */
13
+ constructor(message, status = 500, code = 'INTERNAL_STORAGE_ERROR') {
14
+ super(message);
15
+ this.name = this.constructor.name;
16
+ this.status = status;
17
+ this.code = code;
18
+ Error.captureStackTrace?.(this, this.constructor);
19
+ }
20
+ }
21
+
22
+ export class StorageNotFoundError extends StorageError {
23
+ constructor(message = 'File or folder not found') {
24
+ super(message, 404, 'NOT_FOUND');
25
+ }
26
+ }
27
+
28
+ export class StorageAccessDeniedError extends StorageError {
29
+ constructor(message = 'Access denied') {
30
+ super(message, 403, 'ACCESS_DENIED');
31
+ }
32
+ }
33
+
34
+ export class StorageConflictError extends StorageError {
35
+ constructor(message = 'Resource already exists') {
36
+ super(message, 409, 'CONFLICT');
37
+ }
38
+ }
39
+
40
+ export class StorageValidationError extends StorageError {
41
+ constructor(message = 'Validation failed') {
42
+ super(message, 400, 'VALIDATION_ERROR');
43
+ }
44
+ }
45
+
46
+ export class StorageConfigError extends StorageError {
47
+ constructor(message = 'Storage configuration error') {
48
+ super(message, 503, 'STORAGE_CONFIG_ERROR');
49
+ }
50
+ }
51
+
52
+ export class StorageQuotaExceededError extends StorageError {
53
+ constructor(message = 'Storage quota exceeded') {
54
+ super(message, 507, 'QUOTA_EXCEEDED');
55
+ }
56
+ }
57
+
58
+ export class StorageUnsupportedOperationError extends StorageError {
59
+ constructor(message = 'Operation not supported by this storage provider') {
60
+ super(message, 501, 'NOT_IMPLEMENTED');
61
+ }
62
+ }