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
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
let currentOrder = 'asc';
|
|
21
21
|
let currentViewMode = localStorage.getItem('mbk_view_mode') || 'grid'; // 'grid' or 'list'
|
|
22
22
|
let currentQuickFilter = 'all'; // 'all', 'media', 'docs', 'code', 'starred'
|
|
23
|
-
let showThumbnails = localStorage.getItem('mbk_show_thumbnails') === 'true'; // Fast Mode by default
|
|
23
|
+
let showThumbnails = localStorage.getItem('mbk_show_thumbnails') === 'true'; // Fast Mode by default
|
|
24
24
|
let thumbObserver = null;
|
|
25
25
|
let pageFiles = [];
|
|
26
26
|
let pageFolders = [];
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
let currentNextToken = null;
|
|
29
29
|
let selectedFiles = new Set();
|
|
30
30
|
let starredFiles = new Set(JSON.parse(localStorage.getItem('mbk_starred_keys') || '[]'));
|
|
31
|
+
let currentPreviewKey = '';
|
|
31
32
|
|
|
32
33
|
// Upload State
|
|
33
34
|
let uploadQueue = [];
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
// DOM Elements & Config
|
|
37
38
|
const bucketAppData = document.getElementById('bucketAppData');
|
|
38
39
|
const bucketAppName = bucketAppData?.dataset.appName || '';
|
|
40
|
+
const isPublicViewEnabled = bucketAppData?.dataset.publicViewEnabled === 'true';
|
|
39
41
|
const urlParams = new URLSearchParams(window.location.search);
|
|
40
42
|
const selectedBucket = urlParams.get('bucket') || '';
|
|
41
43
|
let activeBucket = selectedBucket;
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
// File categories & extensions
|
|
47
49
|
const MEDIA_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
48
50
|
const DOC_EXTS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv', 'rtf']);
|
|
49
|
-
const CODE_EXTS = new Set(['js', 'ts', 'jsx', 'tsx', 'html', 'htm', 'css', 'scss', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'json', 'xml', 'log']);
|
|
51
|
+
const CODE_EXTS = new Set(['js', 'ts', 'jsx', 'tsx', 'html', 'htm', 'css', 'scss', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'json', 'xml', 'log', 'env']);
|
|
50
52
|
|
|
51
53
|
const FILE_TYPES = {
|
|
52
54
|
pdf: { color: 'var(--red)', bg: 'var(--red-lt)', label: 'PDF' },
|
|
@@ -85,6 +87,8 @@
|
|
|
85
87
|
media: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="12" height="12" rx="2"/><circle cx="5.5" cy="5.5" r="1.5"/><path d="M14 10l-4-4-6 6"/></svg>',
|
|
86
88
|
docs: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h7l4 4v8a1 1 0 01-1 1H3a1 1 0 01-1-1V3a1 1 0 011-1z"/><path d="M10 2v4h4"/></svg>',
|
|
87
89
|
code: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 5L2 8l3 3M11 5l3 3-3 3M9 3L7 13"/></svg>',
|
|
90
|
+
upload: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M11 6L8 3 5 6"/><line x1="8" y1="3" x2="8" y2="11"/></svg>',
|
|
91
|
+
download: '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M5 8l3 3 3-3"/><line x1="8" y1="2" x2="8" y2="11"/></svg>',
|
|
88
92
|
more: '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="3" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="13" r="1.2"/></svg>'
|
|
89
93
|
};
|
|
90
94
|
|
|
@@ -121,7 +125,7 @@
|
|
|
121
125
|
if (!dateStr) return '—';
|
|
122
126
|
const d = new Date(dateStr);
|
|
123
127
|
if (isNaN(d.getTime())) return String(dateStr);
|
|
124
|
-
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
128
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
function withBucketUrl(url) {
|
|
@@ -134,6 +138,10 @@
|
|
|
134
138
|
return withBucketUrl(`/mbkbucket/view/${encodeURIComponent(key)}`);
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
function getPublicViewUrl(key) {
|
|
142
|
+
return withBucketUrl(`/mbkbucket/p_view/${encodeURIComponent(key)}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
137
145
|
function getDownloadUrl(key) {
|
|
138
146
|
return withBucketUrl(`/mbkbucket/download/${encodeURIComponent(key)}`);
|
|
139
147
|
}
|
|
@@ -158,6 +166,10 @@
|
|
|
158
166
|
return CODE_EXTS.has(ext) || ['txt', 'md', 'csv', 'log', 'json', 'xml', 'env'].includes(ext);
|
|
159
167
|
}
|
|
160
168
|
|
|
169
|
+
function isMarkdown(fileName) {
|
|
170
|
+
return getFileExt(fileName) === 'md';
|
|
171
|
+
}
|
|
172
|
+
|
|
161
173
|
function isPdf(fileName) {
|
|
162
174
|
return getFileExt(fileName) === 'pdf';
|
|
163
175
|
}
|
|
@@ -208,6 +220,22 @@
|
|
|
208
220
|
return Array.from(folderSet).sort();
|
|
209
221
|
}
|
|
210
222
|
|
|
223
|
+
function renderMarkdownToHtml(mdText) {
|
|
224
|
+
let html = escapeHtml(mdText);
|
|
225
|
+
html = html.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => `<pre><code>${code}</code></pre>`);
|
|
226
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
227
|
+
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
|
228
|
+
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
|
|
229
|
+
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
|
|
230
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
231
|
+
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
232
|
+
html = html.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>');
|
|
233
|
+
html = html.replace(/^\s*[\-\*] (.*$)/gim, '<li>$1</li>');
|
|
234
|
+
html = html.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>');
|
|
235
|
+
html = html.replace(/\n\n/g, '<br><br>');
|
|
236
|
+
return `<div class="rendered-markdown">${html}</div>`;
|
|
237
|
+
}
|
|
238
|
+
|
|
211
239
|
function syncStateUrl() {
|
|
212
240
|
try {
|
|
213
241
|
const parts = [];
|
|
@@ -255,6 +283,70 @@
|
|
|
255
283
|
}, 4000);
|
|
256
284
|
}
|
|
257
285
|
|
|
286
|
+
// Live Health Check
|
|
287
|
+
async function checkConnectionHealth(silent = true) {
|
|
288
|
+
const healthBadge = document.getElementById('topHealthBadge');
|
|
289
|
+
const healthText = document.getElementById('topHealthText');
|
|
290
|
+
const latencyTag = document.getElementById('storageLatency');
|
|
291
|
+
|
|
292
|
+
if (healthBadge) {
|
|
293
|
+
healthBadge.className = 'health-status-badge checking';
|
|
294
|
+
if (healthText) healthText.textContent = 'Checking...';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const startTime = Date.now();
|
|
298
|
+
try {
|
|
299
|
+
const response = await fetch(withBucketUrl('/mbkbucket/api/health'));
|
|
300
|
+
const data = await response.json();
|
|
301
|
+
const latency = data.responseTime || (Date.now() - startTime);
|
|
302
|
+
|
|
303
|
+
if (data.status === 'healthy' || data.success) {
|
|
304
|
+
if (healthBadge) {
|
|
305
|
+
healthBadge.className = 'health-status-badge healthy';
|
|
306
|
+
if (healthText) healthText.textContent = 'Healthy';
|
|
307
|
+
}
|
|
308
|
+
if (latencyTag) latencyTag.textContent = `${latency}ms`;
|
|
309
|
+
if (!silent) showToast(`Storage connection healthy (${latency}ms)`, 'success');
|
|
310
|
+
} else {
|
|
311
|
+
if (healthBadge) {
|
|
312
|
+
healthBadge.className = 'health-status-badge unhealthy';
|
|
313
|
+
if (healthText) healthText.textContent = 'Unhealthy';
|
|
314
|
+
}
|
|
315
|
+
if (latencyTag) latencyTag.textContent = 'err';
|
|
316
|
+
if (!silent) showToast(`Connection error: ${data.error || 'Check configuration'}`, 'error');
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (healthBadge) {
|
|
320
|
+
healthBadge.className = 'health-status-badge unhealthy';
|
|
321
|
+
if (healthText) healthText.textContent = 'Offline';
|
|
322
|
+
}
|
|
323
|
+
if (latencyTag) latencyTag.textContent = 'offline';
|
|
324
|
+
if (!silent) showToast(`Connection test failed: ${err.message}`, 'error');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Batch Selection System
|
|
329
|
+
function updateBatchBar() {
|
|
330
|
+
const bar = document.getElementById('batchActionBar');
|
|
331
|
+
const countEl = document.getElementById('batchSelectedCount');
|
|
332
|
+
if (!bar) return;
|
|
333
|
+
|
|
334
|
+
const count = selectedFiles.size;
|
|
335
|
+
if (count > 0) {
|
|
336
|
+
bar.style.display = 'block';
|
|
337
|
+
if (countEl) countEl.textContent = count;
|
|
338
|
+
} else {
|
|
339
|
+
bar.style.display = 'none';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function clearBatchSelection() {
|
|
344
|
+
selectedFiles.clear();
|
|
345
|
+
document.querySelectorAll('.file-select-cb').forEach(cb => { cb.checked = false; });
|
|
346
|
+
document.querySelectorAll('.fm-item.is-selected').forEach(item => { item.classList.remove('is-selected'); });
|
|
347
|
+
updateBatchBar();
|
|
348
|
+
}
|
|
349
|
+
|
|
258
350
|
// Tree Renderer
|
|
259
351
|
function renderTree() {
|
|
260
352
|
const treeEl = document.getElementById('fm-tree');
|
|
@@ -366,13 +458,11 @@
|
|
|
366
458
|
function getFilteredFiles() {
|
|
367
459
|
let list = (pageFiles || []).filter(f => !String(f.Key || '').endsWith('/'));
|
|
368
460
|
|
|
369
|
-
// Search filter
|
|
370
461
|
if (currentSearch) {
|
|
371
462
|
const q = currentSearch.toLowerCase();
|
|
372
463
|
list = list.filter(f => getBaseName(f.Key).toLowerCase().includes(q));
|
|
373
464
|
}
|
|
374
465
|
|
|
375
|
-
// Quick filter
|
|
376
466
|
if (currentQuickFilter === 'media') {
|
|
377
467
|
list = list.filter(f => MEDIA_EXTS.has(getFileExt(f.Key)));
|
|
378
468
|
} else if (currentQuickFilter === 'docs') {
|
|
@@ -404,7 +494,6 @@
|
|
|
404
494
|
const files = getFilteredFiles();
|
|
405
495
|
const folders = getFilteredFolders();
|
|
406
496
|
|
|
407
|
-
// Update stats
|
|
408
497
|
const totalCountEl = document.getElementById('totalFilesCount');
|
|
409
498
|
const filterInfoEl = document.getElementById('filterInfo');
|
|
410
499
|
const totalItems = files.length + folders.length;
|
|
@@ -433,11 +522,10 @@
|
|
|
433
522
|
|
|
434
523
|
let html = '';
|
|
435
524
|
|
|
436
|
-
// If List view, add header row
|
|
437
525
|
if (currentViewMode === 'list') {
|
|
438
526
|
html += `
|
|
439
527
|
<div class="fm-list-header">
|
|
440
|
-
<div></div>
|
|
528
|
+
<div><input type="checkbox" id="selectAllCb" title="Select All"></div>
|
|
441
529
|
<div></div>
|
|
442
530
|
<div>Name</div>
|
|
443
531
|
<div>Size</div>
|
|
@@ -499,6 +587,7 @@
|
|
|
499
587
|
const safeKey = escapeAttr(rawKey);
|
|
500
588
|
const safeName = escapeHtml(fileName);
|
|
501
589
|
const isStarred = starredFiles.has(rawKey);
|
|
590
|
+
const isSelected = selectedFiles.has(rawKey);
|
|
502
591
|
const isImg = isImage(fileName);
|
|
503
592
|
const typeCfg = getFileTypeConfig(fileName);
|
|
504
593
|
const formattedSize = file.Size ? formatFileSize(file.Size) : '—';
|
|
@@ -512,7 +601,10 @@
|
|
|
512
601
|
|
|
513
602
|
if (currentViewMode === 'grid') {
|
|
514
603
|
html += `
|
|
515
|
-
<div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
604
|
+
<div class="fm-item ${isStarred ? 'starred' : ''} ${isSelected ? 'is-selected' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
605
|
+
<div class="fm-item-checkbox-grid">
|
|
606
|
+
<input type="checkbox" class="file-select-cb" data-key="${safeKey}" ${isSelected ? 'checked' : ''} title="Select">
|
|
607
|
+
</div>
|
|
516
608
|
<div class="fm-item-icon" style="${showImgThumb ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
517
609
|
${iconInner}
|
|
518
610
|
</div>
|
|
@@ -530,9 +622,9 @@
|
|
|
530
622
|
`;
|
|
531
623
|
} else {
|
|
532
624
|
html += `
|
|
533
|
-
<div class="fm-item ${isStarred ? 'starred' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
625
|
+
<div class="fm-item ${isStarred ? 'starred' : ''} ${isSelected ? 'is-selected' : ''}" data-type="file" data-key="${safeKey}" tabindex="0">
|
|
534
626
|
<div class="fm-item-checkbox">
|
|
535
|
-
<input type="checkbox" class="file-select-cb" data-key="${safeKey}">
|
|
627
|
+
<input type="checkbox" class="file-select-cb" data-key="${safeKey}" ${isSelected ? 'checked' : ''} title="Select">
|
|
536
628
|
</div>
|
|
537
629
|
<div class="fm-item-icon" style="${isImg ? '' : `background:${typeCfg.bg};color:${typeCfg.color}`}">
|
|
538
630
|
<span class="ext">${typeCfg.label}</span>
|
|
@@ -553,6 +645,7 @@
|
|
|
553
645
|
|
|
554
646
|
gridEl.innerHTML = html;
|
|
555
647
|
observeThumbnails();
|
|
648
|
+
updateBatchBar();
|
|
556
649
|
}
|
|
557
650
|
|
|
558
651
|
function observeThumbnails() {
|
|
@@ -646,6 +739,7 @@
|
|
|
646
739
|
renderBreadcrumbs();
|
|
647
740
|
renderFiles();
|
|
648
741
|
renderPagination(data);
|
|
742
|
+
checkConnectionHealth(true);
|
|
649
743
|
} catch (err) {
|
|
650
744
|
console.error('Error loading files:', err);
|
|
651
745
|
showToast(err.message || 'Error loading files', 'error');
|
|
@@ -717,8 +811,8 @@
|
|
|
717
811
|
|
|
718
812
|
menu.style.display = 'block';
|
|
719
813
|
|
|
720
|
-
const menuWidth =
|
|
721
|
-
const menuHeight = menu.offsetHeight ||
|
|
814
|
+
const menuWidth = 190;
|
|
815
|
+
const menuHeight = menu.offsetHeight || 160;
|
|
722
816
|
const posX = Math.min(x, window.innerWidth - menuWidth - 10);
|
|
723
817
|
const posY = Math.min(y, window.innerHeight - menuHeight - 10);
|
|
724
818
|
|
|
@@ -743,7 +837,7 @@
|
|
|
743
837
|
if (menu) menu.style.display = 'none';
|
|
744
838
|
}
|
|
745
839
|
|
|
746
|
-
// Actions: Star, Download, Preview, Delete, New Folder
|
|
840
|
+
// Actions: Star, Download, Preview, Delete, New Folder, Move
|
|
747
841
|
function toggleStar(key) {
|
|
748
842
|
if (starredFiles.has(key)) {
|
|
749
843
|
starredFiles.delete(key);
|
|
@@ -787,8 +881,18 @@
|
|
|
787
881
|
});
|
|
788
882
|
}
|
|
789
883
|
|
|
884
|
+
function copyPublicLink(key) {
|
|
885
|
+
const publicUrl = window.location.origin + getPublicViewUrl(key);
|
|
886
|
+
navigator.clipboard.writeText(publicUrl).then(() => {
|
|
887
|
+
showToast('Public link copied to clipboard', 'success');
|
|
888
|
+
}).catch(() => {
|
|
889
|
+
showToast('Failed to copy public link', 'error');
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
790
893
|
// Preview Modal
|
|
791
|
-
function openPreview(key) {
|
|
894
|
+
async function openPreview(key) {
|
|
895
|
+
currentPreviewKey = key;
|
|
792
896
|
const modal = document.getElementById('previewModal');
|
|
793
897
|
const titleEl = document.getElementById('previewFileName');
|
|
794
898
|
const contentEl = document.getElementById('previewContent');
|
|
@@ -797,6 +901,13 @@
|
|
|
797
901
|
|
|
798
902
|
if (!modal || !contentEl) return;
|
|
799
903
|
|
|
904
|
+
// Reset tabs
|
|
905
|
+
document.querySelectorAll('.preview-tab').forEach(t => t.classList.toggle('active', t.dataset.previewTab === 'preview'));
|
|
906
|
+
const panePreview = document.getElementById('pane-preview');
|
|
907
|
+
const paneInfo = document.getElementById('pane-info');
|
|
908
|
+
if (panePreview) panePreview.style.display = 'block';
|
|
909
|
+
if (paneInfo) paneInfo.style.display = 'none';
|
|
910
|
+
|
|
800
911
|
const fileName = getBaseName(key);
|
|
801
912
|
const viewUrl = getViewUrl(key);
|
|
802
913
|
const downloadUrl = getDownloadUrl(key);
|
|
@@ -808,6 +919,42 @@
|
|
|
808
919
|
contentEl.innerHTML = '<div class="empty-preview"><div class="spinner-border text-light"></div><div class="mt-2 text-light">Loading preview...</div></div>';
|
|
809
920
|
modal.classList.add('show');
|
|
810
921
|
|
|
922
|
+
// Populate file info tab
|
|
923
|
+
const fileInfoKey = document.getElementById('infoFileKey');
|
|
924
|
+
const fileInfoSize = document.getElementById('infoFileSize');
|
|
925
|
+
const fileInfoMime = document.getElementById('infoMimeType');
|
|
926
|
+
const fileInfoDate = document.getElementById('infoLastModified');
|
|
927
|
+
const fileInfoETag = document.getElementById('infoETag');
|
|
928
|
+
|
|
929
|
+
if (fileInfoKey) fileInfoKey.textContent = key;
|
|
930
|
+
|
|
931
|
+
// Async fetch full metadata
|
|
932
|
+
fetch(withBucketUrl(`/mbkbucket/api/file-info/${encodeURIComponent(key)}`))
|
|
933
|
+
.then(r => r.json())
|
|
934
|
+
.then(info => {
|
|
935
|
+
if (fileInfoSize) fileInfoSize.textContent = info.ContentLength ? formatFileSize(info.ContentLength) : '—';
|
|
936
|
+
if (fileInfoMime) fileInfoMime.textContent = info.ContentType || '—';
|
|
937
|
+
if (fileInfoDate) fileInfoDate.textContent = info.LastModified ? formatDate(info.LastModified) : '—';
|
|
938
|
+
if (fileInfoETag) fileInfoETag.textContent = info.ETag || '—';
|
|
939
|
+
}).catch(() => {});
|
|
940
|
+
|
|
941
|
+
// Configure public link button and metadata card
|
|
942
|
+
const copyPublicBtn = document.getElementById('previewCopyPublicLinkBtn');
|
|
943
|
+
if (copyPublicBtn) {
|
|
944
|
+
copyPublicBtn.style.display = isPublicViewEnabled ? '' : 'none';
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const publicUrl = window.location.origin + getPublicViewUrl(key);
|
|
948
|
+
const infoPublicLinkCard = document.getElementById('infoPublicLinkCard');
|
|
949
|
+
const infoPublicUrl = document.getElementById('infoPublicUrl');
|
|
950
|
+
if (infoPublicLinkCard) {
|
|
951
|
+
infoPublicLinkCard.style.display = isPublicViewEnabled ? '' : 'none';
|
|
952
|
+
}
|
|
953
|
+
if (infoPublicUrl) {
|
|
954
|
+
infoPublicUrl.textContent = publicUrl;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Render Preview Content
|
|
811
958
|
if (isImage(fileName)) {
|
|
812
959
|
contentEl.innerHTML = `<img src="${viewUrl}" alt="${escapeAttr(fileName)}">`;
|
|
813
960
|
} else if (isVideo(fileName)) {
|
|
@@ -816,6 +963,12 @@
|
|
|
816
963
|
contentEl.innerHTML = `<div class="p-4 text-center"><audio controls autoplay class="w-100"><source src="${viewUrl}"></audio></div>`;
|
|
817
964
|
} else if (isPdf(fileName)) {
|
|
818
965
|
contentEl.innerHTML = `<iframe src="${viewUrl}" class="preview-iframe"></iframe>`;
|
|
966
|
+
} else if (isMarkdown(fileName)) {
|
|
967
|
+
fetch(viewUrl).then(res => res.text()).then(text => {
|
|
968
|
+
contentEl.innerHTML = renderMarkdownToHtml(text);
|
|
969
|
+
}).catch(err => {
|
|
970
|
+
contentEl.innerHTML = `<div class="p-4 text-danger text-center">Failed to load preview: ${escapeHtml(err.message)}</div>`;
|
|
971
|
+
});
|
|
819
972
|
} else if (isTextOrCode(fileName)) {
|
|
820
973
|
fetch(viewUrl).then(res => res.text()).then(text => {
|
|
821
974
|
contentEl.innerHTML = `<pre class="preview-code-block"><code>${escapeHtml(text)}</code></pre>`;
|
|
@@ -837,6 +990,70 @@
|
|
|
837
990
|
if (modal) modal.classList.remove('show');
|
|
838
991
|
};
|
|
839
992
|
|
|
993
|
+
window.copyCurrentPreviewLink = function () {
|
|
994
|
+
if (currentPreviewKey) copyShareLink(currentPreviewKey);
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
window.copyCurrentPreviewPublicLink = function () {
|
|
998
|
+
if (currentPreviewKey) copyPublicLink(currentPreviewKey);
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// Move & Rename Modal
|
|
1002
|
+
function promptMove(key, isCopy = false) {
|
|
1003
|
+
const modal = document.getElementById('moveModal');
|
|
1004
|
+
const title = document.getElementById('moveModalTitle');
|
|
1005
|
+
const preview = document.getElementById('moveSourcePathPreview');
|
|
1006
|
+
const srcInput = document.getElementById('moveSourceKeyInput');
|
|
1007
|
+
const destInput = document.getElementById('moveDestNameInput');
|
|
1008
|
+
const copyInput = document.getElementById('moveIsCopyInput');
|
|
1009
|
+
const label = document.getElementById('moveDestNameLabel');
|
|
1010
|
+
|
|
1011
|
+
if (!modal) return;
|
|
1012
|
+
|
|
1013
|
+
if (title) title.textContent = isCopy ? 'Copy File' : 'Move or Rename File';
|
|
1014
|
+
if (preview) preview.textContent = key;
|
|
1015
|
+
if (srcInput) srcInput.value = key;
|
|
1016
|
+
if (copyInput) copyInput.value = isCopy ? 'true' : 'false';
|
|
1017
|
+
if (destInput) destInput.value = key;
|
|
1018
|
+
if (label) label.textContent = isCopy ? 'Destination Key / Name' : 'New Key / Name';
|
|
1019
|
+
|
|
1020
|
+
modal.classList.add('show');
|
|
1021
|
+
setTimeout(() => destInput?.focus(), 50);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
window.closeMoveModal = function () {
|
|
1025
|
+
const modal = document.getElementById('moveModal');
|
|
1026
|
+
if (modal) modal.classList.remove('show');
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
window.submitMoveItem = async function () {
|
|
1030
|
+
const srcKey = document.getElementById('moveSourceKeyInput')?.value;
|
|
1031
|
+
const destKey = document.getElementById('moveDestNameInput')?.value?.trim();
|
|
1032
|
+
const isCopy = document.getElementById('moveIsCopyInput')?.value === 'true';
|
|
1033
|
+
|
|
1034
|
+
if (!srcKey || !destKey) {
|
|
1035
|
+
showToast('Source and destination paths are required', 'warning');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const endpoint = isCopy ? '/mbkbucket/copy' : '/mbkbucket/move';
|
|
1040
|
+
try {
|
|
1041
|
+
const response = await fetch(withBucketUrl(endpoint), {
|
|
1042
|
+
method: 'POST',
|
|
1043
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1044
|
+
body: JSON.stringify({ sourceKey: srcKey, destKey })
|
|
1045
|
+
});
|
|
1046
|
+
const data = await response.json();
|
|
1047
|
+
if (!data.success) throw new Error(data.error || `${isCopy ? 'Copy' : 'Move'} failed`);
|
|
1048
|
+
|
|
1049
|
+
window.closeMoveModal();
|
|
1050
|
+
showToast(`${isCopy ? 'Copied' : 'Moved'} to ${destKey}`, 'success');
|
|
1051
|
+
loadFiles(currentPage, currentPrefix);
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
showToast(err.message, 'error');
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
840
1057
|
// Delete Modal
|
|
841
1058
|
function promptDelete(key, isFolder = false) {
|
|
842
1059
|
const modal = document.getElementById('deleteModal');
|
|
@@ -989,6 +1206,35 @@
|
|
|
989
1206
|
|
|
990
1207
|
// Event Listeners Setup
|
|
991
1208
|
function initEventListeners() {
|
|
1209
|
+
// Refresh button
|
|
1210
|
+
document.getElementById('refresh-btn')?.addEventListener('click', () => {
|
|
1211
|
+
loadFiles(currentPage, currentPrefix, currentSearch);
|
|
1212
|
+
checkConnectionHealth(false);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
// Health check button
|
|
1216
|
+
document.getElementById('checkHealthBtn')?.addEventListener('click', () => {
|
|
1217
|
+
checkConnectionHealth(false);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// Preview Tabs switcher
|
|
1221
|
+
document.querySelector('.preview-tabs')?.addEventListener('click', (e) => {
|
|
1222
|
+
const btn = e.target.closest('.preview-tab');
|
|
1223
|
+
if (!btn) return;
|
|
1224
|
+
document.querySelectorAll('.preview-tab').forEach(b => b.classList.remove('active'));
|
|
1225
|
+
btn.classList.add('active');
|
|
1226
|
+
const target = btn.dataset.previewTab;
|
|
1227
|
+
const pPane = document.getElementById('pane-preview');
|
|
1228
|
+
const iPane = document.getElementById('pane-info');
|
|
1229
|
+
if (target === 'preview') {
|
|
1230
|
+
if (pPane) pPane.style.display = 'block';
|
|
1231
|
+
if (iPane) iPane.style.display = 'none';
|
|
1232
|
+
} else {
|
|
1233
|
+
if (pPane) pPane.style.display = 'none';
|
|
1234
|
+
if (iPane) iPane.style.display = 'block';
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
|
|
992
1238
|
// Tree clicks
|
|
993
1239
|
document.getElementById('fm-tree')?.addEventListener('click', (e) => {
|
|
994
1240
|
const link = e.target.closest('.tree-link');
|
|
@@ -1092,8 +1338,40 @@
|
|
|
1092
1338
|
renderFiles();
|
|
1093
1339
|
});
|
|
1094
1340
|
|
|
1095
|
-
// Grid Item Interactions (Click, Double Click, Star, Context Menu)
|
|
1341
|
+
// Grid Item Interactions (Click, Double Click, Star, Checkbox, Context Menu)
|
|
1096
1342
|
document.getElementById('fm-grid')?.addEventListener('click', (e) => {
|
|
1343
|
+
// Checkbox selection
|
|
1344
|
+
const cb = e.target.closest('.file-select-cb');
|
|
1345
|
+
if (cb) {
|
|
1346
|
+
e.stopPropagation();
|
|
1347
|
+
const key = cb.dataset.key;
|
|
1348
|
+
if (cb.checked) {
|
|
1349
|
+
selectedFiles.add(key);
|
|
1350
|
+
} else {
|
|
1351
|
+
selectedFiles.delete(key);
|
|
1352
|
+
}
|
|
1353
|
+
const parentItem = cb.closest('.fm-item');
|
|
1354
|
+
if (parentItem) parentItem.classList.toggle('is-selected', cb.checked);
|
|
1355
|
+
updateBatchBar();
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Select All in list header
|
|
1360
|
+
const selectAll = e.target.closest('#selectAllCb');
|
|
1361
|
+
if (selectAll) {
|
|
1362
|
+
e.stopPropagation();
|
|
1363
|
+
const checked = selectAll.checked;
|
|
1364
|
+
const visibleFiles = getFilteredFiles();
|
|
1365
|
+
visibleFiles.forEach(f => {
|
|
1366
|
+
if (checked) selectedFiles.add(f.Key);
|
|
1367
|
+
else selectedFiles.delete(f.Key);
|
|
1368
|
+
});
|
|
1369
|
+
document.querySelectorAll('.file-select-cb').forEach(c => { c.checked = checked; });
|
|
1370
|
+
document.querySelectorAll('.fm-item[data-type="file"]').forEach(item => item.classList.toggle('is-selected', checked));
|
|
1371
|
+
updateBatchBar();
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1097
1375
|
// Star click
|
|
1098
1376
|
const starBtn = e.target.closest('[data-star-key]');
|
|
1099
1377
|
if (starBtn) {
|
|
@@ -1112,19 +1390,35 @@
|
|
|
1112
1390
|
|
|
1113
1391
|
if (fileKey) {
|
|
1114
1392
|
const isStarred = starredFiles.has(fileKey);
|
|
1115
|
-
|
|
1116
|
-
{ id: 'view', label: 'Preview', icon: '<
|
|
1117
|
-
{ id: 'download', label: 'Download', icon: '<
|
|
1118
|
-
{ id: 'share', label: 'Copy
|
|
1119
|
-
|
|
1393
|
+
const menuItems = [
|
|
1394
|
+
{ id: 'view', label: 'Preview & Info', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 8s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z"/><circle cx="8" cy="8" r="2.5"/></svg>', action: () => openPreview(fileKey) },
|
|
1395
|
+
{ id: 'download', label: 'Download', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M5 8l3 3 3-3"/><line x1="8" y1="2" x2="8" y2="11"/></svg>', action: () => downloadFile(fileKey) },
|
|
1396
|
+
{ id: 'share', label: 'Copy Link', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 2h8a1 1 0 011 1v8M3 5h8a1 1 0 011 1v8a1 1 0 01-1 1H3a1 1 0 01-1-1V6a1 1 0 011-1z"/></svg>', action: () => copyShareLink(fileKey) },
|
|
1397
|
+
];
|
|
1398
|
+
|
|
1399
|
+
if (isPublicViewEnabled) {
|
|
1400
|
+
menuItems.push({
|
|
1401
|
+
id: 'public-share',
|
|
1402
|
+
label: 'Copy Public Link',
|
|
1403
|
+
icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="6.25"/><path d="M1.75 8h12.5M8 1.75a10.5 10.5 0 010 12.5 10.5 10.5 0 010-12.5z"/></svg>',
|
|
1404
|
+
action: () => copyPublicLink(fileKey)
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
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
|
+
{ 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) },
|
|
1120
1412
|
'-',
|
|
1121
|
-
{ id: 'delete', label: 'Delete', icon: '<
|
|
1122
|
-
|
|
1413
|
+
{ 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) }
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
openContextMenu(rect.left, rect.bottom + 4, menuItems);
|
|
1123
1417
|
} else if (folderPath) {
|
|
1124
1418
|
openContextMenu(rect.left, rect.bottom + 4, [
|
|
1125
|
-
{ id: 'open', label: 'Open Folder', icon: '<
|
|
1419
|
+
{ id: 'open', label: 'Open Folder', icon: '<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4a1 1 0 011-1h3l2 2h5a1 1 0 011 1v6a1 1 0 01-1 1H3a1 1 0 01-1-1V4z"/></svg>', action: () => loadFiles(1, folderPath) },
|
|
1126
1420
|
'-',
|
|
1127
|
-
{ id: 'delete', label: 'Delete Folder', icon: '<
|
|
1421
|
+
{ id: 'delete', label: 'Delete Folder', 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(folderPath + '/', true) }
|
|
1128
1422
|
]);
|
|
1129
1423
|
}
|
|
1130
1424
|
return;
|
|
@@ -1140,17 +1434,53 @@
|
|
|
1140
1434
|
|
|
1141
1435
|
// File click to preview
|
|
1142
1436
|
const fileItem = e.target.closest('[data-type="file"]');
|
|
1143
|
-
if (fileItem && !e.target.closest('.file-select-cb')) {
|
|
1437
|
+
if (fileItem && !e.target.closest('.file-select-cb') && !e.target.closest('.fm-star')) {
|
|
1144
1438
|
e.stopPropagation();
|
|
1145
1439
|
openPreview(fileItem.dataset.key);
|
|
1146
1440
|
}
|
|
1147
1441
|
});
|
|
1148
1442
|
|
|
1443
|
+
// Batch Bar Actions
|
|
1444
|
+
document.getElementById('batchClearBtn')?.addEventListener('click', clearBatchSelection);
|
|
1445
|
+
|
|
1446
|
+
document.getElementById('batchDownloadBtn')?.addEventListener('click', async () => {
|
|
1447
|
+
const keys = Array.from(selectedFiles);
|
|
1448
|
+
if (!keys.length) return;
|
|
1449
|
+
showToast(`Starting download for ${keys.length} item(s)...`);
|
|
1450
|
+
for (const key of keys) {
|
|
1451
|
+
await downloadFile(key);
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
document.getElementById('batchDeleteBtn')?.addEventListener('click', () => {
|
|
1456
|
+
const keys = Array.from(selectedFiles);
|
|
1457
|
+
if (!keys.length) return;
|
|
1458
|
+
promptDelete(`${keys.length} selected files`, false);
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
document.getElementById('batchMoveBtn')?.addEventListener('click', () => {
|
|
1462
|
+
const keys = Array.from(selectedFiles);
|
|
1463
|
+
if (!keys.length) return;
|
|
1464
|
+
promptMove(keys[0], false);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1149
1467
|
// Bucket Selector
|
|
1150
1468
|
document.getElementById('bucketSelector')?.addEventListener('change', (e) => {
|
|
1469
|
+
const opt = e.target.selectedOptions?.[0];
|
|
1151
1470
|
const newBucket = e.target.value;
|
|
1152
1471
|
if (!newBucket) return;
|
|
1153
1472
|
activeBucket = newBucket;
|
|
1473
|
+
|
|
1474
|
+
// Update provider badge
|
|
1475
|
+
const providerType = opt?.dataset?.type || 's3';
|
|
1476
|
+
const badge = document.getElementById('topProviderBadge');
|
|
1477
|
+
if (badge) {
|
|
1478
|
+
badge.className = `provider-badge provider-${providerType}`;
|
|
1479
|
+
const lbl = badge.querySelector('.provider-label');
|
|
1480
|
+
if (lbl) lbl.textContent = providerType;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
clearBatchSelection();
|
|
1154
1484
|
loadFiles(1, '');
|
|
1155
1485
|
});
|
|
1156
1486
|
|
|
@@ -1236,17 +1566,22 @@
|
|
|
1236
1566
|
const key = formData.get('key');
|
|
1237
1567
|
const isFolder = formData.get('folder') === 'true';
|
|
1238
1568
|
|
|
1569
|
+
const payload = selectedFiles.size > 0 && key?.includes('selected')
|
|
1570
|
+
? { keys: Array.from(selectedFiles), prefix: currentPrefix }
|
|
1571
|
+
: { key, isFolder, prefix: currentPrefix };
|
|
1572
|
+
|
|
1239
1573
|
try {
|
|
1240
1574
|
const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
|
|
1241
1575
|
method: 'POST',
|
|
1242
1576
|
headers: { 'Content-Type': 'application/json' },
|
|
1243
|
-
body: JSON.stringify(
|
|
1577
|
+
body: JSON.stringify(payload)
|
|
1244
1578
|
});
|
|
1245
1579
|
const data = await response.json();
|
|
1246
1580
|
if (!data.success) throw new Error(data.error || 'Delete failed');
|
|
1247
1581
|
|
|
1248
1582
|
closeModal();
|
|
1249
|
-
|
|
1583
|
+
clearBatchSelection();
|
|
1584
|
+
showToast('Deleted successfully', 'success');
|
|
1250
1585
|
loadFiles(currentPage, currentPrefix);
|
|
1251
1586
|
} catch (err) {
|
|
1252
1587
|
showToast(err.message, 'error');
|
|
@@ -1256,7 +1591,6 @@
|
|
|
1256
1591
|
|
|
1257
1592
|
// Initialization
|
|
1258
1593
|
document.addEventListener('DOMContentLoaded', () => {
|
|
1259
|
-
// Sync view mode buttons
|
|
1260
1594
|
if (currentViewMode === 'list') {
|
|
1261
1595
|
document.getElementById('view-list')?.classList.add('active');
|
|
1262
1596
|
document.getElementById('view-grid')?.classList.remove('active');
|
|
@@ -1267,19 +1601,21 @@
|
|
|
1267
1601
|
|
|
1268
1602
|
initEventListeners();
|
|
1269
1603
|
|
|
1270
|
-
// Initial load with URL params
|
|
1271
1604
|
const initialFolder = urlParams.get('folder') || '';
|
|
1272
1605
|
const initialPage = parseInt(urlParams.get('page') || '1', 10);
|
|
1273
1606
|
const initialSearch = urlParams.get('search') || '';
|
|
1274
1607
|
|
|
1275
1608
|
loadFiles(initialPage, initialFolder, initialSearch);
|
|
1609
|
+
checkConnectionHealth(true);
|
|
1276
1610
|
});
|
|
1277
1611
|
|
|
1278
|
-
// Expose global methods
|
|
1612
|
+
// Expose global methods
|
|
1279
1613
|
window.mbkbucket = {
|
|
1280
1614
|
loadFiles,
|
|
1281
1615
|
openPreview,
|
|
1282
1616
|
downloadFile,
|
|
1617
|
+
promptMove,
|
|
1618
|
+
checkConnectionHealth,
|
|
1283
1619
|
showToast
|
|
1284
1620
|
};
|
|
1285
1621
|
|