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.
- package/.env.example +4 -3
- package/README.md +375 -167
- package/index.js +3 -1
- package/lib/cli.js +239 -239
- package/lib/public/bucketadmin.css +855 -521
- package/lib/public/bucketadmin.js +367 -31
- 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 +18 -5
- package/lib/src/controllers/api.controller.js +66 -17
- package/lib/src/controllers/dashboard.controller.js +30 -10
- package/lib/src/controllers/info.controller.js +34 -3
- package/lib/src/controllers/view.controller.js +16 -14
- 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 +6 -6
- package/lib/src/routes/api.routes.js +5 -1
- package/lib/src/routes/index.js +2 -2
- package/lib/src/routes/view.routes.js +5 -2
- package/lib/src/services/s3.service.js +16 -568
- package/lib/src/services/storage.service.js +213 -0
- package/lib/src/utils/errors.js +15 -5
- package/package.json +2 -3
- package/views/bucket.hbs +47 -71
- package/views/bucketadmin_move_modal.hbs +35 -0
- package/views/bucketadmin_preview_modal.hbs +81 -13
- package/views/bucketadmincontent.hbs +56 -7
- 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.
|
|
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,
|
|
17
|
-
--mbkh-primary-lt: var(--primary-light-color-ml,
|
|
18
|
-
--mbkh-primary-dk: var(--primary-dark-color-ml,
|
|
19
|
-
--mbkh-primary-hover: var(--primary-hover,
|
|
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, #
|
|
22
|
-
--mbkh-success: var(--success-color-ml, #
|
|
23
|
-
--mbkh-warning: var(--warning-color-ml, #
|
|
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, #
|
|
27
|
-
--mbkh-surface: var(--bg-surface,
|
|
28
|
-
--mbkh-surface2: var(--bg-surface-secondary,
|
|
29
|
-
--mbkh-surface3: var(--bg-surface-tertiary, #
|
|
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,
|
|
33
|
-
--mbkh-border-lt: var(--border-color-light,
|
|
34
|
-
--mbkh-border-active: rgba(15,118,110,0.
|
|
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, #
|
|
38
|
-
--mbkh-text-sec: var(--text-secondary, #
|
|
39
|
-
--mbkh-text-muted: var(--text-muted, #
|
|
40
|
-
--mbkh-text-dim: var(--text-disabled, #
|
|
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, "
|
|
44
|
-
--mbkh-font-heading: var(--font-headings, "
|
|
45
|
-
--mbkh-font-mono: var(--font-mono, "
|
|
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,
|
|
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-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
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:
|
|
155
|
-
upload:
|
|
156
|
-
|
|
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
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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/
|
|
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
|
-
|
|
302
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
219
|
-
|
|
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
|
-
|
|
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
|
+
}
|