mbkbucket 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -2
- package/README.md +72 -0
- package/index.d.ts +87 -49
- package/index.js +15 -13
- package/lib/config/index.js +9 -89
- package/lib/config/validation.js +86 -0
- package/lib/routes/api.js +139 -45
- package/lib/routes/index.js +70 -1
- package/lib/routes/view.js +71 -18
- package/lib/s3.js +64 -32
- package/package.json +2 -2
- package/public/bucketadmin.css +447 -17
- package/public/bucketadmin.js +272 -82
- package/test/config-validation.test.js +116 -0
- package/views/bucket.handlebars +66 -31
- package/views/bucketadmincontent.handlebars +30 -9
- package/views/bucketportal.handlebars +1 -0
package/public/bucketadmin.js
CHANGED
|
@@ -10,7 +10,29 @@ let pageFolders = [];
|
|
|
10
10
|
let currentNextToken = null;
|
|
11
11
|
let currentApiPrefix = '';
|
|
12
12
|
const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
|
|
13
|
-
|
|
13
|
+
const selectedBucket = new URLSearchParams(window.location.search).get('bucket') || '';
|
|
14
|
+
let activeBucket = selectedBucket;
|
|
15
|
+
const isRootApp = String(bucketAppName || '').toLowerCase() === 'portal';
|
|
16
|
+
const rootLabel = isRootApp ? 'root' : (bucketAppName || 'app');
|
|
17
|
+
let currentQuickFilter = 'all';
|
|
18
|
+
let virtualRenderLimit = 0;
|
|
19
|
+
const VIRTUAL_RENDER_THRESHOLD = 500;
|
|
20
|
+
const VIRTUAL_RENDER_BATCH = 250;
|
|
21
|
+
let lastRenderContext = null;
|
|
22
|
+
|
|
23
|
+
function withBucketUrl(url) {
|
|
24
|
+
if (!activeBucket) return url;
|
|
25
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
26
|
+
return `${url}${separator}bucket=${encodeURIComponent(activeBucket)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getViewUrl(encodedKey) {
|
|
30
|
+
return withBucketUrl(`/mbkbucket/view/${encodedKey}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getDownloadUrl(encodedKey) {
|
|
34
|
+
return withBucketUrl(`/mbkbucket/download/${encodedKey}`);
|
|
35
|
+
}
|
|
14
36
|
|
|
15
37
|
function stripApiPrefixFromPath(path, apiPrefix = '') {
|
|
16
38
|
const normalizedPath = String(path || '').replace(/^\/+/, '');
|
|
@@ -37,6 +59,64 @@ function toApiFolderPath(prefix = '', apiPrefix = '') {
|
|
|
37
59
|
return `${normalizedApiPrefix}/${uiPrefix}`;
|
|
38
60
|
}
|
|
39
61
|
|
|
62
|
+
function escapeHtml(value = '') {
|
|
63
|
+
return String(value)
|
|
64
|
+
.replace(/&/g, '&')
|
|
65
|
+
.replace(/</g, '<')
|
|
66
|
+
.replace(/>/g, '>')
|
|
67
|
+
.replace(/"/g, '"')
|
|
68
|
+
.replace(/'/g, ''');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function escapeAttr(value = '') {
|
|
72
|
+
return escapeHtml(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function syncStateUrl(page = currentPage, prefix = currentPrefix, search = currentSearch) {
|
|
76
|
+
try {
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
|
|
79
|
+
if (prefix) {
|
|
80
|
+
const folderPathForUrl = toApiFolderPath(prefix, currentApiPrefix);
|
|
81
|
+
const folderEncoded = encodeURIComponent(folderPathForUrl).replace(/%2F/g, '/');
|
|
82
|
+
parts.push(`folder=${folderEncoded}`);
|
|
83
|
+
}
|
|
84
|
+
if (page && page > 1) parts.push(`page=${page}`);
|
|
85
|
+
if (search) parts.push(`search=${encodeURIComponent(search)}`);
|
|
86
|
+
parts.push(`sort=${encodeURIComponent(currentSort)}`);
|
|
87
|
+
parts.push(`order=${encodeURIComponent(currentOrder)}`);
|
|
88
|
+
parts.push(`view=${encodeURIComponent(currentViewMode)}`);
|
|
89
|
+
parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
|
|
90
|
+
|
|
91
|
+
const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
|
|
92
|
+
window.history.replaceState(null, '', newUrl);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.warn('history update failed', e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getActiveBucketLabel() {
|
|
99
|
+
return activeBucket || 'default bucket';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function matchesQuickFilter(fileName = '', filter = currentQuickFilter) {
|
|
103
|
+
const ext = String(fileName).split('.').pop().toLowerCase();
|
|
104
|
+
const media = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
105
|
+
const docs = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv']);
|
|
106
|
+
const code = new Set(['js', 'ts', 'html', 'htm', 'css', 'php', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'rb', 'go', 'rs', 'sql', 'sh', 'bat', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'conf', 'json', 'xml', 'log']);
|
|
107
|
+
|
|
108
|
+
if (filter === 'media') return media.has(ext);
|
|
109
|
+
if (filter === 'docs') return docs.has(ext);
|
|
110
|
+
if (filter === 'code') return code.has(ext);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function updateQuickFilterUI() {
|
|
115
|
+
document.querySelectorAll('.quick-filter-btn').forEach((btn) => {
|
|
116
|
+
btn.classList.toggle('active', btn.dataset.filter === currentQuickFilter);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
40
120
|
// Show alert message (Toast)
|
|
41
121
|
function showAlert(message, type = 'success') {
|
|
42
122
|
const container = document.getElementById('toast-container');
|
|
@@ -103,26 +183,10 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
103
183
|
currentSearch = search;
|
|
104
184
|
currentSort = sort;
|
|
105
185
|
currentOrder = order;
|
|
186
|
+
virtualRenderLimit = 0;
|
|
106
187
|
|
|
107
|
-
// Update browser URL so navigation is shareable/bookmarkable
|
|
108
|
-
|
|
109
|
-
const parts = [];
|
|
110
|
-
if (prefix) {
|
|
111
|
-
const folderPathForUrl = toApiFolderPath(prefix, currentApiPrefix);
|
|
112
|
-
// preserve slashes in folder param for readability: encode then restore slashes
|
|
113
|
-
const folderEncoded = encodeURIComponent(folderPathForUrl).replace(/%2F/g, '/');
|
|
114
|
-
parts.push(`folder=${folderEncoded}`);
|
|
115
|
-
}
|
|
116
|
-
if (page && page > 1) parts.push(`page=${page}`);
|
|
117
|
-
if (search) parts.push(`search=${encodeURIComponent(search)}`);
|
|
118
|
-
// Optional: Add sort/order to URL if desired, skipping for simplicity/clean URLs for now
|
|
119
|
-
|
|
120
|
-
const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
|
|
121
|
-
window.history.replaceState(null, '', newUrl);
|
|
122
|
-
} catch (e) {
|
|
123
|
-
// Ignore history errors in older browsers
|
|
124
|
-
console.warn('history update failed', e);
|
|
125
|
-
}
|
|
188
|
+
// Update browser URL so navigation is shareable/bookmarkable.
|
|
189
|
+
syncStateUrl(page, prefix, search);
|
|
126
190
|
|
|
127
191
|
const container = document.getElementById('fileListContainer');
|
|
128
192
|
const paginationContainer = document.getElementById('paginationContainer');
|
|
@@ -157,7 +221,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
157
221
|
const recursiveParam = `&recursive=${!isFolderMode}`; // false if folder mode, true if flat mode
|
|
158
222
|
const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
|
|
159
223
|
|
|
160
|
-
const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`);
|
|
224
|
+
const response = await fetch(withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`));
|
|
161
225
|
const data = await response.json();
|
|
162
226
|
|
|
163
227
|
if (!data.success) {
|
|
@@ -176,11 +240,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
176
240
|
}
|
|
177
241
|
|
|
178
242
|
if (search) {
|
|
179
|
-
|
|
243
|
+
const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
|
|
244
|
+
filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Search: "${search}"${filterSuffix}`;
|
|
180
245
|
fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
|
|
181
246
|
} else {
|
|
182
|
-
const label = prefix ||
|
|
183
|
-
|
|
247
|
+
const label = prefix || rootLabel;
|
|
248
|
+
const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
|
|
249
|
+
filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Folder: ${label}${filterSuffix}`;
|
|
184
250
|
fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
|
|
185
251
|
}
|
|
186
252
|
|
|
@@ -192,7 +258,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
192
258
|
<div class="empty-state">
|
|
193
259
|
<i class="fas fa-folder-open"></i>
|
|
194
260
|
<h4>No files found</h4>
|
|
195
|
-
|
|
261
|
+
<p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Upload some files to get started!</p>
|
|
196
262
|
<button class="btn btn-lg" onclick="document.getElementById('fileInput').click()">
|
|
197
263
|
<i class="fas fa-plus"></i> Upload Your First File
|
|
198
264
|
</button>
|
|
@@ -223,7 +289,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
223
289
|
<div class="empty-state">
|
|
224
290
|
<i class="fas fa-exclamation-triangle" style="color: var(--danger-color-ml);"></i>
|
|
225
291
|
<h4>Error Loading Files</h4>
|
|
226
|
-
|
|
292
|
+
<p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>: ${escapeHtml(error.message)}</p>
|
|
227
293
|
<button class="btn btn-primary" onclick="loadFiles(${page}, '${prefix}')">
|
|
228
294
|
<i class="fas fa-redo"></i> Retry
|
|
229
295
|
</button>
|
|
@@ -263,6 +329,7 @@ function toggleSort(field) {
|
|
|
263
329
|
});
|
|
264
330
|
|
|
265
331
|
renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
|
|
332
|
+
syncStateUrl(currentPage, currentPrefix, currentSearch);
|
|
266
333
|
}
|
|
267
334
|
}
|
|
268
335
|
|
|
@@ -281,6 +348,15 @@ function clearSearch() {
|
|
|
281
348
|
loadFiles(1, currentPrefix, '');
|
|
282
349
|
}
|
|
283
350
|
|
|
351
|
+
function setQuickFilter(filter = 'all') {
|
|
352
|
+
const nextFilter = String(filter || 'all').toLowerCase();
|
|
353
|
+
if (currentQuickFilter === nextFilter) return;
|
|
354
|
+
currentQuickFilter = nextFilter;
|
|
355
|
+
updateQuickFilterUI();
|
|
356
|
+
renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
|
|
357
|
+
syncStateUrl(currentPage, currentPrefix, currentSearch);
|
|
358
|
+
}
|
|
359
|
+
|
|
284
360
|
// Extract unique folders at current level
|
|
285
361
|
function extractFoldersAtCurrentLevel(files, prefix) {
|
|
286
362
|
const folderSet = new Set();
|
|
@@ -330,8 +406,14 @@ function changeViewMode(mode) {
|
|
|
330
406
|
btnFolder.classList.add('btn-outline-light');
|
|
331
407
|
}
|
|
332
408
|
|
|
333
|
-
//
|
|
334
|
-
|
|
409
|
+
// Refetch to respect recursive/non-recursive mode and keep URL state.
|
|
410
|
+
loadFiles(1, currentPrefix, currentSearch, currentSort, currentOrder);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function expandVirtualRows() {
|
|
414
|
+
if (!lastRenderContext) return;
|
|
415
|
+
virtualRenderLimit += VIRTUAL_RENDER_BATCH;
|
|
416
|
+
renderFilesTable(lastRenderContext.files, lastRenderContext.folders, lastRenderContext.prefix, lastRenderContext.apiPrefix);
|
|
335
417
|
}
|
|
336
418
|
|
|
337
419
|
// Extract folders that match a search query from file paths
|
|
@@ -364,6 +446,8 @@ function extractMatchingFolders(files, search) {
|
|
|
364
446
|
|
|
365
447
|
// Render files table
|
|
366
448
|
function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
449
|
+
lastRenderContext = { files, folders, prefix, apiPrefix };
|
|
450
|
+
|
|
367
451
|
// Determine which files/folders to show based on view mode
|
|
368
452
|
let displayFiles = [];
|
|
369
453
|
let displayFolders = [];
|
|
@@ -409,6 +493,12 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
409
493
|
displayFolders = folders;
|
|
410
494
|
}
|
|
411
495
|
|
|
496
|
+
// Apply quick file-type filter after mode/path filtering.
|
|
497
|
+
displayFiles = displayFiles.filter((f) => {
|
|
498
|
+
const name = String(f?.Key || '').split('/').pop();
|
|
499
|
+
return matchesQuickFilter(name, currentQuickFilter);
|
|
500
|
+
});
|
|
501
|
+
|
|
412
502
|
// Generate breadcrumb navigation
|
|
413
503
|
let breadcrumb = '';
|
|
414
504
|
// Show breadcrumb only if we are in folder mode OR if we have a prefix filter active even in flat mode
|
|
@@ -419,7 +509,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
419
509
|
<div class="breadcrumb-nav">
|
|
420
510
|
<div class="breadcrumb-item">
|
|
421
511
|
<i class="fas fa-home"></i>
|
|
422
|
-
<span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to ${
|
|
512
|
+
<span class="breadcrumb-link" onclick="loadFiles(1, '')" title="Go to ${rootLabel}">${rootLabel}</span>
|
|
423
513
|
</div>
|
|
424
514
|
`;
|
|
425
515
|
|
|
@@ -451,13 +541,18 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
451
541
|
}
|
|
452
542
|
|
|
453
543
|
// Generate folder rows (only used in folder mode)
|
|
454
|
-
|
|
544
|
+
const folderRows = displayFolders.map(folderPath => {
|
|
455
545
|
// Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
|
|
456
|
-
|
|
546
|
+
const rawFolderPath = String(folderPath || '');
|
|
547
|
+
const cleanPath = rawFolderPath.replace(/\/+$/, '');
|
|
548
|
+
const folderKeyForDelete = `${cleanPath}/`;
|
|
457
549
|
const folderName = cleanPath.split('/').pop();
|
|
550
|
+
const safeFolderName = escapeHtml(folderName);
|
|
551
|
+
const safeFolderPathAttr = escapeAttr(rawFolderPath);
|
|
552
|
+
const safeFolderDeleteKeyAttr = escapeAttr(folderKeyForDelete);
|
|
458
553
|
|
|
459
554
|
return `
|
|
460
|
-
|
|
555
|
+
<tr class="folder-row" data-folder-path="${safeFolderPathAttr}" title="Open folder: ${safeFolderName}">
|
|
461
556
|
<td style="text-align:center;">
|
|
462
557
|
<!-- no selection for folders -->
|
|
463
558
|
</td>
|
|
@@ -468,10 +563,10 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
468
563
|
</div>
|
|
469
564
|
<div class="file-info">
|
|
470
565
|
<div class="file-name" style="font-weight: 700;">
|
|
471
|
-
|
|
566
|
+
<i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${safeFolderName}/
|
|
472
567
|
</div>
|
|
473
568
|
</div>
|
|
474
|
-
|
|
569
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn dtm" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
475
570
|
<i class="fas fa-trash"></i>
|
|
476
571
|
</button>
|
|
477
572
|
</div>
|
|
@@ -484,7 +579,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
484
579
|
</td>
|
|
485
580
|
<td style="text-align: center;">
|
|
486
581
|
<div class="btn-group" role="group">
|
|
487
|
-
|
|
582
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
488
583
|
<i class="fas fa-trash"></i>
|
|
489
584
|
</button>
|
|
490
585
|
</div>
|
|
@@ -493,12 +588,22 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
493
588
|
`;
|
|
494
589
|
}).join('');
|
|
495
590
|
|
|
591
|
+
const isVirtualized = displayFiles.length > VIRTUAL_RENDER_THRESHOLD;
|
|
592
|
+
if (!isVirtualized) {
|
|
593
|
+
virtualRenderLimit = 0;
|
|
594
|
+
} else if (!virtualRenderLimit) {
|
|
595
|
+
virtualRenderLimit = VIRTUAL_RENDER_BATCH;
|
|
596
|
+
}
|
|
597
|
+
const visibleFiles = isVirtualized ? displayFiles.slice(0, virtualRenderLimit) : displayFiles;
|
|
598
|
+
|
|
496
599
|
// Generate file rows
|
|
497
|
-
const fileRows =
|
|
498
|
-
const
|
|
600
|
+
const fileRows = visibleFiles.map(file => {
|
|
601
|
+
const rawKey = String(file.Key || '');
|
|
602
|
+
const encodedKey = encodeURIComponent(rawKey);
|
|
603
|
+
const safeKeyAttr = escapeAttr(rawKey);
|
|
499
604
|
|
|
500
605
|
// Calculate display name and icon directly
|
|
501
|
-
const fullPath =
|
|
606
|
+
const fullPath = rawKey;
|
|
502
607
|
const lastSlashIndex = fullPath.lastIndexOf('/');
|
|
503
608
|
let filename = fullPath;
|
|
504
609
|
let folderPath = '';
|
|
@@ -512,18 +617,21 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
512
617
|
const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(filename) : '';
|
|
513
618
|
const truncatedName = (window.truncateFileName ? window.truncateFileName(filename) : filename) || filename;
|
|
514
619
|
const isFileViewable = window.isViewable ? window.isViewable(filename) : false;
|
|
620
|
+
const safeFilename = escapeHtml(filename);
|
|
621
|
+
const safeTruncatedName = escapeHtml(truncatedName);
|
|
622
|
+
const safeFolderPath = escapeHtml(folderPath);
|
|
515
623
|
|
|
516
624
|
// If in flat mode, we might want to show the folder path more prominently
|
|
517
625
|
const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
|
|
518
626
|
? `<div class="file-path-display">
|
|
519
|
-
|
|
627
|
+
<i class="fas fa-folder" style="margin-right:4px; opacity:0.5;"></i> ${safeFolderPath}
|
|
520
628
|
</div>`
|
|
521
629
|
: '';
|
|
522
630
|
|
|
523
631
|
return `
|
|
524
632
|
<tr class="file-row">
|
|
525
633
|
<td style="text-align:center;">
|
|
526
|
-
|
|
634
|
+
<input type="checkbox" class="selectFileCheckbox" data-key="${safeKeyAttr}" aria-label="Select ${safeKeyAttr}" />
|
|
527
635
|
</td>
|
|
528
636
|
<td style="padding-left: 1.5rem;">
|
|
529
637
|
<div style="display: flex; align-items: center;">
|
|
@@ -532,7 +640,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
532
640
|
</div>
|
|
533
641
|
<div class="file-info">
|
|
534
642
|
${pathDisplay}
|
|
535
|
-
|
|
643
|
+
<div class="file-name" data-full-path="${safeKeyAttr}" title="${safeFilename}">${safeTruncatedName}</div>
|
|
536
644
|
</div>
|
|
537
645
|
</div>
|
|
538
646
|
</td>
|
|
@@ -544,18 +652,18 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
544
652
|
</td>
|
|
545
653
|
<td style="text-align: center;">
|
|
546
654
|
<div class="btn-group" role="group">
|
|
547
|
-
|
|
655
|
+
<button type="button" class="btn btn-primary btn-sm view-btn" data-key="${safeKeyAttr}" data-filename="${safeKeyAttr}"
|
|
548
656
|
data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${!isFileViewable ? 'disabled style="opacity:0.5"' : ''}>
|
|
549
657
|
<i class="fas fa-eye"></i>
|
|
550
658
|
</button>
|
|
551
659
|
<button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link" style="color:white;">
|
|
552
660
|
<i class="fas fa-link"></i>
|
|
553
661
|
</button>
|
|
554
|
-
|
|
662
|
+
<a href="${getDownloadUrl(encodedKey)}" class="btn btn-success btn-sm"
|
|
555
663
|
title="Download file">
|
|
556
664
|
<i class="fas fa-download"></i>
|
|
557
665
|
</a>
|
|
558
|
-
|
|
666
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeKeyAttr}" title="Delete file">
|
|
559
667
|
<i class="fas fa-trash"></i>
|
|
560
668
|
</button>
|
|
561
669
|
</div>
|
|
@@ -576,7 +684,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
576
684
|
</div>
|
|
577
685
|
<h4>No files found</h4>
|
|
578
686
|
<p class="empty-state-description">
|
|
579
|
-
|
|
687
|
+
${prefix ? `This folder <strong>${escapeHtml(prefix)}</strong> is currently empty in bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>.` : `Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Start by uploading files or creating a new folder.`}
|
|
580
688
|
</p>
|
|
581
689
|
<div class="empty-state-actions">
|
|
582
690
|
<button class="btn btn-primary" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); document.getElementById('fileInput').click()">
|
|
@@ -586,7 +694,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
586
694
|
<i class="fas fa-folder-plus"></i> New Folder
|
|
587
695
|
</button>
|
|
588
696
|
${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
|
|
589
|
-
<i class="fas fa-home"></i> Go to ${
|
|
697
|
+
<i class="fas fa-home"></i> Go to ${rootLabel}
|
|
590
698
|
</button>` : ''}
|
|
591
699
|
</div>
|
|
592
700
|
</div>
|
|
@@ -602,7 +710,16 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
602
710
|
: '<i class="fas fa-sort-down sort-icon sort-icon-active"></i>';
|
|
603
711
|
};
|
|
604
712
|
|
|
605
|
-
|
|
713
|
+
const virtualizedInfo = isVirtualized
|
|
714
|
+
? `<div class="virtualized-info">
|
|
715
|
+
<span>Showing ${visibleFiles.length} of ${displayFiles.length} files in this view.</span>
|
|
716
|
+
<div class="virtualized-actions">
|
|
717
|
+
${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" onclick="expandVirtualRows()">Load more</button>' : ''}
|
|
718
|
+
</div>
|
|
719
|
+
</div>`
|
|
720
|
+
: '';
|
|
721
|
+
|
|
722
|
+
document.getElementById('fileListContainer').innerHTML = `
|
|
606
723
|
${breadcrumb}
|
|
607
724
|
<div class="table-responsive">
|
|
608
725
|
<div id="bulkActionsBar" class="bulk-actions-bar">
|
|
@@ -637,6 +754,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
637
754
|
${allRows}
|
|
638
755
|
</tbody>
|
|
639
756
|
</table>
|
|
757
|
+
${virtualizedInfo}
|
|
640
758
|
</div>
|
|
641
759
|
`;
|
|
642
760
|
|
|
@@ -819,7 +937,7 @@ document.getElementById('deleteForm')?.addEventListener('submit', async function
|
|
|
819
937
|
const isFolder = document.getElementById('deleteIsFolder').value === 'true';
|
|
820
938
|
|
|
821
939
|
try {
|
|
822
|
-
const response = await fetch('/mbkbucket/delete', {
|
|
940
|
+
const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
|
|
823
941
|
method: 'POST',
|
|
824
942
|
headers: {
|
|
825
943
|
'Content-Type': 'application/json'
|
|
@@ -889,6 +1007,9 @@ function renderUploadQueue() {
|
|
|
889
1007
|
}
|
|
890
1008
|
|
|
891
1009
|
container.innerHTML = uploadQueue.map(item => {
|
|
1010
|
+
const safeName = escapeHtml(item.file.name);
|
|
1011
|
+
const iconClass = window.getFileIcon ? window.getFileIcon(item.file.name) : 'fas fa-file-alt';
|
|
1012
|
+
const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(item.file.name) : '';
|
|
892
1013
|
let statusText;
|
|
893
1014
|
if (item.status === 'uploading') {
|
|
894
1015
|
const pct = Math.round(item.progress || 0);
|
|
@@ -904,8 +1025,8 @@ function renderUploadQueue() {
|
|
|
904
1025
|
<div class="upload-item" data-id="${item.id}">
|
|
905
1026
|
<div class="meta">
|
|
906
1027
|
<div class="upload-item-meta-row">
|
|
907
|
-
|
|
908
|
-
|
|
1028
|
+
<div class="file-icon upload-file-icon ${typeCategory}"><i class="${iconClass}"></i></div>
|
|
1029
|
+
<div class="name upload-item-name" title="${safeName}">${safeName}</div>
|
|
909
1030
|
<div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
|
|
910
1031
|
</div>
|
|
911
1032
|
</div>
|
|
@@ -942,7 +1063,7 @@ function renderUploadQueue() {
|
|
|
942
1063
|
// Tell S3 to discard the incomplete multipart upload
|
|
943
1064
|
if (entry && entry.s3UploadId && entry.s3Key) {
|
|
944
1065
|
try {
|
|
945
|
-
await fetch('/mbkbucket/upload-abort', {
|
|
1066
|
+
await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
946
1067
|
method: 'POST',
|
|
947
1068
|
headers: { 'Content-Type': 'application/json' },
|
|
948
1069
|
body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
|
|
@@ -988,7 +1109,7 @@ async function startUploadEntry(entry) {
|
|
|
988
1109
|
// S3 Multipart Upload
|
|
989
1110
|
// Step 1 — Initiate: get uploadId + key from the server
|
|
990
1111
|
// ---------------------------------------------------------------
|
|
991
|
-
const initResp = await fetch('/mbkbucket/upload-init', {
|
|
1112
|
+
const initResp = await fetch(withBucketUrl('/mbkbucket/upload-init'), {
|
|
992
1113
|
method: 'POST',
|
|
993
1114
|
headers: { 'Content-Type': 'application/json' },
|
|
994
1115
|
body: JSON.stringify({
|
|
@@ -1015,7 +1136,7 @@ async function startUploadEntry(entry) {
|
|
|
1015
1136
|
const _chunkStart = Date.now();
|
|
1016
1137
|
if (entry.status === 'cancelled') {
|
|
1017
1138
|
// Clean up on S3
|
|
1018
|
-
await fetch('/mbkbucket/upload-abort', {
|
|
1139
|
+
await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
1019
1140
|
method: 'POST',
|
|
1020
1141
|
headers: { 'Content-Type': 'application/json' },
|
|
1021
1142
|
body: JSON.stringify({ uploadId, key })
|
|
@@ -1036,7 +1157,7 @@ async function startUploadEntry(entry) {
|
|
|
1036
1157
|
const controller = new AbortController();
|
|
1037
1158
|
xhrMap.set(entry.id, { controller, s3UploadId: uploadId, s3Key: key });
|
|
1038
1159
|
|
|
1039
|
-
const chunkResp = await fetch('/mbkbucket/upload-chunk', {
|
|
1160
|
+
const chunkResp = await fetch(withBucketUrl('/mbkbucket/upload-chunk'), {
|
|
1040
1161
|
method: 'POST',
|
|
1041
1162
|
body: fd,
|
|
1042
1163
|
signal: controller.signal
|
|
@@ -1056,7 +1177,7 @@ async function startUploadEntry(entry) {
|
|
|
1056
1177
|
// ---------------------------------------------------------------
|
|
1057
1178
|
// Step 3 — Complete: assemble all parts on S3
|
|
1058
1179
|
// ---------------------------------------------------------------
|
|
1059
|
-
const completeResp = await fetch('/mbkbucket/upload-complete', {
|
|
1180
|
+
const completeResp = await fetch(withBucketUrl('/mbkbucket/upload-complete'), {
|
|
1060
1181
|
method: 'POST',
|
|
1061
1182
|
headers: { 'Content-Type': 'application/json' },
|
|
1062
1183
|
body: JSON.stringify({ uploadId, key, parts })
|
|
@@ -1076,7 +1197,7 @@ async function startUploadEntry(entry) {
|
|
|
1076
1197
|
fd.append('prefix', currentPrefix);
|
|
1077
1198
|
|
|
1078
1199
|
const xhr = new XMLHttpRequest();
|
|
1079
|
-
xhr.open('POST', '/mbkbucket/upload');
|
|
1200
|
+
xhr.open('POST', withBucketUrl('/mbkbucket/upload'));
|
|
1080
1201
|
|
|
1081
1202
|
xhr.upload.addEventListener('progress', (e) => {
|
|
1082
1203
|
if (!e.lengthComputable) return;
|
|
@@ -1123,7 +1244,7 @@ async function startUploadEntry(entry) {
|
|
|
1123
1244
|
console.error('Upload failed for', entry.file.name, err);
|
|
1124
1245
|
// If a multipart upload was initiated but didn't finish (not a user cancel), abort it on S3
|
|
1125
1246
|
if (entry.s3UploadId && entry.s3Key && entry.status !== 'cancelled') {
|
|
1126
|
-
fetch('/mbkbucket/upload-abort', {
|
|
1247
|
+
fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
1127
1248
|
method: 'POST',
|
|
1128
1249
|
headers: { 'Content-Type': 'application/json' },
|
|
1129
1250
|
body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
|
|
@@ -1151,20 +1272,33 @@ document.getElementById('clearQueueBtn')?.addEventListener('click', function ()
|
|
|
1151
1272
|
function initializeUploadCollapse() {
|
|
1152
1273
|
const toggleBtn = document.getElementById('uploadToggleBtn');
|
|
1153
1274
|
const uploadSection = document.getElementById('uploadSection');
|
|
1275
|
+
const uploadCard = uploadSection?.closest('.dashboard-card');
|
|
1154
1276
|
|
|
1155
1277
|
if (!toggleBtn || !uploadSection) return;
|
|
1278
|
+
|
|
1279
|
+
const syncUploadCollapseState = (expanded) => {
|
|
1280
|
+
uploadSection.classList.toggle('expanded', expanded);
|
|
1281
|
+
toggleBtn.classList.toggle('expanded', expanded);
|
|
1282
|
+
toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|
1283
|
+
uploadSection.setAttribute('aria-hidden', expanded ? 'false' : 'true');
|
|
1284
|
+
if (uploadCard) uploadCard.classList.toggle('upload-panel-open', expanded);
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
toggleBtn.setAttribute('role', 'button');
|
|
1288
|
+
toggleBtn.setAttribute('tabindex', '0');
|
|
1289
|
+
toggleBtn.setAttribute('aria-controls', 'uploadSection');
|
|
1290
|
+
syncUploadCollapseState(uploadSection.classList.contains('expanded'));
|
|
1156
1291
|
|
|
1157
1292
|
toggleBtn.addEventListener('click', function() {
|
|
1158
1293
|
const isExpanded = uploadSection.classList.contains('expanded');
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
toggleBtn.classList.add('expanded');
|
|
1294
|
+
syncUploadCollapseState(!isExpanded);
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
toggleBtn.addEventListener('keydown', function (event) {
|
|
1298
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1299
|
+
event.preventDefault();
|
|
1300
|
+
const isExpanded = uploadSection.classList.contains('expanded');
|
|
1301
|
+
syncUploadCollapseState(!isExpanded);
|
|
1168
1302
|
}
|
|
1169
1303
|
});
|
|
1170
1304
|
}
|
|
@@ -1173,12 +1307,61 @@ function initializeUploadCollapse() {
|
|
|
1173
1307
|
document.addEventListener('DOMContentLoaded', function () {
|
|
1174
1308
|
// Initialize collapsible upload section
|
|
1175
1309
|
initializeUploadCollapse();
|
|
1310
|
+
|
|
1311
|
+
const bucketSelectorEl = document.getElementById('bucketSelector');
|
|
1312
|
+
const bucketSwitchStatusEl = document.getElementById('bucketSwitchStatus');
|
|
1313
|
+
if (bucketSelectorEl && bucketSelectorEl.value) {
|
|
1314
|
+
activeBucket = bucketSelectorEl.value;
|
|
1315
|
+
}
|
|
1316
|
+
if (bucketSelectorEl) {
|
|
1317
|
+
bucketSelectorEl.addEventListener('change', function () {
|
|
1318
|
+
const nextBucket = String(this.value || '').trim();
|
|
1319
|
+
if (!nextBucket) return;
|
|
1320
|
+
if (nextBucket === activeBucket) return;
|
|
1321
|
+
|
|
1322
|
+
const nextParams = new URLSearchParams(window.location.search);
|
|
1323
|
+
nextParams.set('bucket', nextBucket);
|
|
1324
|
+
nextParams.delete('page');
|
|
1325
|
+
nextParams.delete('token');
|
|
1326
|
+
|
|
1327
|
+
this.disabled = true;
|
|
1328
|
+
this.setAttribute('aria-busy', 'true');
|
|
1329
|
+
if (bucketSwitchStatusEl) bucketSwitchStatusEl.style.display = 'inline';
|
|
1330
|
+
|
|
1331
|
+
const qs = nextParams.toString();
|
|
1332
|
+
window.location.href = `/mbkbucket${qs ? `?${qs}` : ''}`;
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1176
1335
|
|
|
1177
|
-
// Load initial files
|
|
1336
|
+
// Load initial files with persisted state.
|
|
1178
1337
|
const urlParams = new URLSearchParams(window.location.search);
|
|
1179
1338
|
const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || '';
|
|
1180
1339
|
const initialPage = parseInt(urlParams.get('page')) || 1;
|
|
1181
|
-
|
|
1340
|
+
const initialSearch = urlParams.get('search') || '';
|
|
1341
|
+
const initialSort = (urlParams.get('sort') || 'name').toLowerCase();
|
|
1342
|
+
const initialOrder = (urlParams.get('order') || 'asc').toLowerCase();
|
|
1343
|
+
const initialView = (urlParams.get('view') || currentViewMode).toLowerCase();
|
|
1344
|
+
const initialFilter = (urlParams.get('filter') || 'all').toLowerCase();
|
|
1345
|
+
|
|
1346
|
+
if (initialView === 'folder' || initialView === 'flat') {
|
|
1347
|
+
currentViewMode = initialView;
|
|
1348
|
+
}
|
|
1349
|
+
currentQuickFilter = ['all', 'media', 'docs', 'code'].includes(initialFilter) ? initialFilter : 'all';
|
|
1350
|
+
updateQuickFilterUI();
|
|
1351
|
+
|
|
1352
|
+
// Ensure view button state reflects restored mode before first render.
|
|
1353
|
+
if (currentViewMode === 'flat') {
|
|
1354
|
+
const btnFolder = document.getElementById('viewBtnFolder');
|
|
1355
|
+
const btnFlat = document.getElementById('viewBtnFlat');
|
|
1356
|
+
btnFlat?.classList.add('active', 'btn-light');
|
|
1357
|
+
btnFlat?.classList.remove('btn-outline-light');
|
|
1358
|
+
btnFolder?.classList.remove('active', 'btn-light');
|
|
1359
|
+
btnFolder?.classList.add('btn-outline-light');
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const searchInputInit = document.getElementById('searchInput');
|
|
1363
|
+
if (searchInputInit) searchInputInit.value = initialSearch;
|
|
1364
|
+
loadFiles(initialPage, initialPrefix, initialSearch, initialSort, initialOrder);
|
|
1182
1365
|
|
|
1183
1366
|
// Search behavior: Trigger when the Search button is clicked OR when the user presses Enter in the search input
|
|
1184
1367
|
const searchInputEl = document.getElementById('searchInput');
|
|
@@ -1201,6 +1384,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1201
1384
|
|
|
1202
1385
|
// Delegate view and delete button clicks to avoid inline JS with unescaped data
|
|
1203
1386
|
document.addEventListener('click', function (e) {
|
|
1387
|
+
const folderRow = e.target.closest('.folder-row');
|
|
1388
|
+
if (folderRow && !e.target.closest('.delete-btn')) {
|
|
1389
|
+
const folderPath = folderRow.dataset.folderPath;
|
|
1390
|
+
if (folderPath) loadFiles(1, folderPath);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1204
1394
|
const vbtn = e.target.closest('.view-btn');
|
|
1205
1395
|
if (vbtn) {
|
|
1206
1396
|
const keyEnc = vbtn.dataset.keyEnc;
|
|
@@ -1220,12 +1410,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1220
1410
|
}
|
|
1221
1411
|
|
|
1222
1412
|
const cbtn = e.target.closest('.copy-btn');
|
|
1223
|
-
console.log(cbtn);
|
|
1224
1413
|
if (cbtn) {
|
|
1225
1414
|
e.stopPropagation();
|
|
1226
1415
|
const keyEnc = cbtn.dataset.keyEnc;
|
|
1227
1416
|
if (!keyEnc) return;
|
|
1228
|
-
const link = `${window.location.origin}
|
|
1417
|
+
const link = `${window.location.origin}${getViewUrl(keyEnc)}`;
|
|
1229
1418
|
const fallbackCopy = () => {
|
|
1230
1419
|
try {
|
|
1231
1420
|
const ta = document.createElement('textarea');
|
|
@@ -1302,7 +1491,7 @@ function handleViewFile(key, filename, event) {
|
|
|
1302
1491
|
function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto') {
|
|
1303
1492
|
const modal = document.getElementById('previewModal');
|
|
1304
1493
|
if (!modal) {
|
|
1305
|
-
window.open(
|
|
1494
|
+
window.open(getViewUrl(encodedKey), '_blank', 'noopener,noreferrer');
|
|
1306
1495
|
return;
|
|
1307
1496
|
}
|
|
1308
1497
|
|
|
@@ -1316,8 +1505,9 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1316
1505
|
const fsBtn = document.getElementById('previewFullscreenBtn');
|
|
1317
1506
|
|
|
1318
1507
|
titleEl.textContent = filename;
|
|
1319
|
-
|
|
1320
|
-
|
|
1508
|
+
titleEl.title = filename;
|
|
1509
|
+
openBtn.href = getViewUrl(encodedKey);
|
|
1510
|
+
downloadBtn.href = getDownloadUrl(encodedKey);
|
|
1321
1511
|
// Ensure downloaded file has the original filename
|
|
1322
1512
|
downloadBtn.setAttribute('download', filename);
|
|
1323
1513
|
|
|
@@ -1345,7 +1535,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1345
1535
|
dialog.classList.add('preview-compact');
|
|
1346
1536
|
const img = document.createElement('img');
|
|
1347
1537
|
img.className = 'preview-img';
|
|
1348
|
-
img.src =
|
|
1538
|
+
img.src = getViewUrl(encodedKey);
|
|
1349
1539
|
img.alt = filename;
|
|
1350
1540
|
img.onload = () => { };
|
|
1351
1541
|
img.onerror = () => { contentEl.textContent = 'Could not load preview.'; };
|
|
@@ -1356,7 +1546,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1356
1546
|
const video = document.createElement('video');
|
|
1357
1547
|
video.className = 'preview-video';
|
|
1358
1548
|
video.controls = true;
|
|
1359
|
-
video.src =
|
|
1549
|
+
video.src = getViewUrl(encodedKey);
|
|
1360
1550
|
contentEl.innerHTML = '';
|
|
1361
1551
|
contentEl.appendChild(video);
|
|
1362
1552
|
} else if (effectiveMode === 'audio') {
|
|
@@ -1364,20 +1554,20 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1364
1554
|
const audio = document.createElement('audio');
|
|
1365
1555
|
audio.className = 'preview-audio';
|
|
1366
1556
|
audio.controls = true;
|
|
1367
|
-
audio.src =
|
|
1557
|
+
audio.src = getViewUrl(encodedKey);
|
|
1368
1558
|
contentEl.innerHTML = '';
|
|
1369
1559
|
contentEl.appendChild(audio);
|
|
1370
1560
|
} else if (effectiveMode === 'pdf') {
|
|
1371
1561
|
dialog.classList.add('preview-large');
|
|
1372
1562
|
const iframe = document.createElement('iframe');
|
|
1373
1563
|
iframe.className = 'preview-iframe';
|
|
1374
|
-
iframe.src =
|
|
1564
|
+
iframe.src = getViewUrl(encodedKey);
|
|
1375
1565
|
contentEl.innerHTML = '';
|
|
1376
1566
|
contentEl.appendChild(iframe);
|
|
1377
1567
|
} else if (effectiveMode === 'text') {
|
|
1378
1568
|
dialog.classList.add('preview-compact');
|
|
1379
1569
|
|
|
1380
|
-
fetch(
|
|
1570
|
+
fetch(getViewUrl(encodedKey))
|
|
1381
1571
|
.then(r => r.text())
|
|
1382
1572
|
.then(text => {
|
|
1383
1573
|
contentEl.innerHTML = '';
|
|
@@ -1388,7 +1578,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1388
1578
|
})
|
|
1389
1579
|
.catch(err => { contentEl.textContent = 'Could not load preview.'; });
|
|
1390
1580
|
} else {
|
|
1391
|
-
contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="
|
|
1581
|
+
contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="${getDownloadUrl(encodedKey)}">Download</a></div>`;
|
|
1392
1582
|
}
|
|
1393
1583
|
|
|
1394
1584
|
// Show modal
|
|
@@ -1567,7 +1757,7 @@ function setupSelection() {
|
|
|
1567
1757
|
if (!confirm('Delete selected files? This cannot be undone.')) return;
|
|
1568
1758
|
const keys = Array.from(selected);
|
|
1569
1759
|
try {
|
|
1570
|
-
const resp = await fetch('/mbkbucket/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
|
|
1760
|
+
const resp = await fetch(withBucketUrl('/mbkbucket/delete'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
|
|
1571
1761
|
const json = await resp.json();
|
|
1572
1762
|
if (json.success) {
|
|
1573
1763
|
showAlert('Deleted selected files', 'success');
|
|
@@ -1590,7 +1780,7 @@ async function createFolder() {
|
|
|
1590
1780
|
if (!safe) return showAlert('Invalid folder name', 'error');
|
|
1591
1781
|
|
|
1592
1782
|
try {
|
|
1593
|
-
const resp = await fetch('/mbkbucket/create-folder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
|
|
1783
|
+
const resp = await fetch(withBucketUrl('/mbkbucket/create-folder'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
|
|
1594
1784
|
const json = await resp.json();
|
|
1595
1785
|
if (json.success) {
|
|
1596
1786
|
showAlert('Folder created', 'success');
|
|
@@ -1626,7 +1816,7 @@ async function createFile() {
|
|
|
1626
1816
|
formData.append('prefix', currentPrefix);
|
|
1627
1817
|
|
|
1628
1818
|
try {
|
|
1629
|
-
const resp = await fetch('/mbkbucket/upload', {
|
|
1819
|
+
const resp = await fetch(withBucketUrl('/mbkbucket/upload'), {
|
|
1630
1820
|
method: 'POST',
|
|
1631
1821
|
body: formData
|
|
1632
1822
|
});
|