mbkbucket 1.5.1 → 2.1.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 +20 -20
- package/lib/config/index.js +34 -98
- package/lib/config/validation.js +89 -0
- package/lib/debug.js +12 -0
- package/lib/routes/api.js +147 -88
- package/lib/routes/index.js +70 -1
- package/lib/routes/view.js +108 -35
- package/lib/s3.js +102 -57
- package/package.json +6 -5
- package/public/bucketadmin.css +571 -18
- package/public/bucketadmin.js +431 -150
- package/test/config-validation.test.js +116 -0
- package/views/bucket.handlebars +66 -31
- package/views/bucketadmincontent.handlebars +40 -19
- package/views/bucketportal.handlebars +1 -0
package/public/bucketadmin.js
CHANGED
|
@@ -10,7 +10,32 @@ 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
|
+
const MEDIA_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'mp4', 'webm', 'ogg', 'avi', 'mov', 'mp3', 'wav', 'flac', 'aac', 'm4a']);
|
|
23
|
+
const DOC_EXTENSIONS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv']);
|
|
24
|
+
const CODE_EXTENSIONS = 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']);
|
|
25
|
+
|
|
26
|
+
function withBucketUrl(url) {
|
|
27
|
+
if (!activeBucket) return url;
|
|
28
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
29
|
+
return `${url}${separator}bucket=${encodeURIComponent(activeBucket)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getViewUrl(encodedKey) {
|
|
33
|
+
return withBucketUrl(`/mbkbucket/view/${encodedKey}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getDownloadUrl(encodedKey) {
|
|
37
|
+
return withBucketUrl(`/mbkbucket/download/${encodedKey}`);
|
|
38
|
+
}
|
|
14
39
|
|
|
15
40
|
function stripApiPrefixFromPath(path, apiPrefix = '') {
|
|
16
41
|
const normalizedPath = String(path || '').replace(/^\/+/, '');
|
|
@@ -37,6 +62,74 @@ function toApiFolderPath(prefix = '', apiPrefix = '') {
|
|
|
37
62
|
return `${normalizedApiPrefix}/${uiPrefix}`;
|
|
38
63
|
}
|
|
39
64
|
|
|
65
|
+
function escapeHtml(value = '') {
|
|
66
|
+
return String(value)
|
|
67
|
+
.replace(/&/g, '&')
|
|
68
|
+
.replace(/</g, '<')
|
|
69
|
+
.replace(/>/g, '>')
|
|
70
|
+
.replace(/"/g, '"')
|
|
71
|
+
.replace(/'/g, ''');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function escapeAttr(value = '') {
|
|
75
|
+
return escapeHtml(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getBaseName(path = '') {
|
|
79
|
+
const value = String(path || '');
|
|
80
|
+
const lastSlashIndex = value.lastIndexOf('/');
|
|
81
|
+
return lastSlashIndex === -1 ? value : value.substring(lastSlashIndex + 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function dataAttr(name, value = '') {
|
|
85
|
+
return `data-${name}="${escapeAttr(value)}"`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mutedDash() {
|
|
89
|
+
return '<span class="muted-cell-value">—</span>';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function syncStateUrl(page = currentPage, prefix = currentPrefix, search = currentSearch) {
|
|
93
|
+
try {
|
|
94
|
+
const parts = [];
|
|
95
|
+
if (activeBucket) parts.push(`bucket=${encodeURIComponent(activeBucket)}`);
|
|
96
|
+
if (prefix) {
|
|
97
|
+
const folderPathForUrl = toApiFolderPath(prefix, currentApiPrefix);
|
|
98
|
+
const folderEncoded = encodeURIComponent(folderPathForUrl).replace(/%2F/g, '/');
|
|
99
|
+
parts.push(`folder=${folderEncoded}`);
|
|
100
|
+
}
|
|
101
|
+
if (page && page > 1) parts.push(`page=${page}`);
|
|
102
|
+
if (search) parts.push(`search=${encodeURIComponent(search)}`);
|
|
103
|
+
parts.push(`sort=${encodeURIComponent(currentSort)}`);
|
|
104
|
+
parts.push(`order=${encodeURIComponent(currentOrder)}`);
|
|
105
|
+
parts.push(`view=${encodeURIComponent(currentViewMode)}`);
|
|
106
|
+
parts.push(`filter=${encodeURIComponent(currentQuickFilter)}`);
|
|
107
|
+
|
|
108
|
+
const newUrl = '/mbkbucket' + (parts.length ? '?' + parts.join('&') : '');
|
|
109
|
+
window.history.replaceState(null, '', newUrl);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.warn('history update failed', e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getActiveBucketLabel() {
|
|
116
|
+
return activeBucket || 'default bucket';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function matchesQuickFilter(fileName = '', filter = currentQuickFilter) {
|
|
120
|
+
const ext = getBaseName(fileName).split('.').pop().toLowerCase();
|
|
121
|
+
if (filter === 'media') return MEDIA_EXTENSIONS.has(ext);
|
|
122
|
+
if (filter === 'docs') return DOC_EXTENSIONS.has(ext);
|
|
123
|
+
if (filter === 'code') return CODE_EXTENSIONS.has(ext);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function updateQuickFilterUI() {
|
|
128
|
+
document.querySelectorAll('.quick-filter-btn').forEach((btn) => {
|
|
129
|
+
btn.classList.toggle('active', btn.dataset.filter === currentQuickFilter);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
40
133
|
// Show alert message (Toast)
|
|
41
134
|
function showAlert(message, type = 'success') {
|
|
42
135
|
const container = document.getElementById('toast-container');
|
|
@@ -75,9 +168,9 @@ function showAlert(message, type = 'success') {
|
|
|
75
168
|
`;
|
|
76
169
|
|
|
77
170
|
toast.innerHTML = `
|
|
78
|
-
<i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'}
|
|
79
|
-
<div
|
|
80
|
-
<button
|
|
171
|
+
<i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'} toast-icon"></i>
|
|
172
|
+
<div class="toast-message">${message}</div>
|
|
173
|
+
<button type="button" data-action="close-toast" class="toast-close-btn">×</button>
|
|
81
174
|
`;
|
|
82
175
|
|
|
83
176
|
container.appendChild(toast);
|
|
@@ -103,26 +196,10 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
103
196
|
currentSearch = search;
|
|
104
197
|
currentSort = sort;
|
|
105
198
|
currentOrder = order;
|
|
199
|
+
virtualRenderLimit = 0;
|
|
106
200
|
|
|
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
|
-
}
|
|
201
|
+
// Update browser URL so navigation is shareable/bookmarkable.
|
|
202
|
+
syncStateUrl(page, prefix, search);
|
|
126
203
|
|
|
127
204
|
const container = document.getElementById('fileListContainer');
|
|
128
205
|
const paginationContainer = document.getElementById('paginationContainer');
|
|
@@ -157,7 +234,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
157
234
|
const recursiveParam = `&recursive=${!isFolderMode}`; // false if folder mode, true if flat mode
|
|
158
235
|
const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
|
|
159
236
|
|
|
160
|
-
const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`);
|
|
237
|
+
const response = await fetch(withBucketUrl(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`));
|
|
161
238
|
const data = await response.json();
|
|
162
239
|
|
|
163
240
|
if (!data.success) {
|
|
@@ -176,11 +253,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
176
253
|
}
|
|
177
254
|
|
|
178
255
|
if (search) {
|
|
179
|
-
|
|
256
|
+
const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
|
|
257
|
+
filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Search: "${search}"${filterSuffix}`;
|
|
180
258
|
fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
|
|
181
259
|
} else {
|
|
182
|
-
const label = prefix ||
|
|
183
|
-
|
|
260
|
+
const label = prefix || rootLabel;
|
|
261
|
+
const filterSuffix = currentQuickFilter !== 'all' ? ` • Filter: ${currentQuickFilter}` : '';
|
|
262
|
+
filterInfo.textContent = `Bucket: ${getActiveBucketLabel()} • Folder: ${label}${filterSuffix}`;
|
|
184
263
|
fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
|
|
185
264
|
}
|
|
186
265
|
|
|
@@ -192,8 +271,8 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
192
271
|
<div class="empty-state">
|
|
193
272
|
<i class="fas fa-folder-open"></i>
|
|
194
273
|
<h4>No files found</h4>
|
|
195
|
-
|
|
196
|
-
<button class="btn btn-lg"
|
|
274
|
+
<p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Upload some files to get started!</p>
|
|
275
|
+
<button class="btn btn-lg" data-action="upload-files">
|
|
197
276
|
<i class="fas fa-plus"></i> Upload Your First File
|
|
198
277
|
</button>
|
|
199
278
|
</div>
|
|
@@ -221,10 +300,10 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
221
300
|
console.error('Error loading files:', error);
|
|
222
301
|
container.innerHTML = `
|
|
223
302
|
<div class="empty-state">
|
|
224
|
-
<i class="fas fa-exclamation-triangle
|
|
303
|
+
<i class="fas fa-exclamation-triangle empty-state-error-icon"></i>
|
|
225
304
|
<h4>Error Loading Files</h4>
|
|
226
|
-
|
|
227
|
-
<button class="btn btn-primary"
|
|
305
|
+
<p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>: ${escapeHtml(error.message)}</p>
|
|
306
|
+
<button class="btn btn-primary" data-action="retry-load" ${dataAttr('retry-page', page)} ${dataAttr('retry-prefix', prefix)}>
|
|
228
307
|
<i class="fas fa-redo"></i> Retry
|
|
229
308
|
</button>
|
|
230
309
|
</div>
|
|
@@ -253,8 +332,8 @@ function toggleSort(field) {
|
|
|
253
332
|
valA = new Date(a.LastModified || 0).getTime();
|
|
254
333
|
valB = new Date(b.LastModified || 0).getTime();
|
|
255
334
|
} else {
|
|
256
|
-
valA = (a.Key
|
|
257
|
-
valB = (b.Key
|
|
335
|
+
valA = getBaseName(a.Key).toLowerCase();
|
|
336
|
+
valB = getBaseName(b.Key).toLowerCase();
|
|
258
337
|
}
|
|
259
338
|
|
|
260
339
|
if (valA < valB) return currentOrder === 'asc' ? -1 : 1;
|
|
@@ -263,6 +342,7 @@ function toggleSort(field) {
|
|
|
263
342
|
});
|
|
264
343
|
|
|
265
344
|
renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
|
|
345
|
+
syncStateUrl(currentPage, currentPrefix, currentSearch);
|
|
266
346
|
}
|
|
267
347
|
}
|
|
268
348
|
|
|
@@ -281,10 +361,18 @@ function clearSearch() {
|
|
|
281
361
|
loadFiles(1, currentPrefix, '');
|
|
282
362
|
}
|
|
283
363
|
|
|
364
|
+
function setQuickFilter(filter = 'all') {
|
|
365
|
+
const nextFilter = String(filter || 'all').toLowerCase();
|
|
366
|
+
if (currentQuickFilter === nextFilter) return;
|
|
367
|
+
currentQuickFilter = nextFilter;
|
|
368
|
+
updateQuickFilterUI();
|
|
369
|
+
renderFilesTable(pageFiles, pageFolders, currentPrefix, currentApiPrefix);
|
|
370
|
+
syncStateUrl(currentPage, currentPrefix, currentSearch);
|
|
371
|
+
}
|
|
372
|
+
|
|
284
373
|
// Extract unique folders at current level
|
|
285
374
|
function extractFoldersAtCurrentLevel(files, prefix) {
|
|
286
375
|
const folderSet = new Set();
|
|
287
|
-
const prefixDepth = prefix ? prefix.split('/').filter(p => p).length : 0;
|
|
288
376
|
|
|
289
377
|
files.forEach(file => {
|
|
290
378
|
const key = file.Key;
|
|
@@ -330,8 +418,14 @@ function changeViewMode(mode) {
|
|
|
330
418
|
btnFolder.classList.add('btn-outline-light');
|
|
331
419
|
}
|
|
332
420
|
|
|
333
|
-
//
|
|
334
|
-
|
|
421
|
+
// Refetch to respect recursive/non-recursive mode and keep URL state.
|
|
422
|
+
loadFiles(1, currentPrefix, currentSearch, currentSort, currentOrder);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function expandVirtualRows() {
|
|
426
|
+
if (!lastRenderContext) return;
|
|
427
|
+
virtualRenderLimit += VIRTUAL_RENDER_BATCH;
|
|
428
|
+
renderFilesTable(lastRenderContext.files, lastRenderContext.folders, lastRenderContext.prefix, lastRenderContext.apiPrefix);
|
|
335
429
|
}
|
|
336
430
|
|
|
337
431
|
// Extract folders that match a search query from file paths
|
|
@@ -364,6 +458,11 @@ function extractMatchingFolders(files, search) {
|
|
|
364
458
|
|
|
365
459
|
// Render files table
|
|
366
460
|
function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
461
|
+
lastRenderContext = { files, folders, prefix, apiPrefix };
|
|
462
|
+
const normalizedPrefix = String(prefix || '').replace(/\/+$/, '');
|
|
463
|
+
const basePrefix = apiPrefix || prefix;
|
|
464
|
+
const relativeBaseOffset = basePrefix ? basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1) : 0;
|
|
465
|
+
|
|
367
466
|
// Determine which files/folders to show based on view mode
|
|
368
467
|
let displayFiles = [];
|
|
369
468
|
let displayFolders = [];
|
|
@@ -382,7 +481,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
382
481
|
// Skip folder marker objects (keys ending with /)
|
|
383
482
|
if (f.Key.endsWith('/')) return false;
|
|
384
483
|
|
|
385
|
-
const fileName = f.Key
|
|
484
|
+
const fileName = getBaseName(f.Key);
|
|
386
485
|
return fileName.toLowerCase().includes(searchLower);
|
|
387
486
|
});
|
|
388
487
|
|
|
@@ -401,27 +500,34 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
401
500
|
// Skip folder marker objects (keys ending with /)
|
|
402
501
|
if (file.Key.endsWith('/')) return false;
|
|
403
502
|
|
|
404
|
-
const
|
|
405
|
-
const relativePath = basePrefix ? file.Key.substring(basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1)) : file.Key;
|
|
503
|
+
const relativePath = basePrefix ? file.Key.substring(relativeBaseOffset) : file.Key;
|
|
406
504
|
// File is at current level if it has no '/' in relative path
|
|
407
505
|
return !relativePath.includes('/');
|
|
408
506
|
});
|
|
409
507
|
displayFolders = folders;
|
|
410
508
|
}
|
|
411
509
|
|
|
510
|
+
// Apply quick file-type filter after mode/path filtering.
|
|
511
|
+
displayFiles = displayFiles.filter((f) => {
|
|
512
|
+
const name = getBaseName(f?.Key);
|
|
513
|
+
return matchesQuickFilter(name, currentQuickFilter);
|
|
514
|
+
});
|
|
515
|
+
|
|
412
516
|
// Generate breadcrumb navigation
|
|
413
517
|
let breadcrumb = '';
|
|
414
518
|
// Show breadcrumb only if we are in folder mode OR if we have a prefix filter active even in flat mode
|
|
415
519
|
// (In flat mode, the prefix acts as a base filter, but we see recursive files under it)
|
|
416
520
|
if (prefix) {
|
|
417
521
|
const parts = prefix.split('/').filter(p => p);
|
|
418
|
-
|
|
522
|
+
const safeRootLabel = escapeHtml(rootLabel);
|
|
523
|
+
const safeRootTitle = escapeAttr(rootLabel);
|
|
524
|
+
const breadcrumbParts = [`
|
|
419
525
|
<div class="breadcrumb-nav">
|
|
420
526
|
<div class="breadcrumb-item">
|
|
421
527
|
<i class="fas fa-home"></i>
|
|
422
|
-
<
|
|
528
|
+
<button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', '')} title="Go to ${safeRootTitle}">${safeRootLabel}</button>
|
|
423
529
|
</div>
|
|
424
|
-
|
|
530
|
+
`];
|
|
425
531
|
|
|
426
532
|
let currentPath = '';
|
|
427
533
|
parts.forEach((part, index) => {
|
|
@@ -429,62 +535,73 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
429
535
|
const isLast = index === parts.length - 1;
|
|
430
536
|
|
|
431
537
|
if (isLast) {
|
|
432
|
-
|
|
538
|
+
const safePart = escapeHtml(part);
|
|
539
|
+
breadcrumbParts.push(`
|
|
433
540
|
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
434
541
|
<div class="breadcrumb-item">
|
|
435
|
-
<span class="breadcrumb-current">${
|
|
542
|
+
<span class="breadcrumb-current">${safePart}</span>
|
|
436
543
|
</div>
|
|
437
|
-
|
|
544
|
+
`);
|
|
438
545
|
} else {
|
|
439
546
|
const pathCopy = currentPath;
|
|
440
|
-
|
|
547
|
+
const safePart = escapeHtml(part);
|
|
548
|
+
const safePathTitle = escapeAttr(pathCopy);
|
|
549
|
+
breadcrumbParts.push(`
|
|
441
550
|
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
442
551
|
<div class="breadcrumb-item">
|
|
443
|
-
<
|
|
552
|
+
<button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', pathCopy)} title="Go to ${safePathTitle}">${safePart}</button>
|
|
444
553
|
</div>
|
|
445
|
-
|
|
554
|
+
`);
|
|
446
555
|
}
|
|
447
556
|
});
|
|
448
557
|
|
|
449
|
-
|
|
450
|
-
breadcrumb =
|
|
558
|
+
breadcrumbParts.push(`</div>`);
|
|
559
|
+
breadcrumb = breadcrumbParts.join('');
|
|
451
560
|
}
|
|
452
561
|
|
|
453
562
|
// Generate folder rows (only used in folder mode)
|
|
454
|
-
|
|
563
|
+
const folderRows = displayFolders.map(folderPath => {
|
|
455
564
|
// Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
|
|
456
|
-
|
|
457
|
-
|
|
565
|
+
const rawFolderPath = String(folderPath || '');
|
|
566
|
+
const cleanPath = rawFolderPath.replace(/\/+$/, '');
|
|
567
|
+
const folderPathForAction = normalizedPrefix && cleanPath && !cleanPath.startsWith(`${normalizedPrefix}/`)
|
|
568
|
+
? `${normalizedPrefix}/${cleanPath}`
|
|
569
|
+
: cleanPath;
|
|
570
|
+
const folderKeyForDelete = `${folderPathForAction}/`;
|
|
571
|
+
const folderName = getBaseName(cleanPath);
|
|
572
|
+
const safeFolderName = escapeHtml(folderName);
|
|
573
|
+
const safeFolderPathAttr = escapeAttr(`${folderPathForAction}/`);
|
|
574
|
+
const safeFolderDeleteKeyAttr = escapeAttr(folderKeyForDelete);
|
|
458
575
|
|
|
459
576
|
return `
|
|
460
|
-
|
|
461
|
-
<td
|
|
577
|
+
<tr class="folder-row" data-folder-path="${safeFolderPathAttr}" title="Open folder: ${safeFolderName}">
|
|
578
|
+
<td class="table-cell-center">
|
|
462
579
|
<!-- no selection for folders -->
|
|
463
580
|
</td>
|
|
464
|
-
<td
|
|
465
|
-
<div
|
|
581
|
+
<td class="file-name-cell">
|
|
582
|
+
<div class="file-row-main">
|
|
466
583
|
<div class="file-icon">
|
|
467
584
|
<i class="fas fa-folder"></i>
|
|
468
585
|
</div>
|
|
469
586
|
<div class="file-info">
|
|
470
|
-
<div class="file-name
|
|
471
|
-
|
|
587
|
+
<div class="file-name folder-name">
|
|
588
|
+
<i class="fas fa-folder folder-leading-icon"></i>${safeFolderName}/
|
|
472
589
|
</div>
|
|
473
590
|
</div>
|
|
474
|
-
|
|
591
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn dtm" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
475
592
|
<i class="fas fa-trash"></i>
|
|
476
593
|
</button>
|
|
477
594
|
</div>
|
|
478
595
|
</td>
|
|
479
596
|
<td>
|
|
480
|
-
|
|
597
|
+
${mutedDash()}
|
|
481
598
|
</td>
|
|
482
|
-
<td
|
|
483
|
-
|
|
599
|
+
<td class="table-cell-center">
|
|
600
|
+
${mutedDash()}
|
|
484
601
|
</td>
|
|
485
|
-
<td
|
|
602
|
+
<td class="table-cell-center">
|
|
486
603
|
<div class="btn-group" role="group">
|
|
487
|
-
|
|
604
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
488
605
|
<i class="fas fa-trash"></i>
|
|
489
606
|
</button>
|
|
490
607
|
</div>
|
|
@@ -493,12 +610,22 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
493
610
|
`;
|
|
494
611
|
}).join('');
|
|
495
612
|
|
|
613
|
+
const isVirtualized = displayFiles.length > VIRTUAL_RENDER_THRESHOLD;
|
|
614
|
+
if (!isVirtualized) {
|
|
615
|
+
virtualRenderLimit = 0;
|
|
616
|
+
} else if (!virtualRenderLimit) {
|
|
617
|
+
virtualRenderLimit = VIRTUAL_RENDER_BATCH;
|
|
618
|
+
}
|
|
619
|
+
const visibleFiles = isVirtualized ? displayFiles.slice(0, virtualRenderLimit) : displayFiles;
|
|
620
|
+
|
|
496
621
|
// Generate file rows
|
|
497
|
-
const fileRows =
|
|
498
|
-
const
|
|
622
|
+
const fileRows = visibleFiles.map(file => {
|
|
623
|
+
const rawKey = String(file.Key || '');
|
|
624
|
+
const encodedKey = encodeURIComponent(rawKey);
|
|
625
|
+
const safeKeyAttr = escapeAttr(rawKey);
|
|
499
626
|
|
|
500
627
|
// Calculate display name and icon directly
|
|
501
|
-
const fullPath =
|
|
628
|
+
const fullPath = rawKey;
|
|
502
629
|
const lastSlashIndex = fullPath.lastIndexOf('/');
|
|
503
630
|
let filename = fullPath;
|
|
504
631
|
let folderPath = '';
|
|
@@ -512,50 +639,55 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
512
639
|
const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(filename) : '';
|
|
513
640
|
const truncatedName = (window.truncateFileName ? window.truncateFileName(filename) : filename) || filename;
|
|
514
641
|
const isFileViewable = window.isViewable ? window.isViewable(filename) : false;
|
|
642
|
+
const safeFilename = escapeHtml(filename);
|
|
643
|
+
const safeTruncatedName = escapeHtml(truncatedName);
|
|
644
|
+
const safeFolderPath = escapeHtml(folderPath);
|
|
645
|
+
const viewButtonClass = isFileViewable ? 'btn btn-primary btn-sm view-btn' : 'btn btn-primary btn-sm view-btn is-disabled';
|
|
646
|
+
const viewButtonDisabled = isFileViewable ? '' : 'disabled';
|
|
515
647
|
|
|
516
648
|
// If in flat mode, we might want to show the folder path more prominently
|
|
517
649
|
const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
|
|
518
650
|
? `<div class="file-path-display">
|
|
519
|
-
|
|
651
|
+
<i class="fas fa-folder path-display-icon"></i> ${safeFolderPath}
|
|
520
652
|
</div>`
|
|
521
653
|
: '';
|
|
522
654
|
|
|
523
655
|
return `
|
|
524
656
|
<tr class="file-row">
|
|
525
|
-
<td
|
|
526
|
-
|
|
657
|
+
<td class="table-cell-center">
|
|
658
|
+
<input type="checkbox" class="selectFileCheckbox" data-key="${safeKeyAttr}" aria-label="Select ${safeKeyAttr}" />
|
|
527
659
|
</td>
|
|
528
|
-
<td
|
|
529
|
-
<div
|
|
660
|
+
<td class="file-name-cell">
|
|
661
|
+
<div class="file-row-main">
|
|
530
662
|
<div class="file-icon ${typeCategory}">
|
|
531
663
|
<i class="${iconClass}"></i>
|
|
532
664
|
</div>
|
|
533
665
|
<div class="file-info">
|
|
534
666
|
${pathDisplay}
|
|
535
|
-
|
|
667
|
+
<div class="file-name" data-full-path="${safeKeyAttr}" title="${safeFilename}">${safeTruncatedName}</div>
|
|
536
668
|
</div>
|
|
537
669
|
</div>
|
|
538
670
|
</td>
|
|
539
671
|
<td>
|
|
540
|
-
${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span
|
|
672
|
+
${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span class="muted-cell-value">N/A</span>'}
|
|
541
673
|
</td>
|
|
542
674
|
<td>
|
|
543
|
-
${file.LastModified ? `<span
|
|
675
|
+
${file.LastModified ? `<span class="muted-cell-value">${formatDate(file.LastModified)}</span>` : '<span class="muted-cell-value">N/A</span>'}
|
|
544
676
|
</td>
|
|
545
|
-
<td
|
|
677
|
+
<td class="table-cell-center">
|
|
546
678
|
<div class="btn-group" role="group">
|
|
547
|
-
|
|
548
|
-
data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${
|
|
679
|
+
<button type="button" class="${viewButtonClass}" data-key="${safeKeyAttr}" data-filename="${safeKeyAttr}"
|
|
680
|
+
data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${viewButtonDisabled}>
|
|
549
681
|
<i class="fas fa-eye"></i>
|
|
550
682
|
</button>
|
|
551
|
-
<button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link"
|
|
683
|
+
<button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link">
|
|
552
684
|
<i class="fas fa-link"></i>
|
|
553
685
|
</button>
|
|
554
|
-
|
|
686
|
+
<a href="${getDownloadUrl(encodedKey)}" class="btn btn-success btn-sm"
|
|
555
687
|
title="Download file">
|
|
556
688
|
<i class="fas fa-download"></i>
|
|
557
689
|
</a>
|
|
558
|
-
|
|
690
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeKeyAttr}" title="Delete file">
|
|
559
691
|
<i class="fas fa-trash"></i>
|
|
560
692
|
</button>
|
|
561
693
|
</div>
|
|
@@ -571,22 +703,22 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
571
703
|
${breadcrumb}
|
|
572
704
|
<div class="empty-state">
|
|
573
705
|
<div class="empty-state-illustration">
|
|
574
|
-
<i class="fas fa-folder-open
|
|
706
|
+
<i class="fas fa-folder-open empty-state-folder-icon"></i>
|
|
575
707
|
<i class="fas fa-search empty-state-search-icon"></i>
|
|
576
708
|
</div>
|
|
577
709
|
<h4>No files found</h4>
|
|
578
710
|
<p class="empty-state-description">
|
|
579
|
-
|
|
711
|
+
${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
712
|
</p>
|
|
581
713
|
<div class="empty-state-actions">
|
|
582
|
-
<button class="btn btn-primary"
|
|
714
|
+
<button class="btn btn-primary" data-action="upload-files">
|
|
583
715
|
<i class="fas fa-cloud-upload-alt"></i> Upload Files
|
|
584
716
|
</button>
|
|
585
|
-
<button class="btn btn-outline-secondary"
|
|
717
|
+
<button class="btn btn-outline-secondary" data-action="create-folder">
|
|
586
718
|
<i class="fas fa-folder-plus"></i> New Folder
|
|
587
719
|
</button>
|
|
588
|
-
${prefix ? `<button class="btn btn-light"
|
|
589
|
-
<i class="fas fa-home"></i> Go to ${
|
|
720
|
+
${prefix ? `<button class="btn btn-light" data-action="go-root">
|
|
721
|
+
<i class="fas fa-home"></i> Go to ${rootLabel}
|
|
590
722
|
</button>` : ''}
|
|
591
723
|
</div>
|
|
592
724
|
</div>
|
|
@@ -602,12 +734,21 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
602
734
|
: '<i class="fas fa-sort-down sort-icon sort-icon-active"></i>';
|
|
603
735
|
};
|
|
604
736
|
|
|
605
|
-
|
|
737
|
+
const virtualizedInfo = isVirtualized
|
|
738
|
+
? `<div class="virtualized-info">
|
|
739
|
+
<span>Showing ${visibleFiles.length} of ${displayFiles.length} files in this view.</span>
|
|
740
|
+
<div class="virtualized-actions">
|
|
741
|
+
${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" data-action="expand-virtual">Load more</button>' : ''}
|
|
742
|
+
</div>
|
|
743
|
+
</div>`
|
|
744
|
+
: '';
|
|
745
|
+
|
|
746
|
+
document.getElementById('fileListContainer').innerHTML = `
|
|
606
747
|
${breadcrumb}
|
|
607
748
|
<div class="table-responsive">
|
|
608
749
|
<div id="bulkActionsBar" class="bulk-actions-bar">
|
|
609
750
|
<span id="bulkSelectedCount">0 selected</span>
|
|
610
|
-
<div
|
|
751
|
+
<div class="bulk-actions-controls">
|
|
611
752
|
<button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
|
|
612
753
|
<i class="fas fa-trash"></i> Delete Selected
|
|
613
754
|
</button>
|
|
@@ -616,19 +757,19 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
616
757
|
<table class="table table-hover mb-0">
|
|
617
758
|
<thead>
|
|
618
759
|
<tr>
|
|
619
|
-
<th
|
|
760
|
+
<th class="table-select-col">
|
|
620
761
|
<input type="checkbox" id="selectAllCheckbox" aria-label="Select all files" />
|
|
621
762
|
</th>
|
|
622
|
-
<th
|
|
763
|
+
<th class="sortable-heading file-name-heading" data-sort-field="name">
|
|
623
764
|
<i class="fas fa-file-alt"></i> Name ${getSortIcon('name')}
|
|
624
765
|
</th>
|
|
625
|
-
<th
|
|
766
|
+
<th class="sortable-heading" data-sort-field="size">
|
|
626
767
|
<i class="fas fa-weight"></i> Size ${getSortIcon('size')}
|
|
627
768
|
</th>
|
|
628
|
-
<th
|
|
769
|
+
<th class="sortable-heading" data-sort-field="date">
|
|
629
770
|
<i class="fas fa-calendar"></i> Last Modified ${getSortIcon('date')}
|
|
630
771
|
</th>
|
|
631
|
-
<th
|
|
772
|
+
<th class="table-cell-center">
|
|
632
773
|
<i class="fas fa-cogs"></i> Actions
|
|
633
774
|
</th>
|
|
634
775
|
</tr>
|
|
@@ -637,6 +778,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
637
778
|
${allRows}
|
|
638
779
|
</tbody>
|
|
639
780
|
</table>
|
|
781
|
+
${virtualizedInfo}
|
|
640
782
|
</div>
|
|
641
783
|
`;
|
|
642
784
|
|
|
@@ -649,6 +791,17 @@ function renderPagination(data) {
|
|
|
649
791
|
const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
|
|
650
792
|
|
|
651
793
|
let paginationHTML = '';
|
|
794
|
+
const pageLink = (page, label, extra = {}) => {
|
|
795
|
+
const attrs = [
|
|
796
|
+
dataAttr('page', page),
|
|
797
|
+
dataAttr('prefix', currentPrefix),
|
|
798
|
+
dataAttr('search', currentSearch),
|
|
799
|
+
dataAttr('sort', extra.sort || currentSort),
|
|
800
|
+
dataAttr('order', extra.order || currentOrder)
|
|
801
|
+
];
|
|
802
|
+
if (extra.token) attrs.push(dataAttr('token', extra.token));
|
|
803
|
+
return `<a class="page-link" href="#" ${attrs.join(' ')}>${label}</a>`;
|
|
804
|
+
};
|
|
652
805
|
|
|
653
806
|
if (mode === 'optimized') {
|
|
654
807
|
// Optimized Mode: Token-based pagination (Next only, limited Prev)
|
|
@@ -671,13 +824,9 @@ function renderPagination(data) {
|
|
|
671
824
|
|
|
672
825
|
// Next button
|
|
673
826
|
if (hasNextPage) {
|
|
674
|
-
// Quote the token correctly
|
|
675
|
-
const tokenStr = currentNextToken ? currentNextToken.replace(/'/g, "\\'") : "";
|
|
676
827
|
paginationHTML += `
|
|
677
828
|
<li class="page-item">
|
|
678
|
-
|
|
679
|
-
Next <i class="fas fa-chevron-right"></i>
|
|
680
|
-
</a>
|
|
829
|
+
${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>', { token: currentNextToken || '' })}
|
|
681
830
|
</li>
|
|
682
831
|
`;
|
|
683
832
|
} else {
|
|
@@ -697,9 +846,7 @@ function renderPagination(data) {
|
|
|
697
846
|
if (hasPrevPage) {
|
|
698
847
|
paginationHTML += `
|
|
699
848
|
<li class="page-item">
|
|
700
|
-
|
|
701
|
-
<i class="fas fa-chevron-left"></i> Previous
|
|
702
|
-
</a>
|
|
849
|
+
${pageLink(currentPage - 1, '<i class="fas fa-chevron-left"></i> Previous')}
|
|
703
850
|
</li>
|
|
704
851
|
`;
|
|
705
852
|
} else {
|
|
@@ -725,7 +872,7 @@ function renderPagination(data) {
|
|
|
725
872
|
if (startPage > 1) {
|
|
726
873
|
paginationHTML += `
|
|
727
874
|
<li class="page-item">
|
|
728
|
-
|
|
875
|
+
${pageLink(1, '1')}
|
|
729
876
|
</li>`;
|
|
730
877
|
if (startPage > 2) {
|
|
731
878
|
paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
@@ -742,7 +889,7 @@ function renderPagination(data) {
|
|
|
742
889
|
} else {
|
|
743
890
|
paginationHTML += `
|
|
744
891
|
<li class="page-item">
|
|
745
|
-
|
|
892
|
+
${pageLink(i, String(i))}
|
|
746
893
|
</li>
|
|
747
894
|
`;
|
|
748
895
|
}
|
|
@@ -755,7 +902,7 @@ function renderPagination(data) {
|
|
|
755
902
|
}
|
|
756
903
|
paginationHTML += `
|
|
757
904
|
<li class="page-item">
|
|
758
|
-
|
|
905
|
+
${pageLink(totalPages, String(totalPages))}
|
|
759
906
|
</li>`;
|
|
760
907
|
}
|
|
761
908
|
|
|
@@ -763,9 +910,7 @@ function renderPagination(data) {
|
|
|
763
910
|
if (hasNextPage) {
|
|
764
911
|
paginationHTML += `
|
|
765
912
|
<li class="page-item">
|
|
766
|
-
|
|
767
|
-
Next <i class="fas fa-chevron-right"></i>
|
|
768
|
-
</a>
|
|
913
|
+
${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>')}
|
|
769
914
|
</li>
|
|
770
915
|
`;
|
|
771
916
|
} else {
|
|
@@ -819,7 +964,7 @@ document.getElementById('deleteForm')?.addEventListener('submit', async function
|
|
|
819
964
|
const isFolder = document.getElementById('deleteIsFolder').value === 'true';
|
|
820
965
|
|
|
821
966
|
try {
|
|
822
|
-
const response = await fetch('/mbkbucket/delete', {
|
|
967
|
+
const response = await fetch(withBucketUrl('/mbkbucket/delete'), {
|
|
823
968
|
method: 'POST',
|
|
824
969
|
headers: {
|
|
825
970
|
'Content-Type': 'application/json'
|
|
@@ -858,7 +1003,7 @@ setTimeout(() => {
|
|
|
858
1003
|
// Upload queue, drag-and-drop and multi-file support
|
|
859
1004
|
const uploadQueue = []; // { id, file, status, progress, controller/xhr, uploadId }
|
|
860
1005
|
const xhrMap = new Map();
|
|
861
|
-
const CHUNK_SIZE =
|
|
1006
|
+
const CHUNK_SIZE = 50 * 1024 * 1024; // 50MB chunk threshold
|
|
862
1007
|
|
|
863
1008
|
function createId() { return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9); }
|
|
864
1009
|
|
|
@@ -889,6 +1034,9 @@ function renderUploadQueue() {
|
|
|
889
1034
|
}
|
|
890
1035
|
|
|
891
1036
|
container.innerHTML = uploadQueue.map(item => {
|
|
1037
|
+
const safeName = escapeHtml(item.file.name);
|
|
1038
|
+
const iconClass = window.getFileIcon ? window.getFileIcon(item.file.name) : 'fas fa-file-alt';
|
|
1039
|
+
const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(item.file.name) : '';
|
|
892
1040
|
let statusText;
|
|
893
1041
|
if (item.status === 'uploading') {
|
|
894
1042
|
const pct = Math.round(item.progress || 0);
|
|
@@ -904,14 +1052,14 @@ function renderUploadQueue() {
|
|
|
904
1052
|
<div class="upload-item" data-id="${item.id}">
|
|
905
1053
|
<div class="meta">
|
|
906
1054
|
<div class="upload-item-meta-row">
|
|
907
|
-
|
|
908
|
-
|
|
1055
|
+
<div class="file-icon upload-file-icon ${typeCategory}"><i class="${iconClass}"></i></div>
|
|
1056
|
+
<div class="name upload-item-name" title="${safeName}">${safeName}</div>
|
|
909
1057
|
<div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
|
|
910
1058
|
</div>
|
|
911
1059
|
</div>
|
|
912
|
-
<div
|
|
913
|
-
<div class="progress-container
|
|
914
|
-
<div class="progress-bar" style="width: ${Math.round(item.progress)}%;
|
|
1060
|
+
<div class="upload-progress-cell">
|
|
1061
|
+
<div class="progress-container upload-progress-track">
|
|
1062
|
+
<div class="progress-bar upload-progress-bar" style="width: ${Math.round(item.progress)}%;"></div>
|
|
915
1063
|
</div>
|
|
916
1064
|
<div class="upload-item-status-row">
|
|
917
1065
|
<small class="text-muted status">${statusText}</small>
|
|
@@ -942,7 +1090,7 @@ function renderUploadQueue() {
|
|
|
942
1090
|
// Tell S3 to discard the incomplete multipart upload
|
|
943
1091
|
if (entry && entry.s3UploadId && entry.s3Key) {
|
|
944
1092
|
try {
|
|
945
|
-
await fetch('/mbkbucket/upload-abort', {
|
|
1093
|
+
await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
946
1094
|
method: 'POST',
|
|
947
1095
|
headers: { 'Content-Type': 'application/json' },
|
|
948
1096
|
body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
|
|
@@ -988,7 +1136,7 @@ async function startUploadEntry(entry) {
|
|
|
988
1136
|
// S3 Multipart Upload
|
|
989
1137
|
// Step 1 — Initiate: get uploadId + key from the server
|
|
990
1138
|
// ---------------------------------------------------------------
|
|
991
|
-
const initResp = await fetch('/mbkbucket/upload-init', {
|
|
1139
|
+
const initResp = await fetch(withBucketUrl('/mbkbucket/upload-init'), {
|
|
992
1140
|
method: 'POST',
|
|
993
1141
|
headers: { 'Content-Type': 'application/json' },
|
|
994
1142
|
body: JSON.stringify({
|
|
@@ -1015,7 +1163,7 @@ async function startUploadEntry(entry) {
|
|
|
1015
1163
|
const _chunkStart = Date.now();
|
|
1016
1164
|
if (entry.status === 'cancelled') {
|
|
1017
1165
|
// Clean up on S3
|
|
1018
|
-
await fetch('/mbkbucket/upload-abort', {
|
|
1166
|
+
await fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
1019
1167
|
method: 'POST',
|
|
1020
1168
|
headers: { 'Content-Type': 'application/json' },
|
|
1021
1169
|
body: JSON.stringify({ uploadId, key })
|
|
@@ -1036,7 +1184,7 @@ async function startUploadEntry(entry) {
|
|
|
1036
1184
|
const controller = new AbortController();
|
|
1037
1185
|
xhrMap.set(entry.id, { controller, s3UploadId: uploadId, s3Key: key });
|
|
1038
1186
|
|
|
1039
|
-
const chunkResp = await fetch('/mbkbucket/upload-chunk', {
|
|
1187
|
+
const chunkResp = await fetch(withBucketUrl('/mbkbucket/upload-chunk'), {
|
|
1040
1188
|
method: 'POST',
|
|
1041
1189
|
body: fd,
|
|
1042
1190
|
signal: controller.signal
|
|
@@ -1056,7 +1204,7 @@ async function startUploadEntry(entry) {
|
|
|
1056
1204
|
// ---------------------------------------------------------------
|
|
1057
1205
|
// Step 3 — Complete: assemble all parts on S3
|
|
1058
1206
|
// ---------------------------------------------------------------
|
|
1059
|
-
const completeResp = await fetch('/mbkbucket/upload-complete', {
|
|
1207
|
+
const completeResp = await fetch(withBucketUrl('/mbkbucket/upload-complete'), {
|
|
1060
1208
|
method: 'POST',
|
|
1061
1209
|
headers: { 'Content-Type': 'application/json' },
|
|
1062
1210
|
body: JSON.stringify({ uploadId, key, parts })
|
|
@@ -1076,7 +1224,7 @@ async function startUploadEntry(entry) {
|
|
|
1076
1224
|
fd.append('prefix', currentPrefix);
|
|
1077
1225
|
|
|
1078
1226
|
const xhr = new XMLHttpRequest();
|
|
1079
|
-
xhr.open('POST', '/mbkbucket/upload');
|
|
1227
|
+
xhr.open('POST', withBucketUrl('/mbkbucket/upload'));
|
|
1080
1228
|
|
|
1081
1229
|
xhr.upload.addEventListener('progress', (e) => {
|
|
1082
1230
|
if (!e.lengthComputable) return;
|
|
@@ -1123,7 +1271,7 @@ async function startUploadEntry(entry) {
|
|
|
1123
1271
|
console.error('Upload failed for', entry.file.name, err);
|
|
1124
1272
|
// If a multipart upload was initiated but didn't finish (not a user cancel), abort it on S3
|
|
1125
1273
|
if (entry.s3UploadId && entry.s3Key && entry.status !== 'cancelled') {
|
|
1126
|
-
fetch('/mbkbucket/upload-abort', {
|
|
1274
|
+
fetch(withBucketUrl('/mbkbucket/upload-abort'), {
|
|
1127
1275
|
method: 'POST',
|
|
1128
1276
|
headers: { 'Content-Type': 'application/json' },
|
|
1129
1277
|
body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
|
|
@@ -1151,20 +1299,33 @@ document.getElementById('clearQueueBtn')?.addEventListener('click', function ()
|
|
|
1151
1299
|
function initializeUploadCollapse() {
|
|
1152
1300
|
const toggleBtn = document.getElementById('uploadToggleBtn');
|
|
1153
1301
|
const uploadSection = document.getElementById('uploadSection');
|
|
1302
|
+
const uploadCard = uploadSection?.closest('.dashboard-card');
|
|
1154
1303
|
|
|
1155
1304
|
if (!toggleBtn || !uploadSection) return;
|
|
1305
|
+
|
|
1306
|
+
const syncUploadCollapseState = (expanded) => {
|
|
1307
|
+
uploadSection.classList.toggle('expanded', expanded);
|
|
1308
|
+
toggleBtn.classList.toggle('expanded', expanded);
|
|
1309
|
+
toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|
1310
|
+
uploadSection.setAttribute('aria-hidden', expanded ? 'false' : 'true');
|
|
1311
|
+
if (uploadCard) uploadCard.classList.toggle('upload-panel-open', expanded);
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
toggleBtn.setAttribute('role', 'button');
|
|
1315
|
+
toggleBtn.setAttribute('tabindex', '0');
|
|
1316
|
+
toggleBtn.setAttribute('aria-controls', 'uploadSection');
|
|
1317
|
+
syncUploadCollapseState(uploadSection.classList.contains('expanded'));
|
|
1156
1318
|
|
|
1157
1319
|
toggleBtn.addEventListener('click', function() {
|
|
1158
1320
|
const isExpanded = uploadSection.classList.contains('expanded');
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
toggleBtn.classList.add('expanded');
|
|
1321
|
+
syncUploadCollapseState(!isExpanded);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
toggleBtn.addEventListener('keydown', function (event) {
|
|
1325
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1326
|
+
event.preventDefault();
|
|
1327
|
+
const isExpanded = uploadSection.classList.contains('expanded');
|
|
1328
|
+
syncUploadCollapseState(!isExpanded);
|
|
1168
1329
|
}
|
|
1169
1330
|
});
|
|
1170
1331
|
}
|
|
@@ -1173,12 +1334,61 @@ function initializeUploadCollapse() {
|
|
|
1173
1334
|
document.addEventListener('DOMContentLoaded', function () {
|
|
1174
1335
|
// Initialize collapsible upload section
|
|
1175
1336
|
initializeUploadCollapse();
|
|
1337
|
+
|
|
1338
|
+
const bucketSelectorEl = document.getElementById('bucketSelector');
|
|
1339
|
+
const bucketSwitchStatusEl = document.getElementById('bucketSwitchStatus');
|
|
1340
|
+
if (bucketSelectorEl && bucketSelectorEl.value) {
|
|
1341
|
+
activeBucket = bucketSelectorEl.value;
|
|
1342
|
+
}
|
|
1343
|
+
if (bucketSelectorEl) {
|
|
1344
|
+
bucketSelectorEl.addEventListener('change', function () {
|
|
1345
|
+
const nextBucket = String(this.value || '').trim();
|
|
1346
|
+
if (!nextBucket) return;
|
|
1347
|
+
if (nextBucket === activeBucket) return;
|
|
1348
|
+
|
|
1349
|
+
const nextParams = new URLSearchParams(window.location.search);
|
|
1350
|
+
nextParams.set('bucket', nextBucket);
|
|
1351
|
+
nextParams.delete('page');
|
|
1352
|
+
nextParams.delete('token');
|
|
1353
|
+
|
|
1354
|
+
this.disabled = true;
|
|
1355
|
+
this.setAttribute('aria-busy', 'true');
|
|
1356
|
+
bucketSwitchStatusEl?.classList.remove('is-hidden');
|
|
1357
|
+
|
|
1358
|
+
const qs = nextParams.toString();
|
|
1359
|
+
window.location.href = `/mbkbucket${qs ? `?${qs}` : ''}`;
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1176
1362
|
|
|
1177
|
-
// Load initial files
|
|
1363
|
+
// Load initial files with persisted state.
|
|
1178
1364
|
const urlParams = new URLSearchParams(window.location.search);
|
|
1179
1365
|
const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || '';
|
|
1180
1366
|
const initialPage = parseInt(urlParams.get('page')) || 1;
|
|
1181
|
-
|
|
1367
|
+
const initialSearch = urlParams.get('search') || '';
|
|
1368
|
+
const initialSort = (urlParams.get('sort') || 'name').toLowerCase();
|
|
1369
|
+
const initialOrder = (urlParams.get('order') || 'asc').toLowerCase();
|
|
1370
|
+
const initialView = (urlParams.get('view') || currentViewMode).toLowerCase();
|
|
1371
|
+
const initialFilter = (urlParams.get('filter') || 'all').toLowerCase();
|
|
1372
|
+
|
|
1373
|
+
if (initialView === 'folder' || initialView === 'flat') {
|
|
1374
|
+
currentViewMode = initialView;
|
|
1375
|
+
}
|
|
1376
|
+
currentQuickFilter = ['all', 'media', 'docs', 'code'].includes(initialFilter) ? initialFilter : 'all';
|
|
1377
|
+
updateQuickFilterUI();
|
|
1378
|
+
|
|
1379
|
+
// Ensure view button state reflects restored mode before first render.
|
|
1380
|
+
if (currentViewMode === 'flat') {
|
|
1381
|
+
const btnFolder = document.getElementById('viewBtnFolder');
|
|
1382
|
+
const btnFlat = document.getElementById('viewBtnFlat');
|
|
1383
|
+
btnFlat?.classList.add('active', 'btn-light');
|
|
1384
|
+
btnFlat?.classList.remove('btn-outline-light');
|
|
1385
|
+
btnFolder?.classList.remove('active', 'btn-light');
|
|
1386
|
+
btnFolder?.classList.add('btn-outline-light');
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const searchInputInit = document.getElementById('searchInput');
|
|
1390
|
+
if (searchInputInit) searchInputInit.value = initialSearch;
|
|
1391
|
+
loadFiles(initialPage, initialPrefix, initialSearch, initialSort, initialOrder);
|
|
1182
1392
|
|
|
1183
1393
|
// Search behavior: Trigger when the Search button is clicked OR when the user presses Enter in the search input
|
|
1184
1394
|
const searchInputEl = document.getElementById('searchInput');
|
|
@@ -1201,6 +1411,77 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1201
1411
|
|
|
1202
1412
|
// Delegate view and delete button clicks to avoid inline JS with unescaped data
|
|
1203
1413
|
document.addEventListener('click', function (e) {
|
|
1414
|
+
const actionBtn = e.target.closest('[data-action]');
|
|
1415
|
+
if (actionBtn) {
|
|
1416
|
+
const action = actionBtn.dataset.action;
|
|
1417
|
+
if (action === 'upload-files') {
|
|
1418
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1419
|
+
document.getElementById('fileInput')?.click();
|
|
1420
|
+
} else if (action === 'create-folder') {
|
|
1421
|
+
createFolder();
|
|
1422
|
+
} else if (action === 'create-file') {
|
|
1423
|
+
createFile();
|
|
1424
|
+
} else if (action === 'go-root') {
|
|
1425
|
+
loadFiles(1, '');
|
|
1426
|
+
} else if (action === 'expand-virtual') {
|
|
1427
|
+
expandVirtualRows();
|
|
1428
|
+
} else if (action === 'search') {
|
|
1429
|
+
performSearch();
|
|
1430
|
+
} else if (action === 'clear-search') {
|
|
1431
|
+
clearSearch();
|
|
1432
|
+
} else if (action === 'retry-load') {
|
|
1433
|
+
loadFiles(parseInt(actionBtn.dataset.retryPage, 10) || 1, actionBtn.dataset.retryPrefix || '');
|
|
1434
|
+
} else if (action === 'close-toast') {
|
|
1435
|
+
actionBtn.closest('.toast-notification')?.remove();
|
|
1436
|
+
}
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const quickFilterBtn = e.target.closest('[data-filter]');
|
|
1441
|
+
if (quickFilterBtn) {
|
|
1442
|
+
setQuickFilter(quickFilterBtn.dataset.filter || 'all');
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const viewModeBtn = e.target.closest('[data-view-mode]');
|
|
1447
|
+
if (viewModeBtn) {
|
|
1448
|
+
changeViewMode(viewModeBtn.dataset.viewMode);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const navBtn = e.target.closest('[data-nav-prefix]');
|
|
1453
|
+
if (navBtn) {
|
|
1454
|
+
loadFiles(1, navBtn.dataset.navPrefix || '');
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const sortHeader = e.target.closest('[data-sort-field]');
|
|
1459
|
+
if (sortHeader) {
|
|
1460
|
+
toggleSort(sortHeader.dataset.sortField);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const pageLink = e.target.closest('.page-link[data-page]');
|
|
1465
|
+
if (pageLink) {
|
|
1466
|
+
e.preventDefault();
|
|
1467
|
+
loadFiles(
|
|
1468
|
+
parseInt(pageLink.dataset.page, 10) || 1,
|
|
1469
|
+
pageLink.dataset.prefix || '',
|
|
1470
|
+
pageLink.dataset.search || '',
|
|
1471
|
+
pageLink.dataset.sort || currentSort,
|
|
1472
|
+
pageLink.dataset.order || currentOrder,
|
|
1473
|
+
pageLink.dataset.token || ''
|
|
1474
|
+
);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const folderRow = e.target.closest('.folder-row');
|
|
1479
|
+
if (folderRow && !e.target.closest('.delete-btn')) {
|
|
1480
|
+
const folderPath = folderRow.dataset.folderPath;
|
|
1481
|
+
if (folderPath) loadFiles(1, folderPath);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1204
1485
|
const vbtn = e.target.closest('.view-btn');
|
|
1205
1486
|
if (vbtn) {
|
|
1206
1487
|
const keyEnc = vbtn.dataset.keyEnc;
|
|
@@ -1220,12 +1501,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1220
1501
|
}
|
|
1221
1502
|
|
|
1222
1503
|
const cbtn = e.target.closest('.copy-btn');
|
|
1223
|
-
console.log(cbtn);
|
|
1224
1504
|
if (cbtn) {
|
|
1225
1505
|
e.stopPropagation();
|
|
1226
1506
|
const keyEnc = cbtn.dataset.keyEnc;
|
|
1227
1507
|
if (!keyEnc) return;
|
|
1228
|
-
const link = `${window.location.origin}
|
|
1508
|
+
const link = `${window.location.origin}${getViewUrl(keyEnc)}`;
|
|
1229
1509
|
const fallbackCopy = () => {
|
|
1230
1510
|
try {
|
|
1231
1511
|
const ta = document.createElement('textarea');
|
|
@@ -1302,7 +1582,7 @@ function handleViewFile(key, filename, event) {
|
|
|
1302
1582
|
function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto') {
|
|
1303
1583
|
const modal = document.getElementById('previewModal');
|
|
1304
1584
|
if (!modal) {
|
|
1305
|
-
window.open(
|
|
1585
|
+
window.open(getViewUrl(encodedKey), '_blank', 'noopener,noreferrer');
|
|
1306
1586
|
return;
|
|
1307
1587
|
}
|
|
1308
1588
|
|
|
@@ -1316,8 +1596,9 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1316
1596
|
const fsBtn = document.getElementById('previewFullscreenBtn');
|
|
1317
1597
|
|
|
1318
1598
|
titleEl.textContent = filename;
|
|
1319
|
-
|
|
1320
|
-
|
|
1599
|
+
titleEl.title = filename;
|
|
1600
|
+
openBtn.href = getViewUrl(encodedKey);
|
|
1601
|
+
downloadBtn.href = getDownloadUrl(encodedKey);
|
|
1321
1602
|
// Ensure downloaded file has the original filename
|
|
1322
1603
|
downloadBtn.setAttribute('download', filename);
|
|
1323
1604
|
|
|
@@ -1345,7 +1626,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1345
1626
|
dialog.classList.add('preview-compact');
|
|
1346
1627
|
const img = document.createElement('img');
|
|
1347
1628
|
img.className = 'preview-img';
|
|
1348
|
-
img.src =
|
|
1629
|
+
img.src = getViewUrl(encodedKey);
|
|
1349
1630
|
img.alt = filename;
|
|
1350
1631
|
img.onload = () => { };
|
|
1351
1632
|
img.onerror = () => { contentEl.textContent = 'Could not load preview.'; };
|
|
@@ -1356,7 +1637,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1356
1637
|
const video = document.createElement('video');
|
|
1357
1638
|
video.className = 'preview-video';
|
|
1358
1639
|
video.controls = true;
|
|
1359
|
-
video.src =
|
|
1640
|
+
video.src = getViewUrl(encodedKey);
|
|
1360
1641
|
contentEl.innerHTML = '';
|
|
1361
1642
|
contentEl.appendChild(video);
|
|
1362
1643
|
} else if (effectiveMode === 'audio') {
|
|
@@ -1364,20 +1645,20 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1364
1645
|
const audio = document.createElement('audio');
|
|
1365
1646
|
audio.className = 'preview-audio';
|
|
1366
1647
|
audio.controls = true;
|
|
1367
|
-
audio.src =
|
|
1648
|
+
audio.src = getViewUrl(encodedKey);
|
|
1368
1649
|
contentEl.innerHTML = '';
|
|
1369
1650
|
contentEl.appendChild(audio);
|
|
1370
1651
|
} else if (effectiveMode === 'pdf') {
|
|
1371
1652
|
dialog.classList.add('preview-large');
|
|
1372
1653
|
const iframe = document.createElement('iframe');
|
|
1373
1654
|
iframe.className = 'preview-iframe';
|
|
1374
|
-
iframe.src =
|
|
1655
|
+
iframe.src = getViewUrl(encodedKey);
|
|
1375
1656
|
contentEl.innerHTML = '';
|
|
1376
1657
|
contentEl.appendChild(iframe);
|
|
1377
1658
|
} else if (effectiveMode === 'text') {
|
|
1378
1659
|
dialog.classList.add('preview-compact');
|
|
1379
1660
|
|
|
1380
|
-
fetch(
|
|
1661
|
+
fetch(getViewUrl(encodedKey))
|
|
1381
1662
|
.then(r => r.text())
|
|
1382
1663
|
.then(text => {
|
|
1383
1664
|
contentEl.innerHTML = '';
|
|
@@ -1388,7 +1669,7 @@ function showPreviewModal(encodedKey, filename, extension, previewMode = 'auto')
|
|
|
1388
1669
|
})
|
|
1389
1670
|
.catch(err => { contentEl.textContent = 'Could not load preview.'; });
|
|
1390
1671
|
} else {
|
|
1391
|
-
contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="
|
|
1672
|
+
contentEl.innerHTML = `<div style="text-align:center;">Preview not available for this file type. <a href="${getDownloadUrl(encodedKey)}">Download</a></div>`;
|
|
1392
1673
|
}
|
|
1393
1674
|
|
|
1394
1675
|
// Show modal
|
|
@@ -1567,7 +1848,7 @@ function setupSelection() {
|
|
|
1567
1848
|
if (!confirm('Delete selected files? This cannot be undone.')) return;
|
|
1568
1849
|
const keys = Array.from(selected);
|
|
1569
1850
|
try {
|
|
1570
|
-
const resp = await fetch('/mbkbucket/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
|
|
1851
|
+
const resp = await fetch(withBucketUrl('/mbkbucket/delete'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys }) });
|
|
1571
1852
|
const json = await resp.json();
|
|
1572
1853
|
if (json.success) {
|
|
1573
1854
|
showAlert('Deleted selected files', 'success');
|
|
@@ -1590,7 +1871,7 @@ async function createFolder() {
|
|
|
1590
1871
|
if (!safe) return showAlert('Invalid folder name', 'error');
|
|
1591
1872
|
|
|
1592
1873
|
try {
|
|
1593
|
-
const resp = await fetch('/mbkbucket/create-folder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
|
|
1874
|
+
const resp = await fetch(withBucketUrl('/mbkbucket/create-folder'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: currentPrefix, folderName: safe }) });
|
|
1594
1875
|
const json = await resp.json();
|
|
1595
1876
|
if (json.success) {
|
|
1596
1877
|
showAlert('Folder created', 'success');
|
|
@@ -1626,7 +1907,7 @@ async function createFile() {
|
|
|
1626
1907
|
formData.append('prefix', currentPrefix);
|
|
1627
1908
|
|
|
1628
1909
|
try {
|
|
1629
|
-
const resp = await fetch('/mbkbucket/upload', {
|
|
1910
|
+
const resp = await fetch(withBucketUrl('/mbkbucket/upload'), {
|
|
1630
1911
|
method: 'POST',
|
|
1631
1912
|
body: formData
|
|
1632
1913
|
});
|
|
@@ -1815,4 +2096,4 @@ async function createFile() {
|
|
|
1815
2096
|
};
|
|
1816
2097
|
};
|
|
1817
2098
|
|
|
1818
|
-
})(window);
|
|
2099
|
+
})(window);
|