mbkbucket 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -2
- package/LICENSE +161 -335
- package/README.md +5 -4
- package/index.js +9 -9
- package/lib/config/index.js +27 -11
- package/lib/config/validation.js +5 -2
- package/lib/debug.js +12 -0
- package/lib/routes/api.js +53 -63
- package/lib/routes/index.js +2 -2
- package/lib/routes/view.js +40 -20
- package/lib/s3.js +44 -29
- package/package.json +12 -6
- package/public/bucketadmin.css +124 -1
- package/public/bucketadmin.js +182 -88
- package/views/bucket.handlebars +0 -1
- package/views/bucketadmincontent.handlebars +15 -15
package/public/bucketadmin.js
CHANGED
|
@@ -12,13 +12,16 @@ let currentApiPrefix = '';
|
|
|
12
12
|
const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
|
|
13
13
|
const selectedBucket = new URLSearchParams(window.location.search).get('bucket') || '';
|
|
14
14
|
let activeBucket = selectedBucket;
|
|
15
|
-
const
|
|
16
|
-
const rootLabel = isRootApp ? 'root' : (bucketAppName || 'app');
|
|
15
|
+
const app = String(bucketAppName || '').toLowerCase();
|
|
16
|
+
const isRootApp = app === 'portal' || app === 'mbkbucket';const rootLabel = isRootApp ? 'root' : (bucketAppName || 'app');
|
|
17
17
|
let currentQuickFilter = 'all';
|
|
18
18
|
let virtualRenderLimit = 0;
|
|
19
19
|
const VIRTUAL_RENDER_THRESHOLD = 500;
|
|
20
20
|
const VIRTUAL_RENDER_BATCH = 250;
|
|
21
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']);
|
|
22
25
|
|
|
23
26
|
function withBucketUrl(url) {
|
|
24
27
|
if (!activeBucket) return url;
|
|
@@ -72,6 +75,20 @@ function escapeAttr(value = '') {
|
|
|
72
75
|
return escapeHtml(value);
|
|
73
76
|
}
|
|
74
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
|
+
|
|
75
92
|
function syncStateUrl(page = currentPage, prefix = currentPrefix, search = currentSearch) {
|
|
76
93
|
try {
|
|
77
94
|
const parts = [];
|
|
@@ -100,14 +117,10 @@ function getActiveBucketLabel() {
|
|
|
100
117
|
}
|
|
101
118
|
|
|
102
119
|
function matchesQuickFilter(fileName = '', filter = currentQuickFilter) {
|
|
103
|
-
const ext =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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);
|
|
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);
|
|
111
124
|
return true;
|
|
112
125
|
}
|
|
113
126
|
|
|
@@ -155,9 +168,9 @@ function showAlert(message, type = 'success') {
|
|
|
155
168
|
`;
|
|
156
169
|
|
|
157
170
|
toast.innerHTML = `
|
|
158
|
-
<i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'}
|
|
159
|
-
<div
|
|
160
|
-
<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>
|
|
161
174
|
`;
|
|
162
175
|
|
|
163
176
|
container.appendChild(toast);
|
|
@@ -259,7 +272,7 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
259
272
|
<i class="fas fa-folder-open"></i>
|
|
260
273
|
<h4>No files found</h4>
|
|
261
274
|
<p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong> is empty. Upload some files to get started!</p>
|
|
262
|
-
<button class="btn btn-lg"
|
|
275
|
+
<button class="btn btn-lg" data-action="upload-files">
|
|
263
276
|
<i class="fas fa-plus"></i> Upload Your First File
|
|
264
277
|
</button>
|
|
265
278
|
</div>
|
|
@@ -284,13 +297,16 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
284
297
|
paginationContainer.style.display = 'block';
|
|
285
298
|
}
|
|
286
299
|
} catch (error) {
|
|
300
|
+
console.error("app: ", app);
|
|
301
|
+
console.error("isRootApp: ", isRootApp, "rootLabel: ", rootLabel);
|
|
302
|
+
console.error("bucketAppName : ", bucketAppName);
|
|
287
303
|
console.error('Error loading files:', error);
|
|
288
304
|
container.innerHTML = `
|
|
289
305
|
<div class="empty-state">
|
|
290
|
-
<i class="fas fa-exclamation-triangle
|
|
306
|
+
<i class="fas fa-exclamation-triangle empty-state-error-icon"></i>
|
|
291
307
|
<h4>Error Loading Files</h4>
|
|
292
308
|
<p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>: ${escapeHtml(error.message)}</p>
|
|
293
|
-
<button class="btn btn-primary"
|
|
309
|
+
<button class="btn btn-primary" data-action="retry-load" ${dataAttr('retry-page', page)} ${dataAttr('retry-prefix', prefix)}>
|
|
294
310
|
<i class="fas fa-redo"></i> Retry
|
|
295
311
|
</button>
|
|
296
312
|
</div>
|
|
@@ -319,8 +335,8 @@ function toggleSort(field) {
|
|
|
319
335
|
valA = new Date(a.LastModified || 0).getTime();
|
|
320
336
|
valB = new Date(b.LastModified || 0).getTime();
|
|
321
337
|
} else {
|
|
322
|
-
valA = (a.Key
|
|
323
|
-
valB = (b.Key
|
|
338
|
+
valA = getBaseName(a.Key).toLowerCase();
|
|
339
|
+
valB = getBaseName(b.Key).toLowerCase();
|
|
324
340
|
}
|
|
325
341
|
|
|
326
342
|
if (valA < valB) return currentOrder === 'asc' ? -1 : 1;
|
|
@@ -360,7 +376,6 @@ function setQuickFilter(filter = 'all') {
|
|
|
360
376
|
// Extract unique folders at current level
|
|
361
377
|
function extractFoldersAtCurrentLevel(files, prefix) {
|
|
362
378
|
const folderSet = new Set();
|
|
363
|
-
const prefixDepth = prefix ? prefix.split('/').filter(p => p).length : 0;
|
|
364
379
|
|
|
365
380
|
files.forEach(file => {
|
|
366
381
|
const key = file.Key;
|
|
@@ -447,6 +462,9 @@ function extractMatchingFolders(files, search) {
|
|
|
447
462
|
// Render files table
|
|
448
463
|
function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
449
464
|
lastRenderContext = { files, folders, prefix, apiPrefix };
|
|
465
|
+
const normalizedPrefix = String(prefix || '').replace(/\/+$/, '');
|
|
466
|
+
const basePrefix = apiPrefix || prefix;
|
|
467
|
+
const relativeBaseOffset = basePrefix ? basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1) : 0;
|
|
450
468
|
|
|
451
469
|
// Determine which files/folders to show based on view mode
|
|
452
470
|
let displayFiles = [];
|
|
@@ -466,7 +484,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
466
484
|
// Skip folder marker objects (keys ending with /)
|
|
467
485
|
if (f.Key.endsWith('/')) return false;
|
|
468
486
|
|
|
469
|
-
const fileName = f.Key
|
|
487
|
+
const fileName = getBaseName(f.Key);
|
|
470
488
|
return fileName.toLowerCase().includes(searchLower);
|
|
471
489
|
});
|
|
472
490
|
|
|
@@ -485,8 +503,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
485
503
|
// Skip folder marker objects (keys ending with /)
|
|
486
504
|
if (file.Key.endsWith('/')) return false;
|
|
487
505
|
|
|
488
|
-
const
|
|
489
|
-
const relativePath = basePrefix ? file.Key.substring(basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1)) : file.Key;
|
|
506
|
+
const relativePath = basePrefix ? file.Key.substring(relativeBaseOffset) : file.Key;
|
|
490
507
|
// File is at current level if it has no '/' in relative path
|
|
491
508
|
return !relativePath.includes('/');
|
|
492
509
|
});
|
|
@@ -495,7 +512,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
495
512
|
|
|
496
513
|
// Apply quick file-type filter after mode/path filtering.
|
|
497
514
|
displayFiles = displayFiles.filter((f) => {
|
|
498
|
-
const name =
|
|
515
|
+
const name = getBaseName(f?.Key);
|
|
499
516
|
return matchesQuickFilter(name, currentQuickFilter);
|
|
500
517
|
});
|
|
501
518
|
|
|
@@ -505,13 +522,15 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
505
522
|
// (In flat mode, the prefix acts as a base filter, but we see recursive files under it)
|
|
506
523
|
if (prefix) {
|
|
507
524
|
const parts = prefix.split('/').filter(p => p);
|
|
508
|
-
|
|
525
|
+
const safeRootLabel = escapeHtml(rootLabel);
|
|
526
|
+
const safeRootTitle = escapeAttr(rootLabel);
|
|
527
|
+
const breadcrumbParts = [`
|
|
509
528
|
<div class="breadcrumb-nav">
|
|
510
529
|
<div class="breadcrumb-item">
|
|
511
530
|
<i class="fas fa-home"></i>
|
|
512
|
-
<
|
|
531
|
+
<button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', '')} title="Go to ${safeRootTitle}">${safeRootLabel}</button>
|
|
513
532
|
</div>
|
|
514
|
-
|
|
533
|
+
`];
|
|
515
534
|
|
|
516
535
|
let currentPath = '';
|
|
517
536
|
parts.forEach((part, index) => {
|
|
@@ -519,51 +538,57 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
519
538
|
const isLast = index === parts.length - 1;
|
|
520
539
|
|
|
521
540
|
if (isLast) {
|
|
522
|
-
|
|
541
|
+
const safePart = escapeHtml(part);
|
|
542
|
+
breadcrumbParts.push(`
|
|
523
543
|
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
524
544
|
<div class="breadcrumb-item">
|
|
525
|
-
<span class="breadcrumb-current">${
|
|
545
|
+
<span class="breadcrumb-current">${safePart}</span>
|
|
526
546
|
</div>
|
|
527
|
-
|
|
547
|
+
`);
|
|
528
548
|
} else {
|
|
529
549
|
const pathCopy = currentPath;
|
|
530
|
-
|
|
550
|
+
const safePart = escapeHtml(part);
|
|
551
|
+
const safePathTitle = escapeAttr(pathCopy);
|
|
552
|
+
breadcrumbParts.push(`
|
|
531
553
|
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
532
554
|
<div class="breadcrumb-item">
|
|
533
|
-
<
|
|
555
|
+
<button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', pathCopy)} title="Go to ${safePathTitle}">${safePart}</button>
|
|
534
556
|
</div>
|
|
535
|
-
|
|
557
|
+
`);
|
|
536
558
|
}
|
|
537
559
|
});
|
|
538
560
|
|
|
539
|
-
|
|
540
|
-
breadcrumb =
|
|
561
|
+
breadcrumbParts.push(`</div>`);
|
|
562
|
+
breadcrumb = breadcrumbParts.join('');
|
|
541
563
|
}
|
|
542
564
|
|
|
543
565
|
// Generate folder rows (only used in folder mode)
|
|
544
566
|
const folderRows = displayFolders.map(folderPath => {
|
|
545
567
|
// Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
|
|
546
568
|
const rawFolderPath = String(folderPath || '');
|
|
547
|
-
const cleanPath = rawFolderPath.replace(/\/+$/, '');
|
|
548
|
-
const
|
|
549
|
-
|
|
569
|
+
const cleanPath = rawFolderPath.replace(/\/+$/, '');
|
|
570
|
+
const folderPathForAction = normalizedPrefix && cleanPath && !cleanPath.startsWith(`${normalizedPrefix}/`)
|
|
571
|
+
? `${normalizedPrefix}/${cleanPath}`
|
|
572
|
+
: cleanPath;
|
|
573
|
+
const folderKeyForDelete = `${folderPathForAction}/`;
|
|
574
|
+
const folderName = getBaseName(cleanPath);
|
|
550
575
|
const safeFolderName = escapeHtml(folderName);
|
|
551
|
-
const safeFolderPathAttr = escapeAttr(
|
|
576
|
+
const safeFolderPathAttr = escapeAttr(`${folderPathForAction}/`);
|
|
552
577
|
const safeFolderDeleteKeyAttr = escapeAttr(folderKeyForDelete);
|
|
553
578
|
|
|
554
579
|
return `
|
|
555
580
|
<tr class="folder-row" data-folder-path="${safeFolderPathAttr}" title="Open folder: ${safeFolderName}">
|
|
556
|
-
<td
|
|
581
|
+
<td class="table-cell-center">
|
|
557
582
|
<!-- no selection for folders -->
|
|
558
583
|
</td>
|
|
559
|
-
<td
|
|
560
|
-
<div
|
|
584
|
+
<td class="file-name-cell">
|
|
585
|
+
<div class="file-row-main">
|
|
561
586
|
<div class="file-icon">
|
|
562
587
|
<i class="fas fa-folder"></i>
|
|
563
588
|
</div>
|
|
564
589
|
<div class="file-info">
|
|
565
|
-
<div class="file-name
|
|
566
|
-
<i class="fas fa-folder
|
|
590
|
+
<div class="file-name folder-name">
|
|
591
|
+
<i class="fas fa-folder folder-leading-icon"></i>${safeFolderName}/
|
|
567
592
|
</div>
|
|
568
593
|
</div>
|
|
569
594
|
<button type="button" class="btn btn-danger btn-sm delete-btn dtm" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
@@ -572,12 +597,12 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
572
597
|
</div>
|
|
573
598
|
</td>
|
|
574
599
|
<td>
|
|
575
|
-
|
|
600
|
+
${mutedDash()}
|
|
576
601
|
</td>
|
|
577
|
-
<td
|
|
578
|
-
|
|
602
|
+
<td class="table-cell-center">
|
|
603
|
+
${mutedDash()}
|
|
579
604
|
</td>
|
|
580
|
-
<td
|
|
605
|
+
<td class="table-cell-center">
|
|
581
606
|
<div class="btn-group" role="group">
|
|
582
607
|
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
583
608
|
<i class="fas fa-trash"></i>
|
|
@@ -620,21 +645,23 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
620
645
|
const safeFilename = escapeHtml(filename);
|
|
621
646
|
const safeTruncatedName = escapeHtml(truncatedName);
|
|
622
647
|
const safeFolderPath = escapeHtml(folderPath);
|
|
648
|
+
const viewButtonClass = isFileViewable ? 'btn btn-primary btn-sm view-btn' : 'btn btn-primary btn-sm view-btn is-disabled';
|
|
649
|
+
const viewButtonDisabled = isFileViewable ? '' : 'disabled';
|
|
623
650
|
|
|
624
651
|
// If in flat mode, we might want to show the folder path more prominently
|
|
625
652
|
const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
|
|
626
653
|
? `<div class="file-path-display">
|
|
627
|
-
<i class="fas fa-folder
|
|
654
|
+
<i class="fas fa-folder path-display-icon"></i> ${safeFolderPath}
|
|
628
655
|
</div>`
|
|
629
656
|
: '';
|
|
630
657
|
|
|
631
658
|
return `
|
|
632
659
|
<tr class="file-row">
|
|
633
|
-
<td
|
|
660
|
+
<td class="table-cell-center">
|
|
634
661
|
<input type="checkbox" class="selectFileCheckbox" data-key="${safeKeyAttr}" aria-label="Select ${safeKeyAttr}" />
|
|
635
662
|
</td>
|
|
636
|
-
<td
|
|
637
|
-
<div
|
|
663
|
+
<td class="file-name-cell">
|
|
664
|
+
<div class="file-row-main">
|
|
638
665
|
<div class="file-icon ${typeCategory}">
|
|
639
666
|
<i class="${iconClass}"></i>
|
|
640
667
|
</div>
|
|
@@ -645,18 +672,18 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
645
672
|
</div>
|
|
646
673
|
</td>
|
|
647
674
|
<td>
|
|
648
|
-
${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span
|
|
675
|
+
${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span class="muted-cell-value">N/A</span>'}
|
|
649
676
|
</td>
|
|
650
677
|
<td>
|
|
651
|
-
${file.LastModified ? `<span
|
|
678
|
+
${file.LastModified ? `<span class="muted-cell-value">${formatDate(file.LastModified)}</span>` : '<span class="muted-cell-value">N/A</span>'}
|
|
652
679
|
</td>
|
|
653
|
-
<td
|
|
680
|
+
<td class="table-cell-center">
|
|
654
681
|
<div class="btn-group" role="group">
|
|
655
|
-
<button type="button" class="
|
|
656
|
-
data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${
|
|
682
|
+
<button type="button" class="${viewButtonClass}" data-key="${safeKeyAttr}" data-filename="${safeKeyAttr}"
|
|
683
|
+
data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${viewButtonDisabled}>
|
|
657
684
|
<i class="fas fa-eye"></i>
|
|
658
685
|
</button>
|
|
659
|
-
<button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link"
|
|
686
|
+
<button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link">
|
|
660
687
|
<i class="fas fa-link"></i>
|
|
661
688
|
</button>
|
|
662
689
|
<a href="${getDownloadUrl(encodedKey)}" class="btn btn-success btn-sm"
|
|
@@ -679,7 +706,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
679
706
|
${breadcrumb}
|
|
680
707
|
<div class="empty-state">
|
|
681
708
|
<div class="empty-state-illustration">
|
|
682
|
-
<i class="fas fa-folder-open
|
|
709
|
+
<i class="fas fa-folder-open empty-state-folder-icon"></i>
|
|
683
710
|
<i class="fas fa-search empty-state-search-icon"></i>
|
|
684
711
|
</div>
|
|
685
712
|
<h4>No files found</h4>
|
|
@@ -687,13 +714,13 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
687
714
|
${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.`}
|
|
688
715
|
</p>
|
|
689
716
|
<div class="empty-state-actions">
|
|
690
|
-
<button class="btn btn-primary"
|
|
717
|
+
<button class="btn btn-primary" data-action="upload-files">
|
|
691
718
|
<i class="fas fa-cloud-upload-alt"></i> Upload Files
|
|
692
719
|
</button>
|
|
693
|
-
<button class="btn btn-outline-secondary"
|
|
720
|
+
<button class="btn btn-outline-secondary" data-action="create-folder">
|
|
694
721
|
<i class="fas fa-folder-plus"></i> New Folder
|
|
695
722
|
</button>
|
|
696
|
-
${prefix ? `<button class="btn btn-light"
|
|
723
|
+
${prefix ? `<button class="btn btn-light" data-action="go-root">
|
|
697
724
|
<i class="fas fa-home"></i> Go to ${rootLabel}
|
|
698
725
|
</button>` : ''}
|
|
699
726
|
</div>
|
|
@@ -714,7 +741,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
714
741
|
? `<div class="virtualized-info">
|
|
715
742
|
<span>Showing ${visibleFiles.length} of ${displayFiles.length} files in this view.</span>
|
|
716
743
|
<div class="virtualized-actions">
|
|
717
|
-
${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary"
|
|
744
|
+
${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" data-action="expand-virtual">Load more</button>' : ''}
|
|
718
745
|
</div>
|
|
719
746
|
</div>`
|
|
720
747
|
: '';
|
|
@@ -724,7 +751,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
724
751
|
<div class="table-responsive">
|
|
725
752
|
<div id="bulkActionsBar" class="bulk-actions-bar">
|
|
726
753
|
<span id="bulkSelectedCount">0 selected</span>
|
|
727
|
-
<div
|
|
754
|
+
<div class="bulk-actions-controls">
|
|
728
755
|
<button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
|
|
729
756
|
<i class="fas fa-trash"></i> Delete Selected
|
|
730
757
|
</button>
|
|
@@ -733,19 +760,19 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
733
760
|
<table class="table table-hover mb-0">
|
|
734
761
|
<thead>
|
|
735
762
|
<tr>
|
|
736
|
-
<th
|
|
763
|
+
<th class="table-select-col">
|
|
737
764
|
<input type="checkbox" id="selectAllCheckbox" aria-label="Select all files" />
|
|
738
765
|
</th>
|
|
739
|
-
<th
|
|
766
|
+
<th class="sortable-heading file-name-heading" data-sort-field="name">
|
|
740
767
|
<i class="fas fa-file-alt"></i> Name ${getSortIcon('name')}
|
|
741
768
|
</th>
|
|
742
|
-
<th
|
|
769
|
+
<th class="sortable-heading" data-sort-field="size">
|
|
743
770
|
<i class="fas fa-weight"></i> Size ${getSortIcon('size')}
|
|
744
771
|
</th>
|
|
745
|
-
<th
|
|
772
|
+
<th class="sortable-heading" data-sort-field="date">
|
|
746
773
|
<i class="fas fa-calendar"></i> Last Modified ${getSortIcon('date')}
|
|
747
774
|
</th>
|
|
748
|
-
<th
|
|
775
|
+
<th class="table-cell-center">
|
|
749
776
|
<i class="fas fa-cogs"></i> Actions
|
|
750
777
|
</th>
|
|
751
778
|
</tr>
|
|
@@ -767,6 +794,17 @@ function renderPagination(data) {
|
|
|
767
794
|
const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
|
|
768
795
|
|
|
769
796
|
let paginationHTML = '';
|
|
797
|
+
const pageLink = (page, label, extra = {}) => {
|
|
798
|
+
const attrs = [
|
|
799
|
+
dataAttr('page', page),
|
|
800
|
+
dataAttr('prefix', currentPrefix),
|
|
801
|
+
dataAttr('search', currentSearch),
|
|
802
|
+
dataAttr('sort', extra.sort || currentSort),
|
|
803
|
+
dataAttr('order', extra.order || currentOrder)
|
|
804
|
+
];
|
|
805
|
+
if (extra.token) attrs.push(dataAttr('token', extra.token));
|
|
806
|
+
return `<a class="page-link" href="#" ${attrs.join(' ')}>${label}</a>`;
|
|
807
|
+
};
|
|
770
808
|
|
|
771
809
|
if (mode === 'optimized') {
|
|
772
810
|
// Optimized Mode: Token-based pagination (Next only, limited Prev)
|
|
@@ -789,13 +827,9 @@ function renderPagination(data) {
|
|
|
789
827
|
|
|
790
828
|
// Next button
|
|
791
829
|
if (hasNextPage) {
|
|
792
|
-
// Quote the token correctly
|
|
793
|
-
const tokenStr = currentNextToken ? currentNextToken.replace(/'/g, "\\'") : "";
|
|
794
830
|
paginationHTML += `
|
|
795
831
|
<li class="page-item">
|
|
796
|
-
|
|
797
|
-
Next <i class="fas fa-chevron-right"></i>
|
|
798
|
-
</a>
|
|
832
|
+
${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>', { token: currentNextToken || '' })}
|
|
799
833
|
</li>
|
|
800
834
|
`;
|
|
801
835
|
} else {
|
|
@@ -815,9 +849,7 @@ function renderPagination(data) {
|
|
|
815
849
|
if (hasPrevPage) {
|
|
816
850
|
paginationHTML += `
|
|
817
851
|
<li class="page-item">
|
|
818
|
-
|
|
819
|
-
<i class="fas fa-chevron-left"></i> Previous
|
|
820
|
-
</a>
|
|
852
|
+
${pageLink(currentPage - 1, '<i class="fas fa-chevron-left"></i> Previous')}
|
|
821
853
|
</li>
|
|
822
854
|
`;
|
|
823
855
|
} else {
|
|
@@ -843,7 +875,7 @@ function renderPagination(data) {
|
|
|
843
875
|
if (startPage > 1) {
|
|
844
876
|
paginationHTML += `
|
|
845
877
|
<li class="page-item">
|
|
846
|
-
|
|
878
|
+
${pageLink(1, '1')}
|
|
847
879
|
</li>`;
|
|
848
880
|
if (startPage > 2) {
|
|
849
881
|
paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
@@ -860,7 +892,7 @@ function renderPagination(data) {
|
|
|
860
892
|
} else {
|
|
861
893
|
paginationHTML += `
|
|
862
894
|
<li class="page-item">
|
|
863
|
-
|
|
895
|
+
${pageLink(i, String(i))}
|
|
864
896
|
</li>
|
|
865
897
|
`;
|
|
866
898
|
}
|
|
@@ -873,7 +905,7 @@ function renderPagination(data) {
|
|
|
873
905
|
}
|
|
874
906
|
paginationHTML += `
|
|
875
907
|
<li class="page-item">
|
|
876
|
-
|
|
908
|
+
${pageLink(totalPages, String(totalPages))}
|
|
877
909
|
</li>`;
|
|
878
910
|
}
|
|
879
911
|
|
|
@@ -881,9 +913,7 @@ function renderPagination(data) {
|
|
|
881
913
|
if (hasNextPage) {
|
|
882
914
|
paginationHTML += `
|
|
883
915
|
<li class="page-item">
|
|
884
|
-
|
|
885
|
-
Next <i class="fas fa-chevron-right"></i>
|
|
886
|
-
</a>
|
|
916
|
+
${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>')}
|
|
887
917
|
</li>
|
|
888
918
|
`;
|
|
889
919
|
} else {
|
|
@@ -976,7 +1006,7 @@ setTimeout(() => {
|
|
|
976
1006
|
// Upload queue, drag-and-drop and multi-file support
|
|
977
1007
|
const uploadQueue = []; // { id, file, status, progress, controller/xhr, uploadId }
|
|
978
1008
|
const xhrMap = new Map();
|
|
979
|
-
const CHUNK_SIZE =
|
|
1009
|
+
const CHUNK_SIZE = 50 * 1024 * 1024; // 50MB chunk threshold
|
|
980
1010
|
|
|
981
1011
|
function createId() { return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9); }
|
|
982
1012
|
|
|
@@ -1030,9 +1060,9 @@ function renderUploadQueue() {
|
|
|
1030
1060
|
<div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
|
|
1031
1061
|
</div>
|
|
1032
1062
|
</div>
|
|
1033
|
-
<div
|
|
1034
|
-
<div class="progress-container
|
|
1035
|
-
<div class="progress-bar" style="width: ${Math.round(item.progress)}%;
|
|
1063
|
+
<div class="upload-progress-cell">
|
|
1064
|
+
<div class="progress-container upload-progress-track">
|
|
1065
|
+
<div class="progress-bar upload-progress-bar" style="width: ${Math.round(item.progress)}%;"></div>
|
|
1036
1066
|
</div>
|
|
1037
1067
|
<div class="upload-item-status-row">
|
|
1038
1068
|
<small class="text-muted status">${statusText}</small>
|
|
@@ -1326,7 +1356,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1326
1356
|
|
|
1327
1357
|
this.disabled = true;
|
|
1328
1358
|
this.setAttribute('aria-busy', 'true');
|
|
1329
|
-
|
|
1359
|
+
bucketSwitchStatusEl?.classList.remove('is-hidden');
|
|
1330
1360
|
|
|
1331
1361
|
const qs = nextParams.toString();
|
|
1332
1362
|
window.location.href = `/mbkbucket${qs ? `?${qs}` : ''}`;
|
|
@@ -1384,6 +1414,70 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1384
1414
|
|
|
1385
1415
|
// Delegate view and delete button clicks to avoid inline JS with unescaped data
|
|
1386
1416
|
document.addEventListener('click', function (e) {
|
|
1417
|
+
const actionBtn = e.target.closest('[data-action]');
|
|
1418
|
+
if (actionBtn) {
|
|
1419
|
+
const action = actionBtn.dataset.action;
|
|
1420
|
+
if (action === 'upload-files') {
|
|
1421
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1422
|
+
document.getElementById('fileInput')?.click();
|
|
1423
|
+
} else if (action === 'create-folder') {
|
|
1424
|
+
createFolder();
|
|
1425
|
+
} else if (action === 'create-file') {
|
|
1426
|
+
createFile();
|
|
1427
|
+
} else if (action === 'go-root') {
|
|
1428
|
+
loadFiles(1, '');
|
|
1429
|
+
} else if (action === 'expand-virtual') {
|
|
1430
|
+
expandVirtualRows();
|
|
1431
|
+
} else if (action === 'search') {
|
|
1432
|
+
performSearch();
|
|
1433
|
+
} else if (action === 'clear-search') {
|
|
1434
|
+
clearSearch();
|
|
1435
|
+
} else if (action === 'retry-load') {
|
|
1436
|
+
loadFiles(parseInt(actionBtn.dataset.retryPage, 10) || 1, actionBtn.dataset.retryPrefix || '');
|
|
1437
|
+
} else if (action === 'close-toast') {
|
|
1438
|
+
actionBtn.closest('.toast-notification')?.remove();
|
|
1439
|
+
}
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const quickFilterBtn = e.target.closest('[data-filter]');
|
|
1444
|
+
if (quickFilterBtn) {
|
|
1445
|
+
setQuickFilter(quickFilterBtn.dataset.filter || 'all');
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const viewModeBtn = e.target.closest('[data-view-mode]');
|
|
1450
|
+
if (viewModeBtn) {
|
|
1451
|
+
changeViewMode(viewModeBtn.dataset.viewMode);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const navBtn = e.target.closest('[data-nav-prefix]');
|
|
1456
|
+
if (navBtn) {
|
|
1457
|
+
loadFiles(1, navBtn.dataset.navPrefix || '');
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const sortHeader = e.target.closest('[data-sort-field]');
|
|
1462
|
+
if (sortHeader) {
|
|
1463
|
+
toggleSort(sortHeader.dataset.sortField);
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const pageLink = e.target.closest('.page-link[data-page]');
|
|
1468
|
+
if (pageLink) {
|
|
1469
|
+
e.preventDefault();
|
|
1470
|
+
loadFiles(
|
|
1471
|
+
parseInt(pageLink.dataset.page, 10) || 1,
|
|
1472
|
+
pageLink.dataset.prefix || '',
|
|
1473
|
+
pageLink.dataset.search || '',
|
|
1474
|
+
pageLink.dataset.sort || currentSort,
|
|
1475
|
+
pageLink.dataset.order || currentOrder,
|
|
1476
|
+
pageLink.dataset.token || ''
|
|
1477
|
+
);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1387
1481
|
const folderRow = e.target.closest('.folder-row');
|
|
1388
1482
|
if (folderRow && !e.target.closest('.delete-btn')) {
|
|
1389
1483
|
const folderPath = folderRow.dataset.folderPath;
|
|
@@ -2005,4 +2099,4 @@ async function createFile() {
|
|
|
2005
2099
|
};
|
|
2006
2100
|
};
|
|
2007
2101
|
|
|
2008
|
-
})(window);
|
|
2102
|
+
})(window);
|
package/views/bucket.handlebars
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
11
11
|
<script src="/mbkauthe/main.js" defer></script>
|
|
12
|
-
<script src="https://portal.mbktech.org/Assets/Scripts/timeoutlogout.js" defer></script>
|
|
13
12
|
<script src="https://portal.mbktech.org/Assets/Scripts/auto-ids.js" defer></script>
|
|
14
13
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
|
15
14
|
<link rel="icon" type="image/x-icon" href="/icon.svg">
|