mbkbucket 1.2.0 → 1.2.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/index.d.ts +1 -1
- package/index.js +15 -2
- package/lib/bucket.js +14 -6
- package/lib/s3.js +2 -1
- package/package.json +2 -2
- package/public/{css/bucketadmin.css → bucketadmin.css} +108 -33
- package/public/{js/bucketadmincontent.js → bucketadmin.js} +182 -67
- package/views/bucket.handlebars +1 -1
- package/views/bucketadmincontent.handlebars +32 -48
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -40,8 +40,21 @@ const generalLimiter = rateLimit({
|
|
|
40
40
|
server.use(express.json());
|
|
41
41
|
server.use(express.urlencoded({ extended: true }));
|
|
42
42
|
|
|
43
|
-
// Serve static assets
|
|
44
|
-
|
|
43
|
+
// Serve only specific static assets (limit exposure): CSS and JS used by the dashboard
|
|
44
|
+
// This replaces serving the entire /public directory.
|
|
45
|
+
const staticMaxAgeSeconds = 90 * 24 * 60 * 60; // 90 days
|
|
46
|
+
server.get('/mbkbucket/bucketadmin.css', (req, res) => {
|
|
47
|
+
res.set('Cache-Control', `public, max-age=${staticMaxAgeSeconds}, immutable`);
|
|
48
|
+
res.sendFile(path.join(__dirname, 'public', 'bucketadmin.css'), (err) => {
|
|
49
|
+
if (err) res.status(err.status || 404).end();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
server.get('/mbkbucket/bucketadmin.js', (req, res) => {
|
|
53
|
+
res.set('Cache-Control', `public, max-age=${staticMaxAgeSeconds}, immutable`);
|
|
54
|
+
res.sendFile(path.join(__dirname, 'public', 'bucketadmin.js'), (err) => {
|
|
55
|
+
if (err) res.status(err.status || 404).end();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
45
58
|
|
|
46
59
|
// Configure Handlebars (single setup)
|
|
47
60
|
server.engine("handlebars", engine({
|
package/lib/bucket.js
CHANGED
|
@@ -18,7 +18,7 @@ fs.mkdirSync(CHUNKS_DIR, { recursive: true });
|
|
|
18
18
|
// Key/prefix helpers are provided by `s3.js`: `ensurePrefix`, `ensureKeyHasAppPrefix`, `getAppName`.
|
|
19
19
|
|
|
20
20
|
router.get('/mbkbucket', validateSessionAndRole('SuperAdmin'), async (req, res) => {
|
|
21
|
-
renderPage(req, res, "bucket.handlebars", false, { page: "Admin Bucket", bucketvar: mbkautheVar?.bucket, message: req.query.message, error: req.query.error });
|
|
21
|
+
renderPage(req, res, "bucket.handlebars", false, { page: "Admin Bucket", bucketvar: mbkautheVar?.bucket, APP_NAME: mbkautheVar?.APP_NAME, message: req.query.message, error: req.query.error });
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
router.get(['/mbkbucket/info', "/mbkbucket/i"], validateSessionAndRole('SuperAdmin'), async (req, res) => {
|
|
@@ -28,7 +28,7 @@ router.get(['/mbkbucket/info', "/mbkbucket/i"], validateSessionAndRole('SuperAdm
|
|
|
28
28
|
} catch (err) {
|
|
29
29
|
console.error("[mbkauthe] Error fetching package-lock.json:", err);
|
|
30
30
|
}
|
|
31
|
-
renderPage(req, res, "info.handlebars", false, { page: "MBK Bucket Info", appVersion
|
|
31
|
+
renderPage(req, res, "info.handlebars", false, { page: "MBK Bucket Info", appVersion, latestVersion, APP_NAME: mbkautheVar?.APP_NAME });
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
// API endpoint for fetching files
|
|
@@ -45,11 +45,20 @@ router.get('/mbkbucket/api/files', validateSessionAndRole('SuperAdmin'), async (
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
try {
|
|
48
|
-
|
|
49
|
-
let
|
|
48
|
+
let files = [];
|
|
49
|
+
let continuationToken = undefined;
|
|
50
50
|
let usedPrefix = effectivePrefix;
|
|
51
51
|
let legacyFallback = false;
|
|
52
52
|
|
|
53
|
+
// Fetch all files recursively (handling S3 pagination)
|
|
54
|
+
do {
|
|
55
|
+
const result = await listfiles(effectivePrefix, { continuationToken });
|
|
56
|
+
if (result.Contents) {
|
|
57
|
+
files = files.concat(result.Contents);
|
|
58
|
+
}
|
|
59
|
+
continuationToken = result.nextToken;
|
|
60
|
+
} while (continuationToken);
|
|
61
|
+
|
|
53
62
|
// If no files found under the app prefixed path, try root as a legacy fallback
|
|
54
63
|
if (!files.length) {
|
|
55
64
|
try {
|
|
@@ -69,8 +78,7 @@ router.get('/mbkbucket/api/files', validateSessionAndRole('SuperAdmin'), async (
|
|
|
69
78
|
if (search.trim()) {
|
|
70
79
|
const searchLower = search.toLowerCase();
|
|
71
80
|
files = files.filter(file => {
|
|
72
|
-
|
|
73
|
-
return fileName.toLowerCase().includes(searchLower);
|
|
81
|
+
return file.Key.toLowerCase().includes(searchLower);
|
|
74
82
|
});
|
|
75
83
|
}
|
|
76
84
|
|
package/lib/s3.js
CHANGED
|
@@ -216,13 +216,14 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
|
216
216
|
|
|
217
217
|
// Download file with enhanced features
|
|
218
218
|
export async function downloadFile(key, options = {}) {
|
|
219
|
+
let keyToDownload;
|
|
219
220
|
try {
|
|
220
221
|
if (!key) {
|
|
221
222
|
throw new Error('Key is required');
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
// Ensure key is app-prefixed
|
|
225
|
-
|
|
226
|
+
keyToDownload = ensureKeyHasAppPrefix(key);
|
|
226
227
|
|
|
227
228
|
const {
|
|
228
229
|
range = null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkbucket",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "S3 Bucket manager for mbktech.org apps",
|
|
5
5
|
"author": "Muhammad Bin Khalid",
|
|
6
6
|
"homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"express": "^4.22.1",
|
|
20
20
|
"express-handlebars": "^8.0.3",
|
|
21
21
|
"express-rate-limit": "^7.0.1",
|
|
22
|
-
"mbkauthe": "
|
|
22
|
+
"mbkauthe": "latest",
|
|
23
23
|
"multer": "^2.0.2",
|
|
24
24
|
"pg": "^8.16.3"
|
|
25
25
|
},
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
background: linear-gradient(135deg, var(--primary-color-ml) 0%, var(--primary-dark-color-ml) 100%);
|
|
49
49
|
color: white;
|
|
50
50
|
border: none;
|
|
51
|
-
margin-bottom:
|
|
51
|
+
margin-bottom: 1rem;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
.stats-content {
|
|
@@ -121,6 +121,57 @@
|
|
|
121
121
|
gap: 0.5rem;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/* Upload Drop Zone */
|
|
125
|
+
.upload-drop-zone {
|
|
126
|
+
border: 2px dashed var(--gray-color-ml);
|
|
127
|
+
border-radius: var(--border-radius-lg-ml);
|
|
128
|
+
background-color: #f8fafc;
|
|
129
|
+
min-height: 160px;
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
text-align: center;
|
|
135
|
+
transition: all 0.2s ease;
|
|
136
|
+
position: relative;
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.upload-drop-zone:hover,
|
|
141
|
+
.upload-drop-zone.dragover {
|
|
142
|
+
border-color: var(--primary-color-ml);
|
|
143
|
+
background-color: #f0fdfa;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.drop-zone-content {
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
padding: 2rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.drop-zone-content i {
|
|
152
|
+
font-size: 3rem;
|
|
153
|
+
color: var(--primary-color-ml);
|
|
154
|
+
margin-bottom: 1rem;
|
|
155
|
+
opacity: 0.8;
|
|
156
|
+
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.upload-drop-zone:hover .drop-zone-content i {
|
|
160
|
+
opacity: 1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.drop-text {
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
font-size: 1.1rem;
|
|
166
|
+
color: var(--dark-color-ml);
|
|
167
|
+
margin-bottom: 0.5rem;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.drop-hint {
|
|
171
|
+
font-size: 0.85rem;
|
|
172
|
+
color: var(--gray-color-ml);
|
|
173
|
+
}
|
|
174
|
+
|
|
124
175
|
/* Button styling - Modern hover effects */
|
|
125
176
|
.btn {
|
|
126
177
|
background: var(--primary-color-ml);
|
|
@@ -347,64 +398,74 @@
|
|
|
347
398
|
/* Table styling - Modern clean design */
|
|
348
399
|
.table-responsive {
|
|
349
400
|
overflow-x: auto;
|
|
350
|
-
|
|
401
|
+
border-radius: var(--border-radius-lg-ml);
|
|
402
|
+
box-shadow: none;
|
|
351
403
|
}
|
|
352
404
|
|
|
353
405
|
.table {
|
|
354
406
|
width: 100%;
|
|
355
407
|
margin-bottom: 0;
|
|
356
408
|
background: white;
|
|
357
|
-
border-collapse:
|
|
409
|
+
border-collapse: separate;
|
|
410
|
+
border-spacing: 0;
|
|
358
411
|
}
|
|
359
412
|
|
|
360
413
|
.table thead {
|
|
361
|
-
background:
|
|
362
|
-
color:
|
|
414
|
+
background: var(--light-color-ml);
|
|
415
|
+
color: var(--dark-color-ml);
|
|
416
|
+
position: sticky;
|
|
417
|
+
top: 0;
|
|
418
|
+
z-index: 10;
|
|
363
419
|
}
|
|
364
420
|
|
|
365
421
|
.table th,
|
|
366
422
|
.table td {
|
|
367
|
-
padding: 0.5rem
|
|
423
|
+
padding: 0.5rem 1.25rem;
|
|
368
424
|
vertical-align: middle;
|
|
369
425
|
border-bottom: 1px solid var(--gray-light-color-ml);
|
|
370
|
-
font-size: 0.
|
|
426
|
+
font-size: 0.95rem;
|
|
371
427
|
}
|
|
372
428
|
|
|
373
429
|
.table th {
|
|
374
|
-
font-weight:
|
|
375
|
-
text-transform:
|
|
376
|
-
font-size: 0.
|
|
377
|
-
letter-spacing: 0.
|
|
430
|
+
font-weight: 700;
|
|
431
|
+
text-transform: none;
|
|
432
|
+
font-size: 0.85rem;
|
|
433
|
+
letter-spacing: 0.02em;
|
|
434
|
+
color: var(--text-muted-color-ml);
|
|
435
|
+
border-bottom: 2px solid var(--gray-light-color-ml);
|
|
378
436
|
}
|
|
379
437
|
|
|
380
438
|
.table tbody tr {
|
|
381
|
-
transition:
|
|
439
|
+
transition: background-color 0.2s ease, transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
382
440
|
position: relative;
|
|
441
|
+
background-color: white;
|
|
383
442
|
}
|
|
384
443
|
|
|
385
444
|
.table tbody tr:hover {
|
|
386
|
-
background:
|
|
445
|
+
background-color: var(--light-color-ml);
|
|
387
446
|
}
|
|
388
447
|
|
|
389
448
|
.file-row {
|
|
390
449
|
border-left: 3px solid transparent;
|
|
391
|
-
transition: all 0.2s ease;
|
|
392
450
|
}
|
|
393
451
|
|
|
394
452
|
.file-row:hover {
|
|
395
453
|
border-left-color: var(--primary-color-ml);
|
|
396
|
-
background:
|
|
454
|
+
background-color: #f0fdfa;
|
|
455
|
+
/* Very light teal */
|
|
456
|
+
z-index: 1;
|
|
397
457
|
}
|
|
398
458
|
|
|
399
459
|
.folder-row {
|
|
400
460
|
border-left: 3px solid transparent;
|
|
401
|
-
transition: all 0.2s ease;
|
|
402
461
|
cursor: pointer;
|
|
403
462
|
}
|
|
404
463
|
|
|
405
464
|
.folder-row:hover {
|
|
406
465
|
border-left-color: #f59e0b;
|
|
407
|
-
background:
|
|
466
|
+
background-color: #fffbeb;
|
|
467
|
+
/* Very light amber */
|
|
468
|
+
z-index: 1;
|
|
408
469
|
}
|
|
409
470
|
|
|
410
471
|
.folder-row .file-icon {
|
|
@@ -414,12 +475,11 @@
|
|
|
414
475
|
}
|
|
415
476
|
|
|
416
477
|
.folder-row:hover .file-icon {
|
|
417
|
-
transform: scale(1.1);
|
|
418
478
|
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
|
419
479
|
}
|
|
420
480
|
|
|
421
481
|
.breadcrumb-nav {
|
|
422
|
-
padding:
|
|
482
|
+
padding: 0.5rem 1.5rem;
|
|
423
483
|
background: linear-gradient(135deg, var(--light-color-ml) 0%, #f9fafb 100%);
|
|
424
484
|
border-bottom: 1px solid var(--gray-light-color-ml);
|
|
425
485
|
display: flex;
|
|
@@ -459,8 +519,8 @@
|
|
|
459
519
|
}
|
|
460
520
|
|
|
461
521
|
.file-icon {
|
|
462
|
-
width:
|
|
463
|
-
height:
|
|
522
|
+
width: 40px;
|
|
523
|
+
height: 40px;
|
|
464
524
|
background: linear-gradient(135deg, var(--primary-light-color-ml) 0%, rgba(13, 148, 136, 0.08) 100%);
|
|
465
525
|
border-radius: 10px;
|
|
466
526
|
display: flex;
|
|
@@ -474,7 +534,6 @@
|
|
|
474
534
|
}
|
|
475
535
|
|
|
476
536
|
.file-row:hover .file-icon {
|
|
477
|
-
transform: scale(1.1);
|
|
478
537
|
box-shadow: 0 2px 8px rgba(13, 148, 136, 0.2);
|
|
479
538
|
}
|
|
480
539
|
|
|
@@ -609,7 +668,7 @@
|
|
|
609
668
|
}
|
|
610
669
|
|
|
611
670
|
.empty-state i {
|
|
612
|
-
font-size: 5rem;
|
|
671
|
+
font-size: 1.5rem;
|
|
613
672
|
margin-bottom: 1.5rem;
|
|
614
673
|
opacity: 0.3;
|
|
615
674
|
background: linear-gradient(135deg, var(--primary-color-ml), var(--primary-dark-color-ml));
|
|
@@ -618,6 +677,12 @@
|
|
|
618
677
|
background-clip: text;
|
|
619
678
|
}
|
|
620
679
|
|
|
680
|
+
.fa-add:before,
|
|
681
|
+
.fa-plus:before {
|
|
682
|
+
color: #fff !important;
|
|
683
|
+
align-items: center;
|
|
684
|
+
}
|
|
685
|
+
|
|
621
686
|
.empty-state h4 {
|
|
622
687
|
color: var(--dark-color-ml);
|
|
623
688
|
margin-bottom: 0.75rem;
|
|
@@ -1040,8 +1105,8 @@
|
|
|
1040
1105
|
}
|
|
1041
1106
|
|
|
1042
1107
|
.skeleton-row .file-icon {
|
|
1043
|
-
width:
|
|
1044
|
-
height:
|
|
1108
|
+
width: 40px;
|
|
1109
|
+
height: 40px;
|
|
1045
1110
|
border-radius: 8px;
|
|
1046
1111
|
}
|
|
1047
1112
|
|
|
@@ -1195,8 +1260,8 @@
|
|
|
1195
1260
|
}
|
|
1196
1261
|
|
|
1197
1262
|
.file-row td:nth-child(2) .file-icon {
|
|
1198
|
-
width:
|
|
1199
|
-
height:
|
|
1263
|
+
width: 40px;
|
|
1264
|
+
height: 40px;
|
|
1200
1265
|
font-size: 1.25rem;
|
|
1201
1266
|
border-radius: 10px;
|
|
1202
1267
|
}
|
|
@@ -1298,8 +1363,8 @@
|
|
|
1298
1363
|
|
|
1299
1364
|
/* Make folder icon and name more prominent on mobile */
|
|
1300
1365
|
.folder-row .file-icon {
|
|
1301
|
-
width:
|
|
1302
|
-
height:
|
|
1366
|
+
width: 40px;
|
|
1367
|
+
height: 40px;
|
|
1303
1368
|
font-size: 1.2rem;
|
|
1304
1369
|
margin-right: 0.75rem;
|
|
1305
1370
|
}
|
|
@@ -1324,8 +1389,8 @@
|
|
|
1324
1389
|
}
|
|
1325
1390
|
|
|
1326
1391
|
.file-icon {
|
|
1327
|
-
width:
|
|
1328
|
-
height:
|
|
1392
|
+
width: 40px;
|
|
1393
|
+
height: 40px;
|
|
1329
1394
|
font-size: 1.2rem;
|
|
1330
1395
|
margin-right: 0.85rem;
|
|
1331
1396
|
}
|
|
@@ -1355,7 +1420,7 @@
|
|
|
1355
1420
|
}
|
|
1356
1421
|
|
|
1357
1422
|
.breadcrumb-nav {
|
|
1358
|
-
padding: 0.
|
|
1423
|
+
padding: 0.5rem 1rem;
|
|
1359
1424
|
font-size: 0.85rem;
|
|
1360
1425
|
}
|
|
1361
1426
|
|
|
@@ -1379,7 +1444,7 @@
|
|
|
1379
1444
|
}
|
|
1380
1445
|
|
|
1381
1446
|
.empty-state i {
|
|
1382
|
-
font-size:
|
|
1447
|
+
font-size: 1.5rem;
|
|
1383
1448
|
}
|
|
1384
1449
|
|
|
1385
1450
|
.modal-dialog {
|
|
@@ -1410,4 +1475,14 @@
|
|
|
1410
1475
|
.file-stats .stats-text p {
|
|
1411
1476
|
font-size: 0.85rem;
|
|
1412
1477
|
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
.dtm {
|
|
1481
|
+
display: none;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
@media (max-width: 768px) {
|
|
1485
|
+
.dtm {
|
|
1486
|
+
display: block;
|
|
1487
|
+
}
|
|
1413
1488
|
}
|
|
@@ -10,29 +10,63 @@ let pageFolders = [];
|
|
|
10
10
|
const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
|
|
11
11
|
// Bucket is configured centrally on the server; client should not select a bucket.
|
|
12
12
|
|
|
13
|
-
// Show alert message
|
|
13
|
+
// Show alert message (Toast)
|
|
14
14
|
function showAlert(message, type = 'success') {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
messageEl
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
alertEl.style.transform = 'translateY(-10px)';
|
|
29
|
-
setTimeout(() => {
|
|
30
|
-
alertEl.style.display = 'none';
|
|
31
|
-
alertEl.style.opacity = '1';
|
|
32
|
-
alertEl.style.transform = 'translateY(0)';
|
|
33
|
-
}, 300);
|
|
34
|
-
}, 5000);
|
|
15
|
+
const container = document.getElementById('toast-container');
|
|
16
|
+
if (!container) {
|
|
17
|
+
// Fallback to old method if container missing
|
|
18
|
+
const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
|
|
19
|
+
const messageId = type === 'success' ? 'successMessage' : 'errorMessage';
|
|
20
|
+
const alertEl = document.getElementById(alertId);
|
|
21
|
+
const messageEl = document.getElementById(messageId);
|
|
22
|
+
if (alertEl && messageEl) {
|
|
23
|
+
messageEl.textContent = message;
|
|
24
|
+
alertEl.style.display = 'flex';
|
|
25
|
+
setTimeout(() => alertEl.style.display = 'none', 5000);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
35
28
|
}
|
|
29
|
+
|
|
30
|
+
// Create toast element
|
|
31
|
+
const toast = document.createElement('div');
|
|
32
|
+
const isSuccess = type === 'success';
|
|
33
|
+
toast.className = `toast-notification ${type}`;
|
|
34
|
+
toast.style.cssText = `
|
|
35
|
+
background: ${isSuccess ? 'linear-gradient(135deg, #10b981, #059669)' : 'linear-gradient(135deg, #ef4444, #dc2626)'};
|
|
36
|
+
color: white;
|
|
37
|
+
padding: 1rem 1.5rem;
|
|
38
|
+
border-radius: 8px;
|
|
39
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 12px;
|
|
43
|
+
min-width: 300px;
|
|
44
|
+
transform: translateX(100%);
|
|
45
|
+
opacity: 0;
|
|
46
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
47
|
+
font-weight: 500;
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
toast.innerHTML = `
|
|
51
|
+
<i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'}" style="font-size: 1.25rem;"></i>
|
|
52
|
+
<div style="flex: 1;">${message}</div>
|
|
53
|
+
<button onclick="this.parentElement.remove()" style="background:none; border:none; color:white; cursor:pointer; opacity:0.8; font-size:1.2rem;">×</button>
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
container.appendChild(toast);
|
|
57
|
+
|
|
58
|
+
// Animate in
|
|
59
|
+
requestAnimationFrame(() => {
|
|
60
|
+
toast.style.transform = 'translateX(0)';
|
|
61
|
+
toast.style.opacity = '1';
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Auto dismiss
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
toast.style.transform = 'translateX(100%)';
|
|
67
|
+
toast.style.opacity = '0';
|
|
68
|
+
setTimeout(() => toast.remove(), 300);
|
|
69
|
+
}, 5000);
|
|
36
70
|
}
|
|
37
71
|
|
|
38
72
|
// Fetch and render files
|
|
@@ -106,12 +140,12 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
106
140
|
totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
|
|
107
141
|
|
|
108
142
|
if (search) {
|
|
109
|
-
|
|
110
|
-
|
|
143
|
+
filterInfo.textContent = `Search results for: "${search}"`;
|
|
144
|
+
fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
|
|
111
145
|
} else {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
146
|
+
const label = prefix || bucketAppName || 'Root';
|
|
147
|
+
filterInfo.textContent = `Filtered by folder: ${label}`;
|
|
148
|
+
fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
|
|
115
149
|
}
|
|
116
150
|
|
|
117
151
|
// Render files or empty state
|
|
@@ -256,19 +290,62 @@ function changeViewMode(mode) {
|
|
|
256
290
|
renderFilesTable(pageFiles, pageFolders, currentPrefix);
|
|
257
291
|
}
|
|
258
292
|
|
|
293
|
+
// Extract folders that match a search query from file paths
|
|
294
|
+
function extractMatchingFolders(files, search) {
|
|
295
|
+
if (!search) return [];
|
|
296
|
+
|
|
297
|
+
const searchLower = search.toLowerCase();
|
|
298
|
+
const folderSet = new Set();
|
|
299
|
+
|
|
300
|
+
files.forEach(file => {
|
|
301
|
+
const parts = file.Key.split('/');
|
|
302
|
+
// Remove filename
|
|
303
|
+
parts.pop();
|
|
304
|
+
|
|
305
|
+
let currentPath = '';
|
|
306
|
+
parts.forEach(part => {
|
|
307
|
+
const segmentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
308
|
+
currentPath = segmentPath;
|
|
309
|
+
|
|
310
|
+
// If any part of the path matches the search, include that full path as a folder
|
|
311
|
+
// AND ensure we don't include the full file path, just the directory
|
|
312
|
+
if (part.toLowerCase().includes(searchLower)) {
|
|
313
|
+
folderSet.add(currentPath);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return Array.from(folderSet).sort();
|
|
319
|
+
}
|
|
320
|
+
|
|
259
321
|
// Render files table
|
|
260
322
|
function renderFilesTable(files, folders = [], prefix = '') {
|
|
261
323
|
// Determine which files/folders to show based on view mode
|
|
262
324
|
let displayFiles = [];
|
|
263
325
|
let displayFolders = [];
|
|
264
326
|
|
|
265
|
-
|
|
327
|
+
// Force flat view if searching
|
|
328
|
+
const isSearchActive = !!currentSearch;
|
|
329
|
+
const mode = isSearchActive ? 'flat' : currentViewMode;
|
|
330
|
+
|
|
331
|
+
if (mode === 'flat') {
|
|
266
332
|
// In flat mode, show all files in the current loaded page/list regardless of folder depth
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
333
|
+
if (isSearchActive) {
|
|
334
|
+
// Search Mode:
|
|
335
|
+
// 1. Show files where FILENAME matches the search term
|
|
336
|
+
const searchLower = currentSearch.toLowerCase();
|
|
337
|
+
displayFiles = files.filter(f => {
|
|
338
|
+
const fileName = f.Key.split('/').pop();
|
|
339
|
+
return fileName.toLowerCase().includes(searchLower);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// 2. Show folders where FOLDER NAME matches the search term
|
|
343
|
+
displayFolders = extractMatchingFolders(files, currentSearch);
|
|
344
|
+
} else {
|
|
345
|
+
// Normal Flat Mode: Show all files, no folders
|
|
346
|
+
displayFiles = files;
|
|
347
|
+
displayFolders = [];
|
|
348
|
+
}
|
|
272
349
|
} else {
|
|
273
350
|
// Normal Folder View logic
|
|
274
351
|
// Filter files to show only those at current level (not in subfolders)
|
|
@@ -338,7 +415,10 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
338
415
|
<div class="file-name" style="font-weight: 700;">
|
|
339
416
|
<i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${folderName}/
|
|
340
417
|
</div>
|
|
341
|
-
</div>
|
|
418
|
+
</div>
|
|
419
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn dtm" onclick="event.stopPropagation(); confirmDelete('${folderPath}/', true);" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
|
|
420
|
+
<i class="fas fa-trash"></i>
|
|
421
|
+
</button>
|
|
342
422
|
</div>
|
|
343
423
|
</td>
|
|
344
424
|
<td>
|
|
@@ -349,7 +429,7 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
349
429
|
</td>
|
|
350
430
|
<td style="text-align: center;">
|
|
351
431
|
<div class="btn-group" role="group">
|
|
352
|
-
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
|
|
432
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn" onclick="event.stopPropagation(); confirmDelete('${folderPath}/', true);" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
|
|
353
433
|
<i class="fas fa-trash"></i>
|
|
354
434
|
</button>
|
|
355
435
|
</div>
|
|
@@ -431,18 +511,23 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
431
511
|
if (allRows === '') {
|
|
432
512
|
document.getElementById('fileListContainer').innerHTML = `
|
|
433
513
|
<div class="empty-state">
|
|
434
|
-
<
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
514
|
+
<div style="margin-bottom: 1.5rem; position: relative; display: inline-block;">
|
|
515
|
+
<i class="fas fa-folder-open" style="font-size: 4rem; color: #cbd5e0;"></i>
|
|
516
|
+
<i class="fas fa-search" style="font-size: 2rem; color: var(--primary-color-ml); position: absolute; bottom: -5px; right: -10px; background: white; border-radius: 50%; padding: 5px;"></i>
|
|
517
|
+
</div>
|
|
518
|
+
<h4>No files found</h4>
|
|
519
|
+
<p style="max-width: 400px; margin: 0 auto 1.5rem; color: var(--text-muted-color-ml);">
|
|
520
|
+
${prefix ? `This folder <strong>${prefix}</strong> is currently empty.` : 'Your bucket is empty. Start by uploading files or creating a new folder.'}
|
|
521
|
+
</p>
|
|
522
|
+
<div style="display:flex; gap:0.75rem; justify-content:center; flex-wrap: wrap;">
|
|
523
|
+
<button class="btn btn-primary" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); document.getElementById('fileInput').click()">
|
|
524
|
+
<i class="fas fa-cloud-upload-alt"></i> Upload Files
|
|
440
525
|
</button>
|
|
441
526
|
<button class="btn btn-outline-secondary" onclick="createFolder()">
|
|
442
|
-
<i class="fas fa-folder-plus"></i>
|
|
527
|
+
<i class="fas fa-folder-plus"></i> New Folder
|
|
443
528
|
</button>
|
|
444
529
|
${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
|
|
445
|
-
<i class="fas fa-
|
|
530
|
+
<i class="fas fa-home"></i> Go to Root
|
|
446
531
|
</button>` : ''}
|
|
447
532
|
</div>
|
|
448
533
|
</div>
|
|
@@ -461,7 +546,7 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
461
546
|
document.getElementById('fileListContainer').innerHTML = `
|
|
462
547
|
${breadcrumb}
|
|
463
548
|
<div class="table-responsive">
|
|
464
|
-
<div id="bulkActionsBar" style="display:none; padding: 0.
|
|
549
|
+
<div id="bulkActionsBar" style="display:none; padding: 0.5rem; border-bottom: 1px solid var(--gray-light-color-ml); background: var(--light-color-ml);">
|
|
465
550
|
<span id="bulkSelectedCount">0 selected</span>
|
|
466
551
|
<div style="float: right;">
|
|
467
552
|
<button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
|
|
@@ -665,10 +750,10 @@ function renderUploadQueue() {
|
|
|
665
750
|
container.innerHTML = uploadQueue.map(item => `
|
|
666
751
|
<div class="upload-item" data-id="${item.id}">
|
|
667
752
|
<div class="meta">
|
|
668
|
-
<div
|
|
669
|
-
|
|
753
|
+
<div style="display:flex; align-items:center; gap:0.75rem; min-width:0; width:100%; margin-bottom:5px;">
|
|
754
|
+
<div class="file-icon"><i class="fas fa-file"></i></div>
|
|
670
755
|
<div class="name">${item.file.name}</div>
|
|
671
|
-
<div class="form-text">${formatFileSize(item.file.size)}</div>
|
|
756
|
+
<div class="form-text file-size" style="white-space:nowrap; color:var(--text-muted-color-ml); font-size:0.875rem; margin-left:auto;">${formatFileSize(item.file.size)}</div>
|
|
672
757
|
</div>
|
|
673
758
|
</div>
|
|
674
759
|
<div style="min-width:220px;">
|
|
@@ -828,22 +913,7 @@ function startAllUploads() {
|
|
|
828
913
|
document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
|
|
829
914
|
document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
|
|
830
915
|
|
|
831
|
-
// Reset upload form
|
|
832
|
-
function resetUploadForm() {
|
|
833
|
-
const uploadBtn = document.getElementById('uploadBtn');
|
|
834
|
-
const uploadProgress = document.getElementById('uploadProgress');
|
|
835
916
|
|
|
836
|
-
setTimeout(() => {
|
|
837
|
-
uploadBtn.disabled = false;
|
|
838
|
-
uploadBtn.classList.remove('uploading');
|
|
839
|
-
uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload File';
|
|
840
|
-
uploadProgress.style.display = 'none';
|
|
841
|
-
|
|
842
|
-
// Reset progress
|
|
843
|
-
document.getElementById('progressBar').style.width = '0%';
|
|
844
|
-
document.getElementById('uploadPercent').textContent = '0%';
|
|
845
|
-
}, 3000);
|
|
846
|
-
}
|
|
847
917
|
|
|
848
918
|
// Add hover effects and animations
|
|
849
919
|
document.addEventListener('DOMContentLoaded', function () {
|
|
@@ -853,10 +923,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
853
923
|
const initialPage = parseInt(urlParams.get('page')) || 1;
|
|
854
924
|
loadFiles(initialPage, initialPrefix);
|
|
855
925
|
|
|
856
|
-
//
|
|
926
|
+
// Search behavior: Trigger when the Search button is clicked OR when the user presses Enter in the search input
|
|
857
927
|
const searchInputEl = document.getElementById('searchInput');
|
|
858
|
-
if (searchInputEl
|
|
859
|
-
|
|
928
|
+
if (searchInputEl) {
|
|
929
|
+
// Trigger search when Enter is pressed in the input
|
|
930
|
+
searchInputEl.addEventListener('keydown', function (e) {
|
|
931
|
+
if (e.key === 'Enter') {
|
|
932
|
+
e.preventDefault();
|
|
933
|
+
performSearch();
|
|
934
|
+
}
|
|
935
|
+
});
|
|
860
936
|
}
|
|
861
937
|
|
|
862
938
|
// Close modal when clicking outside
|
|
@@ -997,6 +1073,7 @@ function showPreviewModal(encodedKey, filename, extension) {
|
|
|
997
1073
|
contentEl.appendChild(iframe);
|
|
998
1074
|
} else if (textTypes.includes(extension)) {
|
|
999
1075
|
dialog.classList.add('preview-compact');
|
|
1076
|
+
|
|
1000
1077
|
fetch(`/mbkbucket/view/${encodedKey}`)
|
|
1001
1078
|
.then(r => r.text())
|
|
1002
1079
|
.then(text => {
|
|
@@ -1223,9 +1300,47 @@ async function createFolder() {
|
|
|
1223
1300
|
}
|
|
1224
1301
|
}
|
|
1225
1302
|
|
|
1226
|
-
//
|
|
1227
|
-
|
|
1228
|
-
|
|
1303
|
+
// Create new file
|
|
1304
|
+
async function createFile() {
|
|
1305
|
+
let name = prompt('Enter new file name:');
|
|
1306
|
+
if (!name) return;
|
|
1307
|
+
|
|
1308
|
+
name = name.trim();
|
|
1309
|
+
if (!name.includes('.')) {
|
|
1310
|
+
name += '.txt';
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Ensure no leading slashes, we upload to currentPrefix
|
|
1314
|
+
if (name.startsWith('/')) name = name.substring(1);
|
|
1315
|
+
|
|
1316
|
+
// Check if valid
|
|
1317
|
+
if (!name) return showAlert('Invalid file name', 'error');
|
|
1318
|
+
|
|
1319
|
+
// Create empty file content
|
|
1320
|
+
const file = new File([" "], name, { type: "text/plain" });
|
|
1321
|
+
const formData = new FormData();
|
|
1322
|
+
formData.append('file', file);
|
|
1323
|
+
formData.append('prefix', currentPrefix);
|
|
1324
|
+
|
|
1325
|
+
try {
|
|
1326
|
+
const resp = await fetch('/mbkbucket/upload', {
|
|
1327
|
+
method: 'POST',
|
|
1328
|
+
body: formData
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
const json = await resp.json();
|
|
1332
|
+
if (json.success) {
|
|
1333
|
+
showAlert('File created', 'success');
|
|
1334
|
+
loadFiles(currentPage, currentPrefix);
|
|
1335
|
+
} else {
|
|
1336
|
+
showAlert(json.error || 'Create file failed', 'error');
|
|
1337
|
+
}
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
showAlert('Create file failed: ' + err.message, 'error');
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
|
|
1229
1344
|
|
|
1230
1345
|
// mbkbucket shared utilities (exposed to global window for backward compatibility)
|
|
1231
1346
|
(function (w) {
|
package/views/bucket.handlebars
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{{!-- File Management System - Bucket Dashboard --}}
|
|
2
|
-
<link rel="stylesheet" href="/
|
|
3
|
-
<div id="bucketAppData" data-app-name="{{
|
|
2
|
+
<link rel="stylesheet" href="/mbkbucket/bucketadmin.css?v={{assetVersion}}">
|
|
3
|
+
<div id="bucketAppData" data-app-name="{{APP_NAME}}" hidden></div>
|
|
4
4
|
<!-- File Statistics -->
|
|
5
5
|
<div class="dashboard-card file-stats">
|
|
6
6
|
<div class="card-body">
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
</div>
|
|
37
37
|
{{/if}}
|
|
38
38
|
|
|
39
|
-
<div class="
|
|
39
|
+
<div class="mb-4">
|
|
40
40
|
<!-- Upload Form -->
|
|
41
|
-
<div class="col-lg-
|
|
41
|
+
<div class="col-lg-12">
|
|
42
42
|
<div class="dashboard-card h-100">
|
|
43
43
|
<div class="card-header">
|
|
44
44
|
<h5 class="card-title">
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
|
|
55
55
|
<div class="mb-3">
|
|
56
56
|
<label for="fileInput" class="form-label">Select Files or drag & drop</label>
|
|
57
|
-
<div id="dropZone" class="
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
<div
|
|
57
|
+
<div id="dropZone" class="upload-drop-zone">
|
|
58
|
+
<div class="drop-zone-content">
|
|
59
|
+
<i class="fas fa-cloud-upload-alt"></i>
|
|
60
|
+
<div class="drop-text">Drop files here or click to browse</div>
|
|
61
|
+
<div class="drop-hint">Supports multiple files</div>
|
|
62
62
|
</div>
|
|
63
63
|
</div>
|
|
64
|
-
<input type="file" class="form-control d-none" id="fileInput" name="files" multiple>
|
|
64
|
+
<input type="file" class="form-control d-none" style="display: none;" id="fileInput" name="files" multiple>
|
|
65
65
|
<div class="form-text">
|
|
66
66
|
<i class="fas fa-info-circle"></i>
|
|
67
67
|
You can drop multiple files. Large files will use chunked upload when available.
|
|
@@ -99,41 +99,6 @@
|
|
|
99
99
|
</div>
|
|
100
100
|
</div>
|
|
101
101
|
</div>
|
|
102
|
-
|
|
103
|
-
<!-- Search Filter -->
|
|
104
|
-
<div class="col-lg-6">
|
|
105
|
-
<div class="dashboard-card h-100">
|
|
106
|
-
<div class="card-header">
|
|
107
|
-
<h5 class="card-title">
|
|
108
|
-
<div class="card-icon">
|
|
109
|
-
<i class="fas fa-search"></i>
|
|
110
|
-
</div>
|
|
111
|
-
Search Files & Folders
|
|
112
|
-
</h5>
|
|
113
|
-
</div>
|
|
114
|
-
<div class="card-body">
|
|
115
|
-
<div class="mb-3">
|
|
116
|
-
<label for="searchInput" class="form-label">Search by Name</label>
|
|
117
|
-
<input type="text" class="form-control form-control-lg" id="searchInput" placeholder="Enter search term..."
|
|
118
|
-
onkeypress="if(event.key==='Enter') performSearch()">
|
|
119
|
-
<div class="form-text">
|
|
120
|
-
<i class="fas fa-info-circle"></i>
|
|
121
|
-
Search across all files and folders by name (case-insensitive)
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
|
125
|
-
<button type="button" class="btn btn-primary btn-lg" onclick="performSearch()">
|
|
126
|
-
<i class="fas fa-search"></i>
|
|
127
|
-
Search
|
|
128
|
-
</button>
|
|
129
|
-
<button type="button" class="btn btn-outline-secondary btn-lg" onclick="clearSearch()">
|
|
130
|
-
<i class="fas fa-times"></i>
|
|
131
|
-
Clear
|
|
132
|
-
</button>
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
102
|
</div>
|
|
138
103
|
|
|
139
104
|
<!-- File List -->
|
|
@@ -145,7 +110,23 @@
|
|
|
145
110
|
</div>
|
|
146
111
|
<span id="fileListTitle">Files</span>
|
|
147
112
|
</h5>
|
|
148
|
-
|
|
113
|
+
|
|
114
|
+
<!-- Inline Search (moved from separate card) -->
|
|
115
|
+
<div class="search-inline" style="flex:1; max-width:640px; display:flex; gap:0.5rem; align-items:center; justify-content:center;">
|
|
116
|
+
<input type="text" class="form-control form-control-sm" id="searchInput" placeholder="Search files and folders..." aria-label="Search files">
|
|
117
|
+
<button type="button" class="btn btn-sm btn-primary" onclick="performSearch()" aria-label="Search"><i class="fas fa-search"></i></button>
|
|
118
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSearch()" aria-label="Clear search"><i class="fas fa-times"></i></button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="btn-group" role="group" aria-label="Actions">
|
|
122
|
+
<button type="button" class="btn btn-sm btn-success" onclick="createFile()" title="New Text File">
|
|
123
|
+
<i class="fas fa-file-alt"></i> New File
|
|
124
|
+
</button>
|
|
125
|
+
<button type="button" class="btn btn-sm btn-outline-success" onclick="createFolder()" title="New Folder">
|
|
126
|
+
<i class="fas fa-folder-plus"></i> New Folder
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
149
130
|
<div class="btn-group" role="group" aria-label="View Mode">
|
|
150
131
|
<button type="button" class="btn btn-sm btn-light view-toggle-btn active" id="viewBtnFolder" onclick="changeViewMode('folder')" title="Folder View" style="border-top-right-radius: 0; border-bottom-right-radius: 0;">
|
|
151
132
|
<i class="fas fa-folder"></i> Folders
|
|
@@ -230,7 +211,7 @@
|
|
|
230
211
|
</div>
|
|
231
212
|
</div>
|
|
232
213
|
<div class="modal-body">
|
|
233
|
-
<div class="preview-content" aria-live="polite">
|
|
214
|
+
<div class="preview-content" id="previewContent" aria-live="polite">
|
|
234
215
|
<div class="empty-preview">
|
|
235
216
|
<i class="fas fa-spinner fa-spin"></i>
|
|
236
217
|
<div style="margin-top:0.5rem; color:var(--gray-color-ml);">Loading preview...</div>
|
|
@@ -250,4 +231,7 @@
|
|
|
250
231
|
</div>
|
|
251
232
|
</div>
|
|
252
233
|
|
|
253
|
-
|
|
234
|
+
<!-- Toast Container -->
|
|
235
|
+
<div id="toast-container" style="position: fixed; bottom: 20px; right: 20px; z-index: 1050; display: flex; flex-direction: column; gap: 10px;"></div>
|
|
236
|
+
|
|
237
|
+
<script src="/mbkbucket/bucketadmin.js?v={{assetVersion}}"></script>
|