mbkbucket 4.0.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -2
- package/README.md +96 -44
- package/index.js +4 -1
- package/lib/cli.js +1 -1
- package/lib/public/bucketadmin.js +255 -33
- package/lib/public/mbkbucket-helper-demo.html +0 -58
- package/lib/public/mbkbucket-helper.js +1 -9
- package/lib/src/app.js +10 -24
- package/lib/src/config/validation.js +6 -1
- package/lib/src/controllers/api.controller.js +34 -6
- package/lib/src/controllers/dashboard.controller.js +2 -2
- package/lib/src/controllers/info.controller.js +15 -14
- package/lib/src/controllers/view.controller.js +24 -22
- package/lib/src/core/metrics.js +134 -0
- package/lib/src/core/storage/capabilities.js +58 -0
- package/lib/src/core/storage/errors.js +0 -5
- package/lib/src/core/storage/gdrive-auth.js +0 -36
- package/lib/src/core/storage/gdrive-provider.js +103 -44
- package/lib/src/core/storage/index.js +4 -1
- package/lib/src/core/storage/local-provider.js +508 -0
- package/lib/src/core/storage/models.js +91 -50
- package/lib/src/core/storage/path-strategy.js +22 -2
- package/lib/src/core/storage/s3-provider.js +19 -38
- package/lib/src/core/storage/storage-manager.js +76 -53
- package/lib/src/core/storage/storage-provider.js +49 -120
- package/lib/src/middleware/error-handler.js +15 -52
- package/lib/src/middleware/rate-limiter.js +1 -1
- package/lib/src/routes/api.routes.js +4 -1
- package/lib/src/services/s3.service.js +2 -2
- package/lib/src/services/storage.service.js +118 -20
- package/lib/src/utils/cleanup-scheduler.js +91 -0
- package/lib/src/utils/helpers.js +8 -51
- package/package.json +4 -4
- package/views/bucket.hbs +0 -5
- package/views/bucketadmin_preview_modal.hbs +3 -3
|
@@ -29,6 +29,17 @@
|
|
|
29
29
|
let selectedFiles = new Set();
|
|
30
30
|
let starredFiles = new Set(JSON.parse(localStorage.getItem('mbk_starred_keys') || '[]'));
|
|
31
31
|
let currentPreviewKey = '';
|
|
32
|
+
let currentCapabilities = {
|
|
33
|
+
multipart: true,
|
|
34
|
+
copy: true,
|
|
35
|
+
move: true,
|
|
36
|
+
search: true,
|
|
37
|
+
storageQuota: false,
|
|
38
|
+
hierarchicalFolders: true,
|
|
39
|
+
deleteBatch: true,
|
|
40
|
+
publicUrls: false
|
|
41
|
+
};
|
|
42
|
+
let currentStorageQuota = null;
|
|
32
43
|
|
|
33
44
|
// Upload State
|
|
34
45
|
let uploadQueue = [];
|
|
@@ -134,18 +145,26 @@
|
|
|
134
145
|
return `${url}${sep}bucket=${encodeURIComponent(activeBucket)}`;
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
function encodeKeyForUrl(key) {
|
|
149
|
+
return String(key || '')
|
|
150
|
+
.split('/')
|
|
151
|
+
.map(segment => encodeURIComponent(segment))
|
|
152
|
+
.join('/');
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
function getViewUrl(key) {
|
|
138
|
-
return withBucketUrl(`/mbkbucket/view/${
|
|
156
|
+
return withBucketUrl(`/mbkbucket/view/${encodeKeyForUrl(key)}`);
|
|
139
157
|
}
|
|
140
158
|
|
|
141
159
|
function getPublicViewUrl(key) {
|
|
142
|
-
return withBucketUrl(`/mbkbucket/p_view/${
|
|
160
|
+
return withBucketUrl(`/mbkbucket/p_view/${encodeKeyForUrl(key)}`);
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
function getDownloadUrl(key) {
|
|
146
|
-
return withBucketUrl(`/mbkbucket/download/${
|
|
164
|
+
return withBucketUrl(`/mbkbucket/download/${encodeKeyForUrl(key)}`);
|
|
147
165
|
}
|
|
148
166
|
|
|
167
|
+
|
|
149
168
|
function isImage(fileName) {
|
|
150
169
|
const ext = getFileExt(fileName);
|
|
151
170
|
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(ext);
|
|
@@ -258,6 +277,56 @@
|
|
|
258
277
|
}
|
|
259
278
|
}
|
|
260
279
|
|
|
280
|
+
// Clipboard System with Visual Feedback & Fallback
|
|
281
|
+
function copyToClipboard(text, successMsg = 'Copied to clipboard', btnEl = null) {
|
|
282
|
+
if (!text) return;
|
|
283
|
+
const executeCopy = () => {
|
|
284
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
285
|
+
return navigator.clipboard.writeText(text);
|
|
286
|
+
}
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
288
|
+
const textArea = document.createElement('textarea');
|
|
289
|
+
textArea.value = text;
|
|
290
|
+
textArea.style.position = 'fixed';
|
|
291
|
+
textArea.style.left = '-9999px';
|
|
292
|
+
textArea.style.top = '-9999px';
|
|
293
|
+
textArea.setAttribute('readonly', '');
|
|
294
|
+
document.body.appendChild(textArea);
|
|
295
|
+
textArea.select();
|
|
296
|
+
try {
|
|
297
|
+
const ok = document.execCommand('copy');
|
|
298
|
+
textArea.remove();
|
|
299
|
+
ok ? resolve() : reject(new Error('Copy failed'));
|
|
300
|
+
} catch (e) {
|
|
301
|
+
textArea.remove();
|
|
302
|
+
reject(e);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
executeCopy().then(() => {
|
|
308
|
+
showToast(successMsg, 'success');
|
|
309
|
+
if (btnEl && btnEl instanceof HTMLElement) {
|
|
310
|
+
const originalHtml = btnEl.innerHTML;
|
|
311
|
+
const originalBorder = btnEl.style.borderColor;
|
|
312
|
+
const originalColor = btnEl.style.color;
|
|
313
|
+
btnEl.classList.add('is-copied');
|
|
314
|
+
btnEl.innerHTML = `<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--green);"><path d="M2 8.5L6 12.5L14 4.5"/></svg> <span style="color: var(--green); font-weight: 600;">Copied!</span>`;
|
|
315
|
+
btnEl.style.borderColor = 'var(--green)';
|
|
316
|
+
|
|
317
|
+
setTimeout(() => {
|
|
318
|
+
btnEl.classList.remove('is-copied');
|
|
319
|
+
btnEl.innerHTML = originalHtml;
|
|
320
|
+
btnEl.style.borderColor = originalBorder;
|
|
321
|
+
btnEl.style.color = originalColor;
|
|
322
|
+
}, 1600);
|
|
323
|
+
}
|
|
324
|
+
}).catch((err) => {
|
|
325
|
+
console.error('Clipboard copy error:', err);
|
|
326
|
+
showToast('Failed to copy to clipboard', 'error');
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
261
330
|
// Toast System
|
|
262
331
|
function showToast(message, type = 'success') {
|
|
263
332
|
const container = document.getElementById('toast-container');
|
|
@@ -283,6 +352,67 @@
|
|
|
283
352
|
}, 4000);
|
|
284
353
|
}
|
|
285
354
|
|
|
355
|
+
// Storage Quota & Usage Meter
|
|
356
|
+
function updateStorageUsageMeter(quota = null) {
|
|
357
|
+
const usageBar = document.getElementById('storageUsageBar');
|
|
358
|
+
const filterInfo = document.getElementById('filterInfo');
|
|
359
|
+
|
|
360
|
+
if (quota && quota.limit && Number(quota.limit) > 0) {
|
|
361
|
+
currentStorageQuota = quota;
|
|
362
|
+
const usage = Number(quota.usage || 0);
|
|
363
|
+
const limit = Number(quota.limit);
|
|
364
|
+
const percent = Math.min(100, Math.max(0, Math.round((usage / limit) * 100)));
|
|
365
|
+
|
|
366
|
+
if (usageBar) {
|
|
367
|
+
usageBar.style.width = `${percent}%`;
|
|
368
|
+
usageBar.style.background = percent > 90 ? 'var(--red)' : percent > 75 ? 'var(--orange)' : 'var(--primary)';
|
|
369
|
+
}
|
|
370
|
+
if (filterInfo) {
|
|
371
|
+
filterInfo.textContent = `${formatFileSize(usage)} of ${formatFileSize(limit)} (${percent}%)`;
|
|
372
|
+
filterInfo.title = `Storage: ${formatFileSize(usage)} of ${formatFileSize(limit)}`;
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
const totalBytes = (pageFiles || []).reduce((acc, f) => acc + Number(f.Size || 0), 0);
|
|
376
|
+
if (usageBar) {
|
|
377
|
+
usageBar.style.width = '100%';
|
|
378
|
+
usageBar.style.background = 'var(--primary)';
|
|
379
|
+
}
|
|
380
|
+
if (filterInfo && !currentStorageQuota) {
|
|
381
|
+
filterInfo.textContent = activeBucket ? `${activeBucket} · ${formatFileSize(totalBytes)}` : `${formatFileSize(totalBytes)} loaded`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Dynamic UI Adaptation Based on Provider Capabilities
|
|
387
|
+
function applyCapabilitiesUI() {
|
|
388
|
+
const batchMoveBtn = document.getElementById('batchMoveBtn');
|
|
389
|
+
if (batchMoveBtn) {
|
|
390
|
+
batchMoveBtn.style.display = currentCapabilities.move === false ? 'none' : '';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const newFolderBtn = document.getElementById('new-folder-btn');
|
|
394
|
+
if (newFolderBtn) {
|
|
395
|
+
if (currentCapabilities.hierarchicalFolders === false) {
|
|
396
|
+
newFolderBtn.style.opacity = '0.5';
|
|
397
|
+
newFolderBtn.title = 'Folders not supported by current provider';
|
|
398
|
+
} else {
|
|
399
|
+
newFolderBtn.style.opacity = '';
|
|
400
|
+
newFolderBtn.title = '';
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const searchInput = document.getElementById('searchInput');
|
|
405
|
+
if (searchInput) {
|
|
406
|
+
if (currentCapabilities.search === false) {
|
|
407
|
+
searchInput.placeholder = 'Search not supported';
|
|
408
|
+
searchInput.disabled = true;
|
|
409
|
+
} else {
|
|
410
|
+
searchInput.placeholder = 'Search files…';
|
|
411
|
+
searchInput.disabled = false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
286
416
|
// Live Health Check
|
|
287
417
|
async function checkConnectionHealth(silent = true) {
|
|
288
418
|
const healthBadge = document.getElementById('topHealthBadge');
|
|
@@ -306,6 +436,11 @@
|
|
|
306
436
|
if (healthText) healthText.textContent = 'Healthy';
|
|
307
437
|
}
|
|
308
438
|
if (latencyTag) latencyTag.textContent = `${latency}ms`;
|
|
439
|
+
if (data.storageQuota) {
|
|
440
|
+
updateStorageUsageMeter(data.storageQuota);
|
|
441
|
+
} else {
|
|
442
|
+
updateStorageUsageMeter(null);
|
|
443
|
+
}
|
|
309
444
|
if (!silent) showToast(`Storage connection healthy (${latency}ms)`, 'success');
|
|
310
445
|
} else {
|
|
311
446
|
if (healthBadge) {
|
|
@@ -725,6 +860,11 @@
|
|
|
725
860
|
currentApiPrefix = data.prefix || '';
|
|
726
861
|
pageFiles = data.files || [];
|
|
727
862
|
|
|
863
|
+
if (data.capabilities) {
|
|
864
|
+
currentCapabilities = Object.assign({}, currentCapabilities, data.capabilities);
|
|
865
|
+
applyCapabilitiesUI();
|
|
866
|
+
}
|
|
867
|
+
|
|
728
868
|
if (data.mode === 'optimized') {
|
|
729
869
|
pageFolders = (data.folders || []).map(folder => stripApiPrefixFromPath(folder, currentApiPrefix)).filter(Boolean);
|
|
730
870
|
} else {
|
|
@@ -739,6 +879,7 @@
|
|
|
739
879
|
renderBreadcrumbs();
|
|
740
880
|
renderFiles();
|
|
741
881
|
renderPagination(data);
|
|
882
|
+
updateStorageUsageMeter(currentStorageQuota);
|
|
742
883
|
checkConnectionHealth(true);
|
|
743
884
|
} catch (err) {
|
|
744
885
|
console.error('Error loading files:', err);
|
|
@@ -872,22 +1013,14 @@
|
|
|
872
1013
|
}
|
|
873
1014
|
}
|
|
874
1015
|
|
|
875
|
-
function copyShareLink(key) {
|
|
1016
|
+
function copyShareLink(key, btnEl = null) {
|
|
876
1017
|
const viewUrl = window.location.origin + getViewUrl(key);
|
|
877
|
-
|
|
878
|
-
showToast('Link copied to clipboard', 'success');
|
|
879
|
-
}).catch(() => {
|
|
880
|
-
showToast('Failed to copy link', 'error');
|
|
881
|
-
});
|
|
1018
|
+
copyToClipboard(viewUrl, 'Link copied to clipboard', btnEl);
|
|
882
1019
|
}
|
|
883
1020
|
|
|
884
|
-
function copyPublicLink(key) {
|
|
1021
|
+
function copyPublicLink(key, btnEl = null) {
|
|
885
1022
|
const publicUrl = window.location.origin + getPublicViewUrl(key);
|
|
886
|
-
|
|
887
|
-
showToast('Public link copied to clipboard', 'success');
|
|
888
|
-
}).catch(() => {
|
|
889
|
-
showToast('Failed to copy public link', 'error');
|
|
890
|
-
});
|
|
1023
|
+
copyToClipboard(publicUrl, 'Public link copied to clipboard', btnEl);
|
|
891
1024
|
}
|
|
892
1025
|
|
|
893
1026
|
// Preview Modal
|
|
@@ -962,8 +1095,10 @@
|
|
|
962
1095
|
} else if (isAudio(fileName)) {
|
|
963
1096
|
contentEl.innerHTML = `<div class="p-4 text-center"><audio controls autoplay class="w-100"><source src="${viewUrl}"></audio></div>`;
|
|
964
1097
|
} else if (isPdf(fileName)) {
|
|
965
|
-
contentEl.innerHTML = `<iframe src="${viewUrl}" class="preview-iframe"></iframe>`;
|
|
1098
|
+
contentEl.innerHTML = `<iframe src="${viewUrl}" class="preview-iframe" width="100%" height="100%" frameborder="0" title="${escapeAttr(fileName)}"></iframe>`;
|
|
966
1099
|
} else if (isMarkdown(fileName)) {
|
|
1100
|
+
|
|
1101
|
+
|
|
967
1102
|
fetch(viewUrl).then(res => res.text()).then(text => {
|
|
968
1103
|
contentEl.innerHTML = renderMarkdownToHtml(text);
|
|
969
1104
|
}).catch(err => {
|
|
@@ -990,12 +1125,12 @@
|
|
|
990
1125
|
if (modal) modal.classList.remove('show');
|
|
991
1126
|
};
|
|
992
1127
|
|
|
993
|
-
window.copyCurrentPreviewLink = function () {
|
|
994
|
-
if (currentPreviewKey) copyShareLink(currentPreviewKey);
|
|
1128
|
+
window.copyCurrentPreviewLink = function (btnEl) {
|
|
1129
|
+
if (currentPreviewKey) copyShareLink(currentPreviewKey, btnEl || document.getElementById('previewCopyLinkBtn'));
|
|
995
1130
|
};
|
|
996
1131
|
|
|
997
|
-
window.copyCurrentPreviewPublicLink = function () {
|
|
998
|
-
if (currentPreviewKey) copyPublicLink(currentPreviewKey);
|
|
1132
|
+
window.copyCurrentPreviewPublicLink = function (btnEl) {
|
|
1133
|
+
if (currentPreviewKey) copyPublicLink(currentPreviewKey, btnEl || document.getElementById('previewCopyPublicLinkBtn'));
|
|
999
1134
|
};
|
|
1000
1135
|
|
|
1001
1136
|
// Move & Rename Modal
|
|
@@ -1153,6 +1288,67 @@
|
|
|
1153
1288
|
`).join('');
|
|
1154
1289
|
}
|
|
1155
1290
|
|
|
1291
|
+
async function uploadFileMultipart(file, prefix, onProgress) {
|
|
1292
|
+
const initRes = await fetch(withBucketUrl('/mbkbucket/upload-init'), {
|
|
1293
|
+
method: 'POST',
|
|
1294
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1295
|
+
body: JSON.stringify({
|
|
1296
|
+
fileName: file.name,
|
|
1297
|
+
prefix: prefix,
|
|
1298
|
+
contentType: file.type || 'application/octet-stream'
|
|
1299
|
+
})
|
|
1300
|
+
});
|
|
1301
|
+
const initData = await initRes.json();
|
|
1302
|
+
if (!initData.success) throw new Error(initData.error || 'Failed to initialize multipart upload');
|
|
1303
|
+
|
|
1304
|
+
const { uploadId, key } = initData;
|
|
1305
|
+
const chunkSize = 5 * 1024 * 1024;
|
|
1306
|
+
const totalParts = Math.ceil(file.size / chunkSize);
|
|
1307
|
+
const parts = [];
|
|
1308
|
+
|
|
1309
|
+
try {
|
|
1310
|
+
for (let partNumber = 1; partNumber <= totalParts; partNumber++) {
|
|
1311
|
+
const start = (partNumber - 1) * chunkSize;
|
|
1312
|
+
const end = Math.min(start + chunkSize, file.size);
|
|
1313
|
+
const chunkBlob = file.slice(start, end);
|
|
1314
|
+
|
|
1315
|
+
const chunkForm = new FormData();
|
|
1316
|
+
chunkForm.append('uploadId', uploadId);
|
|
1317
|
+
chunkForm.append('key', key);
|
|
1318
|
+
chunkForm.append('partNumber', String(partNumber));
|
|
1319
|
+
chunkForm.append('chunk', chunkBlob, file.name);
|
|
1320
|
+
|
|
1321
|
+
const chunkRes = await fetch(withBucketUrl('/mbkbucket/upload-chunk'), {
|
|
1322
|
+
method: 'POST',
|
|
1323
|
+
body: chunkForm
|
|
1324
|
+
});
|
|
1325
|
+
const chunkData = await chunkRes.json();
|
|
1326
|
+
if (!chunkData.success) throw new Error(chunkData.error || `Failed chunk ${partNumber}`);
|
|
1327
|
+
|
|
1328
|
+
parts.push({ PartNumber: partNumber, ETag: chunkData.etag || chunkData.ETag });
|
|
1329
|
+
if (typeof onProgress === 'function') {
|
|
1330
|
+
onProgress(partNumber / totalParts);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const completeRes = await fetch(withBucketUrl('/mbkbucket/upload-complete'), {
|
|
1335
|
+
method: 'POST',
|
|
1336
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1337
|
+
body: JSON.stringify({ uploadId, key, parts })
|
|
1338
|
+
});
|
|
1339
|
+
const completeData = await completeRes.json();
|
|
1340
|
+
if (!completeData.success) throw new Error(completeData.error || 'Failed to complete multipart upload');
|
|
1341
|
+
return completeData;
|
|
1342
|
+
} catch (uploadErr) {
|
|
1343
|
+
fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
1344
|
+
method: 'POST',
|
|
1345
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1346
|
+
body: JSON.stringify({ uploadId, key })
|
|
1347
|
+
}).catch(() => {});
|
|
1348
|
+
throw uploadErr;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1156
1352
|
async function processUploads() {
|
|
1157
1353
|
if (isUploading || uploadQueue.length === 0) return;
|
|
1158
1354
|
isUploading = true;
|
|
@@ -1173,18 +1369,28 @@
|
|
|
1173
1369
|
const file = uploadQueue[i];
|
|
1174
1370
|
if (statusLabel) statusLabel.textContent = `Uploading ${file.name} (${i + 1}/${totalFiles})...`;
|
|
1175
1371
|
|
|
1176
|
-
const formData = new FormData();
|
|
1177
|
-
formData.append('file', file);
|
|
1178
|
-
formData.append('prefix', currentPrefix);
|
|
1179
|
-
|
|
1180
1372
|
try {
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1373
|
+
const useMultipart = currentCapabilities.multipart && file.size > 5 * 1024 * 1024;
|
|
1374
|
+
if (useMultipart) {
|
|
1375
|
+
await uploadFileMultipart(file, currentPrefix, (fraction) => {
|
|
1376
|
+
const overallPercent = Math.round(((i + fraction) / totalFiles) * 100);
|
|
1377
|
+
if (progressBar) progressBar.style.width = `${overallPercent}%`;
|
|
1378
|
+
if (percentEl) percentEl.textContent = `${overallPercent}%`;
|
|
1379
|
+
});
|
|
1380
|
+
completed++;
|
|
1381
|
+
} else {
|
|
1382
|
+
const formData = new FormData();
|
|
1383
|
+
formData.append('file', file);
|
|
1384
|
+
formData.append('prefix', currentPrefix);
|
|
1385
|
+
|
|
1386
|
+
const response = await fetch(withBucketUrl('/mbkbucket/upload'), {
|
|
1387
|
+
method: 'POST',
|
|
1388
|
+
body: formData
|
|
1389
|
+
});
|
|
1390
|
+
const result = await response.json();
|
|
1391
|
+
if (!result.success) throw new Error(result.error || 'Upload failed');
|
|
1392
|
+
completed++;
|
|
1393
|
+
}
|
|
1188
1394
|
} catch (err) {
|
|
1189
1395
|
showToast(`Failed to upload ${file.name}: ${err.message}`, 'error');
|
|
1190
1396
|
}
|
|
@@ -1405,9 +1611,25 @@
|
|
|
1405
1611
|
});
|
|
1406
1612
|
}
|
|
1407
1613
|
|
|
1614
|
+
if (currentCapabilities.move !== false) {
|
|
1615
|
+
menuItems.push({
|
|
1616
|
+
id: 'move',
|
|
1617
|
+
label: 'Rename / Move',
|
|
1618
|
+
icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M11 2L14 5L11 8M14 5H5C3.34315 5 2 6.34315 2 8V14"/></svg>',
|
|
1619
|
+
action: () => promptMove(fileKey, false)
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
if (currentCapabilities.copy !== false) {
|
|
1624
|
+
menuItems.push({
|
|
1625
|
+
id: 'copy',
|
|
1626
|
+
label: 'Duplicate / Copy',
|
|
1627
|
+
icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M3 11V3a1.5 1.5 0 011.5-1.5h8"/></svg>',
|
|
1628
|
+
action: () => promptMove(fileKey, true)
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1408
1632
|
menuItems.push(
|
|
1409
|
-
{ id: 'move', label: 'Rename / Move', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M11 2L14 5L11 8M14 5H5C3.34315 5 2 6.34315 2 8V14"/></svg>', action: () => promptMove(fileKey, false) },
|
|
1410
|
-
{ id: 'copy', label: 'Duplicate / Copy', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M3 11V3a1.5 1.5 0 011.5-1.5h8"/></svg>', action: () => promptMove(fileKey, true) },
|
|
1411
1633
|
{ id: 'star', label: isStarred ? 'Unstar' : 'Star', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1l2 5 5 .5-4 3.5 1 5-4-2.5-4 2.5 1-5-4-3.5 5-.5z"/></svg>', action: () => toggleStar(fileKey) },
|
|
1412
1634
|
'-',
|
|
1413
1635
|
{ id: 'delete', label: 'Delete', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>', danger: true, action: () => promptDelete(fileKey, false) }
|
|
@@ -13,11 +13,6 @@
|
|
|
13
13
|
<title>Helper Utils Demo — MBKBucket</title>
|
|
14
14
|
<meta name="description" content="Interactive demo of MBKBucket frontend helper utilities: gallery picker, file input, upload widget, multi-select, live preview binding, and API helpers.">
|
|
15
15
|
|
|
16
|
-
<!-- Fonts (same as bucket.hbs) -->
|
|
17
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
18
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
19
|
-
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
20
|
-
|
|
21
16
|
<!-- MBKBucket design system + helper styles -->
|
|
22
17
|
<link rel="stylesheet" href="/mbkbucket/bucketadmin.css">
|
|
23
18
|
<link rel="stylesheet" href="/mbkbucket/mbkbucket-helper.css">
|
|
@@ -834,14 +829,12 @@ uploader.<span class="fn">clear</span>();</pre>
|
|
|
834
829
|
<span class="chip">MBKBucket.url.*</span>
|
|
835
830
|
<span class="chip">listFiles</span>
|
|
836
831
|
<span class="chip">uploadFile</span>
|
|
837
|
-
<span class="chip">getSignedUrl</span>
|
|
838
832
|
</div>
|
|
839
833
|
</div>
|
|
840
834
|
|
|
841
835
|
<div class="demo-tabs">
|
|
842
836
|
<div class="demo-tab active" data-tab="urls" data-group="api">URL Builders</div>
|
|
843
837
|
<div class="demo-tab" data-tab="list" data-group="api">List Files</div>
|
|
844
|
-
<div class="demo-tab" data-tab="signed" data-group="api">Signed URL</div>
|
|
845
838
|
<div class="demo-tab" data-tab="code" data-group="api">Code Reference</div>
|
|
846
839
|
</div>
|
|
847
840
|
|
|
@@ -899,30 +892,6 @@ uploader.<span class="fn">clear</span>();</pre>
|
|
|
899
892
|
</div>
|
|
900
893
|
</div>
|
|
901
894
|
|
|
902
|
-
<!-- Signed URL -->
|
|
903
|
-
<div class="demo-pane" data-pane="api-signed">
|
|
904
|
-
<div class="form-row">
|
|
905
|
-
<label class="form-label" for="api-signed-key">File key:</label>
|
|
906
|
-
<div class="flex-row">
|
|
907
|
-
<input id="api-signed-key"
|
|
908
|
-
class="demo-input"
|
|
909
|
-
type="text"
|
|
910
|
-
style="max-width:320px"
|
|
911
|
-
placeholder="e.g. images/photo.jpg">
|
|
912
|
-
<button class="btn btn-primary btn-sm" onclick="demoSignedUrl()">Get URL</button>
|
|
913
|
-
</div>
|
|
914
|
-
<div class="form-hint">Calls <code class="inline-code">MBKBucket.api.getSignedUrl(key, { expiresIn: 3600 })</code></div>
|
|
915
|
-
</div>
|
|
916
|
-
|
|
917
|
-
<div class="api-result" id="api-signed-result" style="display:none">
|
|
918
|
-
<div class="api-result-header">
|
|
919
|
-
<div class="api-dot" id="api-signed-dot"></div>
|
|
920
|
-
<span id="api-signed-status">—</span>
|
|
921
|
-
</div>
|
|
922
|
-
<pre id="api-signed-pre"></pre>
|
|
923
|
-
</div>
|
|
924
|
-
</div>
|
|
925
|
-
|
|
926
895
|
<!-- Code reference -->
|
|
927
896
|
<div class="code-pane" data-pane="api-code">
|
|
928
897
|
<pre class="code-block"><span class="cm">// URL helpers</span>
|
|
@@ -1132,33 +1101,6 @@ MBKBucket.toast.<span class="fn">info</span>(<span class="str">'Loading…'</spa
|
|
|
1132
1101
|
pre.textContent = JSON.stringify({ error: err.message, status: err.status }, null, 2);
|
|
1133
1102
|
}
|
|
1134
1103
|
}
|
|
1135
|
-
|
|
1136
|
-
// ── 4. API: signed URL ───────────────────────────────────────
|
|
1137
|
-
async function demoSignedUrl() {
|
|
1138
|
-
const key = document.getElementById('api-signed-key').value.trim();
|
|
1139
|
-
if (!key) { MBKBucket.toast.warning('Enter a file key first'); return; }
|
|
1140
|
-
|
|
1141
|
-
const result = document.getElementById('api-signed-result');
|
|
1142
|
-
const dot = document.getElementById('api-signed-dot');
|
|
1143
|
-
const status = document.getElementById('api-signed-status');
|
|
1144
|
-
const pre = document.getElementById('api-signed-pre');
|
|
1145
|
-
|
|
1146
|
-
result.style.display = '';
|
|
1147
|
-
dot.className = 'api-dot';
|
|
1148
|
-
status.textContent = 'Loading…';
|
|
1149
|
-
pre.textContent = '';
|
|
1150
|
-
|
|
1151
|
-
try {
|
|
1152
|
-
const data = await MBKBucket.api.getSignedUrl(key, { expiresIn: 3600 });
|
|
1153
|
-
dot.className = 'api-dot ok';
|
|
1154
|
-
status.textContent = `200 OK — expires in ${data.expiresIn}s`;
|
|
1155
|
-
pre.textContent = JSON.stringify(data, null, 2);
|
|
1156
|
-
} catch (err) {
|
|
1157
|
-
dot.className = 'api-dot err';
|
|
1158
|
-
status.textContent = `Error: ${err.message}`;
|
|
1159
|
-
pre.textContent = JSON.stringify({ error: err.message }, null, 2);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
1104
|
</script>
|
|
1163
1105
|
</body>
|
|
1164
1106
|
</html>
|
|
@@ -330,17 +330,9 @@
|
|
|
330
330
|
}
|
|
331
331
|
return this.uploadSingle(prefix, file, opts);
|
|
332
332
|
},
|
|
333
|
-
|
|
334
|
-
getSignedUrl(key, opts) {
|
|
335
|
-
opts = opts || {};
|
|
336
|
-
return apiFetch(buildUrl(`${BASE}/api/signed-url`, {
|
|
337
|
-
key,
|
|
338
|
-
bucket: opts.bucket || '',
|
|
339
|
-
expiresIn: opts.expiresIn || 3600,
|
|
340
|
-
}));
|
|
341
|
-
},
|
|
342
333
|
};
|
|
343
334
|
|
|
335
|
+
|
|
344
336
|
// ─────────────────────────────────────────────────────────────────────────
|
|
345
337
|
// Gallery Modal (with In-Modal Upload & Multi-Select)
|
|
346
338
|
// ─────────────────────────────────────────────────────────────────────────
|
package/lib/src/app.js
CHANGED
|
@@ -7,17 +7,15 @@
|
|
|
7
7
|
|
|
8
8
|
import express from "express";
|
|
9
9
|
import path from "path";
|
|
10
|
-
import fs from "fs";
|
|
11
10
|
import { fileURLToPath } from "url";
|
|
12
|
-
import { engine } from "express-handlebars";
|
|
13
11
|
import compression from "compression";
|
|
14
12
|
import mbkautheRoutes from "mbkauthe";
|
|
13
|
+
import { setupViewEngine } from "mbkutils";
|
|
15
14
|
import { generalLimiter } from "./middleware/rate-limiter.js";
|
|
16
15
|
import { notFoundHandler, globalErrorHandler } from "./middleware/error-handler.js";
|
|
17
16
|
import bucketRoutes from "./routes/index.js";
|
|
18
17
|
import { packageJson } from "./config/index.js";
|
|
19
18
|
import { createLogger } from "#logger";
|
|
20
|
-
import { commonHandlebarsHelpers } from "./utils/helpers.js";
|
|
21
19
|
|
|
22
20
|
const debugServer = createLogger('server');
|
|
23
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -50,41 +48,29 @@ export function createApp() {
|
|
|
50
48
|
const libDir = path.resolve(__dirname, "..");
|
|
51
49
|
const cwd = process.cwd();
|
|
52
50
|
|
|
53
|
-
const
|
|
54
|
-
path.join(rootDir, "views"),
|
|
51
|
+
const additionalViewsDirs = [
|
|
55
52
|
path.join(libDir, "views"),
|
|
56
|
-
path.join(rootDir, "node_modules", "mbkauthe", "views"),
|
|
57
53
|
path.resolve(cwd, "views"),
|
|
58
|
-
|
|
59
|
-
].filter(dir => fs.existsSync(dir));
|
|
54
|
+
];
|
|
60
55
|
|
|
61
|
-
const
|
|
62
|
-
path.join(rootDir, "views", "templates"),
|
|
63
|
-
path.join(rootDir, "views", "templates", "notice"),
|
|
64
|
-
path.join(rootDir, "views"),
|
|
56
|
+
const additionalPartialsDirs = [
|
|
65
57
|
path.join(rootDir, "views", "partial"),
|
|
66
58
|
path.join(libDir, "views", "templates"),
|
|
67
59
|
path.join(libDir, "views", "templates", "notice"),
|
|
68
60
|
path.join(libDir, "views"),
|
|
69
61
|
path.join(libDir, "views", "partial"),
|
|
70
|
-
|
|
71
|
-
path.resolve(cwd, "node_modules", "mbkauthe", "views"),
|
|
72
|
-
].filter(dir => fs.existsSync(dir));
|
|
62
|
+
];
|
|
73
63
|
|
|
74
64
|
// Handlebars template engine
|
|
75
|
-
server
|
|
76
|
-
|
|
65
|
+
setupViewEngine(server, {
|
|
66
|
+
rootDir,
|
|
77
67
|
defaultLayout: false,
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
additionalViewsDirs,
|
|
69
|
+
additionalPartialsDirs,
|
|
80
70
|
helpers: {
|
|
81
|
-
...commonHandlebarsHelpers,
|
|
82
71
|
mbkbucket_cachebuster: () => `?v=${packageJson.version}`
|
|
83
72
|
}
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
server.set("view engine", "hbs");
|
|
87
|
-
server.set("views", [...new Set(viewDirs)]);
|
|
73
|
+
});
|
|
88
74
|
|
|
89
75
|
server.use(mbkautheRoutes);
|
|
90
76
|
server.use(generalLimiter);
|
|
@@ -63,8 +63,12 @@ export function parseAndValidateBucketConnection(rawValue) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const providerType = String(config.type || config.provider || '').toLowerCase();
|
|
66
|
+
const isLocal = providerType === 'local' || providerType === 'fs' || providerType === 'filesystem' || Boolean(config.basePath || config.rootPath || config.directory);
|
|
66
67
|
|
|
67
|
-
if (
|
|
68
|
+
if (isLocal) {
|
|
69
|
+
// Local filesystem provider needs no cloud credentials, basePath is optional (defaults to ./mbkbucket_storage)
|
|
70
|
+
continue;
|
|
71
|
+
} else if (providerType === 'gdrive' || providerType === 'google-drive' || providerType === 'googledrive') {
|
|
68
72
|
const hasOAuth = Boolean(config.client_id && config.client_secret && (config.refresh_token || config.access_token || config.code));
|
|
69
73
|
const hasServiceAccount = Boolean((config.client_email || config.service_account_email) && (config.private_key || config.service_account_key));
|
|
70
74
|
const hasAccessTokenOnly = Boolean(config.access_token);
|
|
@@ -80,6 +84,7 @@ export function parseAndValidateBucketConnection(rawValue) {
|
|
|
80
84
|
throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
|
|
81
85
|
}
|
|
82
86
|
}
|
|
87
|
+
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
return parsed;
|