mbkbucket 2.0.0 → 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/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 +41 -51
- package/lib/routes/view.js +37 -17
- package/lib/s3.js +41 -28
- package/package.json +5 -4
- package/public/bucketadmin.css +124 -1
- package/public/bucketadmin.js +177 -86
- package/views/bucketadmincontent.handlebars +15 -15
package/public/bucketadmin.js
CHANGED
|
@@ -19,6 +19,9 @@ 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>
|
|
@@ -287,10 +300,10 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
287
300
|
console.error('Error loading files:', error);
|
|
288
301
|
container.innerHTML = `
|
|
289
302
|
<div class="empty-state">
|
|
290
|
-
<i class="fas fa-exclamation-triangle
|
|
303
|
+
<i class="fas fa-exclamation-triangle empty-state-error-icon"></i>
|
|
291
304
|
<h4>Error Loading Files</h4>
|
|
292
305
|
<p>Bucket <strong>${escapeHtml(getActiveBucketLabel())}</strong>: ${escapeHtml(error.message)}</p>
|
|
293
|
-
<button class="btn btn-primary"
|
|
306
|
+
<button class="btn btn-primary" data-action="retry-load" ${dataAttr('retry-page', page)} ${dataAttr('retry-prefix', prefix)}>
|
|
294
307
|
<i class="fas fa-redo"></i> Retry
|
|
295
308
|
</button>
|
|
296
309
|
</div>
|
|
@@ -319,8 +332,8 @@ function toggleSort(field) {
|
|
|
319
332
|
valA = new Date(a.LastModified || 0).getTime();
|
|
320
333
|
valB = new Date(b.LastModified || 0).getTime();
|
|
321
334
|
} else {
|
|
322
|
-
valA = (a.Key
|
|
323
|
-
valB = (b.Key
|
|
335
|
+
valA = getBaseName(a.Key).toLowerCase();
|
|
336
|
+
valB = getBaseName(b.Key).toLowerCase();
|
|
324
337
|
}
|
|
325
338
|
|
|
326
339
|
if (valA < valB) return currentOrder === 'asc' ? -1 : 1;
|
|
@@ -360,7 +373,6 @@ function setQuickFilter(filter = 'all') {
|
|
|
360
373
|
// Extract unique folders at current level
|
|
361
374
|
function extractFoldersAtCurrentLevel(files, prefix) {
|
|
362
375
|
const folderSet = new Set();
|
|
363
|
-
const prefixDepth = prefix ? prefix.split('/').filter(p => p).length : 0;
|
|
364
376
|
|
|
365
377
|
files.forEach(file => {
|
|
366
378
|
const key = file.Key;
|
|
@@ -447,6 +459,9 @@ function extractMatchingFolders(files, search) {
|
|
|
447
459
|
// Render files table
|
|
448
460
|
function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
449
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;
|
|
450
465
|
|
|
451
466
|
// Determine which files/folders to show based on view mode
|
|
452
467
|
let displayFiles = [];
|
|
@@ -466,7 +481,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
466
481
|
// Skip folder marker objects (keys ending with /)
|
|
467
482
|
if (f.Key.endsWith('/')) return false;
|
|
468
483
|
|
|
469
|
-
const fileName = f.Key
|
|
484
|
+
const fileName = getBaseName(f.Key);
|
|
470
485
|
return fileName.toLowerCase().includes(searchLower);
|
|
471
486
|
});
|
|
472
487
|
|
|
@@ -485,8 +500,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
485
500
|
// Skip folder marker objects (keys ending with /)
|
|
486
501
|
if (file.Key.endsWith('/')) return false;
|
|
487
502
|
|
|
488
|
-
const
|
|
489
|
-
const relativePath = basePrefix ? file.Key.substring(basePrefix.length + (basePrefix.endsWith('/') ? 0 : 1)) : file.Key;
|
|
503
|
+
const relativePath = basePrefix ? file.Key.substring(relativeBaseOffset) : file.Key;
|
|
490
504
|
// File is at current level if it has no '/' in relative path
|
|
491
505
|
return !relativePath.includes('/');
|
|
492
506
|
});
|
|
@@ -495,7 +509,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
495
509
|
|
|
496
510
|
// Apply quick file-type filter after mode/path filtering.
|
|
497
511
|
displayFiles = displayFiles.filter((f) => {
|
|
498
|
-
const name =
|
|
512
|
+
const name = getBaseName(f?.Key);
|
|
499
513
|
return matchesQuickFilter(name, currentQuickFilter);
|
|
500
514
|
});
|
|
501
515
|
|
|
@@ -505,13 +519,15 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
505
519
|
// (In flat mode, the prefix acts as a base filter, but we see recursive files under it)
|
|
506
520
|
if (prefix) {
|
|
507
521
|
const parts = prefix.split('/').filter(p => p);
|
|
508
|
-
|
|
522
|
+
const safeRootLabel = escapeHtml(rootLabel);
|
|
523
|
+
const safeRootTitle = escapeAttr(rootLabel);
|
|
524
|
+
const breadcrumbParts = [`
|
|
509
525
|
<div class="breadcrumb-nav">
|
|
510
526
|
<div class="breadcrumb-item">
|
|
511
527
|
<i class="fas fa-home"></i>
|
|
512
|
-
<
|
|
528
|
+
<button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', '')} title="Go to ${safeRootTitle}">${safeRootLabel}</button>
|
|
513
529
|
</div>
|
|
514
|
-
|
|
530
|
+
`];
|
|
515
531
|
|
|
516
532
|
let currentPath = '';
|
|
517
533
|
parts.forEach((part, index) => {
|
|
@@ -519,51 +535,57 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
519
535
|
const isLast = index === parts.length - 1;
|
|
520
536
|
|
|
521
537
|
if (isLast) {
|
|
522
|
-
|
|
538
|
+
const safePart = escapeHtml(part);
|
|
539
|
+
breadcrumbParts.push(`
|
|
523
540
|
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
524
541
|
<div class="breadcrumb-item">
|
|
525
|
-
<span class="breadcrumb-current">${
|
|
542
|
+
<span class="breadcrumb-current">${safePart}</span>
|
|
526
543
|
</div>
|
|
527
|
-
|
|
544
|
+
`);
|
|
528
545
|
} else {
|
|
529
546
|
const pathCopy = currentPath;
|
|
530
|
-
|
|
547
|
+
const safePart = escapeHtml(part);
|
|
548
|
+
const safePathTitle = escapeAttr(pathCopy);
|
|
549
|
+
breadcrumbParts.push(`
|
|
531
550
|
<span class="breadcrumb-separator"><i class="fas fa-chevron-right"></i></span>
|
|
532
551
|
<div class="breadcrumb-item">
|
|
533
|
-
<
|
|
552
|
+
<button type="button" class="breadcrumb-link" ${dataAttr('nav-prefix', pathCopy)} title="Go to ${safePathTitle}">${safePart}</button>
|
|
534
553
|
</div>
|
|
535
|
-
|
|
554
|
+
`);
|
|
536
555
|
}
|
|
537
556
|
});
|
|
538
557
|
|
|
539
|
-
|
|
540
|
-
breadcrumb =
|
|
558
|
+
breadcrumbParts.push(`</div>`);
|
|
559
|
+
breadcrumb = breadcrumbParts.join('');
|
|
541
560
|
}
|
|
542
561
|
|
|
543
562
|
// Generate folder rows (only used in folder mode)
|
|
544
563
|
const folderRows = displayFolders.map(folderPath => {
|
|
545
564
|
// Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
|
|
546
565
|
const rawFolderPath = String(folderPath || '');
|
|
547
|
-
const cleanPath = rawFolderPath.replace(/\/+$/, '');
|
|
548
|
-
const
|
|
549
|
-
|
|
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);
|
|
550
572
|
const safeFolderName = escapeHtml(folderName);
|
|
551
|
-
const safeFolderPathAttr = escapeAttr(
|
|
573
|
+
const safeFolderPathAttr = escapeAttr(`${folderPathForAction}/`);
|
|
552
574
|
const safeFolderDeleteKeyAttr = escapeAttr(folderKeyForDelete);
|
|
553
575
|
|
|
554
576
|
return `
|
|
555
577
|
<tr class="folder-row" data-folder-path="${safeFolderPathAttr}" title="Open folder: ${safeFolderName}">
|
|
556
|
-
<td
|
|
578
|
+
<td class="table-cell-center">
|
|
557
579
|
<!-- no selection for folders -->
|
|
558
580
|
</td>
|
|
559
|
-
<td
|
|
560
|
-
<div
|
|
581
|
+
<td class="file-name-cell">
|
|
582
|
+
<div class="file-row-main">
|
|
561
583
|
<div class="file-icon">
|
|
562
584
|
<i class="fas fa-folder"></i>
|
|
563
585
|
</div>
|
|
564
586
|
<div class="file-info">
|
|
565
|
-
<div class="file-name
|
|
566
|
-
<i class="fas fa-folder
|
|
587
|
+
<div class="file-name folder-name">
|
|
588
|
+
<i class="fas fa-folder folder-leading-icon"></i>${safeFolderName}/
|
|
567
589
|
</div>
|
|
568
590
|
</div>
|
|
569
591
|
<button type="button" class="btn btn-danger btn-sm delete-btn dtm" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
@@ -572,12 +594,12 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
572
594
|
</div>
|
|
573
595
|
</td>
|
|
574
596
|
<td>
|
|
575
|
-
|
|
597
|
+
${mutedDash()}
|
|
576
598
|
</td>
|
|
577
|
-
<td
|
|
578
|
-
|
|
599
|
+
<td class="table-cell-center">
|
|
600
|
+
${mutedDash()}
|
|
579
601
|
</td>
|
|
580
|
-
<td
|
|
602
|
+
<td class="table-cell-center">
|
|
581
603
|
<div class="btn-group" role="group">
|
|
582
604
|
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${safeFolderDeleteKeyAttr}" data-is-folder="true" title="Delete folder">
|
|
583
605
|
<i class="fas fa-trash"></i>
|
|
@@ -620,21 +642,23 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
620
642
|
const safeFilename = escapeHtml(filename);
|
|
621
643
|
const safeTruncatedName = escapeHtml(truncatedName);
|
|
622
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';
|
|
623
647
|
|
|
624
648
|
// If in flat mode, we might want to show the folder path more prominently
|
|
625
649
|
const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
|
|
626
650
|
? `<div class="file-path-display">
|
|
627
|
-
<i class="fas fa-folder
|
|
651
|
+
<i class="fas fa-folder path-display-icon"></i> ${safeFolderPath}
|
|
628
652
|
</div>`
|
|
629
653
|
: '';
|
|
630
654
|
|
|
631
655
|
return `
|
|
632
656
|
<tr class="file-row">
|
|
633
|
-
<td
|
|
657
|
+
<td class="table-cell-center">
|
|
634
658
|
<input type="checkbox" class="selectFileCheckbox" data-key="${safeKeyAttr}" aria-label="Select ${safeKeyAttr}" />
|
|
635
659
|
</td>
|
|
636
|
-
<td
|
|
637
|
-
<div
|
|
660
|
+
<td class="file-name-cell">
|
|
661
|
+
<div class="file-row-main">
|
|
638
662
|
<div class="file-icon ${typeCategory}">
|
|
639
663
|
<i class="${iconClass}"></i>
|
|
640
664
|
</div>
|
|
@@ -645,18 +669,18 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
645
669
|
</div>
|
|
646
670
|
</td>
|
|
647
671
|
<td>
|
|
648
|
-
${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>'}
|
|
649
673
|
</td>
|
|
650
674
|
<td>
|
|
651
|
-
${file.LastModified ? `<span
|
|
675
|
+
${file.LastModified ? `<span class="muted-cell-value">${formatDate(file.LastModified)}</span>` : '<span class="muted-cell-value">N/A</span>'}
|
|
652
676
|
</td>
|
|
653
|
-
<td
|
|
677
|
+
<td class="table-cell-center">
|
|
654
678
|
<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'}" ${
|
|
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}>
|
|
657
681
|
<i class="fas fa-eye"></i>
|
|
658
682
|
</button>
|
|
659
|
-
<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">
|
|
660
684
|
<i class="fas fa-link"></i>
|
|
661
685
|
</button>
|
|
662
686
|
<a href="${getDownloadUrl(encodedKey)}" class="btn btn-success btn-sm"
|
|
@@ -679,7 +703,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
679
703
|
${breadcrumb}
|
|
680
704
|
<div class="empty-state">
|
|
681
705
|
<div class="empty-state-illustration">
|
|
682
|
-
<i class="fas fa-folder-open
|
|
706
|
+
<i class="fas fa-folder-open empty-state-folder-icon"></i>
|
|
683
707
|
<i class="fas fa-search empty-state-search-icon"></i>
|
|
684
708
|
</div>
|
|
685
709
|
<h4>No files found</h4>
|
|
@@ -687,13 +711,13 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
687
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.`}
|
|
688
712
|
</p>
|
|
689
713
|
<div class="empty-state-actions">
|
|
690
|
-
<button class="btn btn-primary"
|
|
714
|
+
<button class="btn btn-primary" data-action="upload-files">
|
|
691
715
|
<i class="fas fa-cloud-upload-alt"></i> Upload Files
|
|
692
716
|
</button>
|
|
693
|
-
<button class="btn btn-outline-secondary"
|
|
717
|
+
<button class="btn btn-outline-secondary" data-action="create-folder">
|
|
694
718
|
<i class="fas fa-folder-plus"></i> New Folder
|
|
695
719
|
</button>
|
|
696
|
-
${prefix ? `<button class="btn btn-light"
|
|
720
|
+
${prefix ? `<button class="btn btn-light" data-action="go-root">
|
|
697
721
|
<i class="fas fa-home"></i> Go to ${rootLabel}
|
|
698
722
|
</button>` : ''}
|
|
699
723
|
</div>
|
|
@@ -714,7 +738,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
714
738
|
? `<div class="virtualized-info">
|
|
715
739
|
<span>Showing ${visibleFiles.length} of ${displayFiles.length} files in this view.</span>
|
|
716
740
|
<div class="virtualized-actions">
|
|
717
|
-
${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary"
|
|
741
|
+
${visibleFiles.length < displayFiles.length ? '<button class="btn btn-sm btn-outline-secondary" data-action="expand-virtual">Load more</button>' : ''}
|
|
718
742
|
</div>
|
|
719
743
|
</div>`
|
|
720
744
|
: '';
|
|
@@ -724,7 +748,7 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
724
748
|
<div class="table-responsive">
|
|
725
749
|
<div id="bulkActionsBar" class="bulk-actions-bar">
|
|
726
750
|
<span id="bulkSelectedCount">0 selected</span>
|
|
727
|
-
<div
|
|
751
|
+
<div class="bulk-actions-controls">
|
|
728
752
|
<button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
|
|
729
753
|
<i class="fas fa-trash"></i> Delete Selected
|
|
730
754
|
</button>
|
|
@@ -733,19 +757,19 @@ function renderFilesTable(files, folders = [], prefix = '', apiPrefix = '') {
|
|
|
733
757
|
<table class="table table-hover mb-0">
|
|
734
758
|
<thead>
|
|
735
759
|
<tr>
|
|
736
|
-
<th
|
|
760
|
+
<th class="table-select-col">
|
|
737
761
|
<input type="checkbox" id="selectAllCheckbox" aria-label="Select all files" />
|
|
738
762
|
</th>
|
|
739
|
-
<th
|
|
763
|
+
<th class="sortable-heading file-name-heading" data-sort-field="name">
|
|
740
764
|
<i class="fas fa-file-alt"></i> Name ${getSortIcon('name')}
|
|
741
765
|
</th>
|
|
742
|
-
<th
|
|
766
|
+
<th class="sortable-heading" data-sort-field="size">
|
|
743
767
|
<i class="fas fa-weight"></i> Size ${getSortIcon('size')}
|
|
744
768
|
</th>
|
|
745
|
-
<th
|
|
769
|
+
<th class="sortable-heading" data-sort-field="date">
|
|
746
770
|
<i class="fas fa-calendar"></i> Last Modified ${getSortIcon('date')}
|
|
747
771
|
</th>
|
|
748
|
-
<th
|
|
772
|
+
<th class="table-cell-center">
|
|
749
773
|
<i class="fas fa-cogs"></i> Actions
|
|
750
774
|
</th>
|
|
751
775
|
</tr>
|
|
@@ -767,6 +791,17 @@ function renderPagination(data) {
|
|
|
767
791
|
const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
|
|
768
792
|
|
|
769
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
|
+
};
|
|
770
805
|
|
|
771
806
|
if (mode === 'optimized') {
|
|
772
807
|
// Optimized Mode: Token-based pagination (Next only, limited Prev)
|
|
@@ -789,13 +824,9 @@ function renderPagination(data) {
|
|
|
789
824
|
|
|
790
825
|
// Next button
|
|
791
826
|
if (hasNextPage) {
|
|
792
|
-
// Quote the token correctly
|
|
793
|
-
const tokenStr = currentNextToken ? currentNextToken.replace(/'/g, "\\'") : "";
|
|
794
827
|
paginationHTML += `
|
|
795
828
|
<li class="page-item">
|
|
796
|
-
|
|
797
|
-
Next <i class="fas fa-chevron-right"></i>
|
|
798
|
-
</a>
|
|
829
|
+
${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>', { token: currentNextToken || '' })}
|
|
799
830
|
</li>
|
|
800
831
|
`;
|
|
801
832
|
} else {
|
|
@@ -815,9 +846,7 @@ function renderPagination(data) {
|
|
|
815
846
|
if (hasPrevPage) {
|
|
816
847
|
paginationHTML += `
|
|
817
848
|
<li class="page-item">
|
|
818
|
-
|
|
819
|
-
<i class="fas fa-chevron-left"></i> Previous
|
|
820
|
-
</a>
|
|
849
|
+
${pageLink(currentPage - 1, '<i class="fas fa-chevron-left"></i> Previous')}
|
|
821
850
|
</li>
|
|
822
851
|
`;
|
|
823
852
|
} else {
|
|
@@ -843,7 +872,7 @@ function renderPagination(data) {
|
|
|
843
872
|
if (startPage > 1) {
|
|
844
873
|
paginationHTML += `
|
|
845
874
|
<li class="page-item">
|
|
846
|
-
|
|
875
|
+
${pageLink(1, '1')}
|
|
847
876
|
</li>`;
|
|
848
877
|
if (startPage > 2) {
|
|
849
878
|
paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
@@ -860,7 +889,7 @@ function renderPagination(data) {
|
|
|
860
889
|
} else {
|
|
861
890
|
paginationHTML += `
|
|
862
891
|
<li class="page-item">
|
|
863
|
-
|
|
892
|
+
${pageLink(i, String(i))}
|
|
864
893
|
</li>
|
|
865
894
|
`;
|
|
866
895
|
}
|
|
@@ -873,7 +902,7 @@ function renderPagination(data) {
|
|
|
873
902
|
}
|
|
874
903
|
paginationHTML += `
|
|
875
904
|
<li class="page-item">
|
|
876
|
-
|
|
905
|
+
${pageLink(totalPages, String(totalPages))}
|
|
877
906
|
</li>`;
|
|
878
907
|
}
|
|
879
908
|
|
|
@@ -881,9 +910,7 @@ function renderPagination(data) {
|
|
|
881
910
|
if (hasNextPage) {
|
|
882
911
|
paginationHTML += `
|
|
883
912
|
<li class="page-item">
|
|
884
|
-
|
|
885
|
-
Next <i class="fas fa-chevron-right"></i>
|
|
886
|
-
</a>
|
|
913
|
+
${pageLink(currentPage + 1, 'Next <i class="fas fa-chevron-right"></i>')}
|
|
887
914
|
</li>
|
|
888
915
|
`;
|
|
889
916
|
} else {
|
|
@@ -976,7 +1003,7 @@ setTimeout(() => {
|
|
|
976
1003
|
// Upload queue, drag-and-drop and multi-file support
|
|
977
1004
|
const uploadQueue = []; // { id, file, status, progress, controller/xhr, uploadId }
|
|
978
1005
|
const xhrMap = new Map();
|
|
979
|
-
const CHUNK_SIZE =
|
|
1006
|
+
const CHUNK_SIZE = 50 * 1024 * 1024; // 50MB chunk threshold
|
|
980
1007
|
|
|
981
1008
|
function createId() { return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 9); }
|
|
982
1009
|
|
|
@@ -1030,9 +1057,9 @@ function renderUploadQueue() {
|
|
|
1030
1057
|
<div class="form-text file-size upload-item-size-label">${formatFileSize(item.file.size)}</div>
|
|
1031
1058
|
</div>
|
|
1032
1059
|
</div>
|
|
1033
|
-
<div
|
|
1034
|
-
<div class="progress-container
|
|
1035
|
-
<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>
|
|
1036
1063
|
</div>
|
|
1037
1064
|
<div class="upload-item-status-row">
|
|
1038
1065
|
<small class="text-muted status">${statusText}</small>
|
|
@@ -1326,7 +1353,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1326
1353
|
|
|
1327
1354
|
this.disabled = true;
|
|
1328
1355
|
this.setAttribute('aria-busy', 'true');
|
|
1329
|
-
|
|
1356
|
+
bucketSwitchStatusEl?.classList.remove('is-hidden');
|
|
1330
1357
|
|
|
1331
1358
|
const qs = nextParams.toString();
|
|
1332
1359
|
window.location.href = `/mbkbucket${qs ? `?${qs}` : ''}`;
|
|
@@ -1384,6 +1411,70 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
1384
1411
|
|
|
1385
1412
|
// Delegate view and delete button clicks to avoid inline JS with unescaped data
|
|
1386
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
|
+
|
|
1387
1478
|
const folderRow = e.target.closest('.folder-row');
|
|
1388
1479
|
if (folderRow && !e.target.closest('.delete-btn')) {
|
|
1389
1480
|
const folderPath = folderRow.dataset.folderPath;
|
|
@@ -2005,4 +2096,4 @@ async function createFile() {
|
|
|
2005
2096
|
};
|
|
2006
2097
|
};
|
|
2007
2098
|
|
|
2008
|
-
})(window);
|
|
2099
|
+
})(window);
|