mbkbucket 1.2.1 → 1.3.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 CHANGED
@@ -49,6 +49,7 @@ server.get('/mbkbucket/bucketadmin.css', (req, res) => {
49
49
  if (err) res.status(err.status || 404).end();
50
50
  });
51
51
  });
52
+
52
53
  server.get('/mbkbucket/bucketadmin.js', (req, res) => {
53
54
  res.set('Cache-Control', `public, max-age=${staticMaxAgeSeconds}, immutable`);
54
55
  res.sendFile(path.join(__dirname, 'public', 'bucketadmin.js'), (err) => {
package/lib/bucket.js CHANGED
@@ -33,9 +33,7 @@ router.get(['/mbkbucket/info', "/mbkbucket/i"], validateSessionAndRole('SuperAdm
33
33
 
34
34
  // API endpoint for fetching files
35
35
  router.get('/mbkbucket/api/files', validateSessionAndRole('SuperAdmin'), async (req, res) => {
36
- //mock response for testing
37
- //return res.json({success: true,files: [],prefix: getAppName(),search: '',bucket: '',currentPage: 1,totalPages: 1,hasNextPage: false,hasPrevPage: false,nextPage: 2,prevPage: 1,totalFiles: 1});
38
- const { prefix = '', page = '1', search = '' } = req.query;
36
+ const { prefix = '', page = '1', search = '', recursive = 'true', token = '' } = req.query;
39
37
  const pageSize = 100;
40
38
  let effectivePrefix;
41
39
  try {
@@ -44,64 +42,121 @@ router.get('/mbkbucket/api/files', validateSessionAndRole('SuperAdmin'), async (
44
42
  return res.status(400).json({ success: false, error: e.message });
45
43
  }
46
44
 
45
+ // Determine mode: Optimized (Folder View, No Search) vs Legacy/Deep (Flat View or Search)
46
+ const isSearch = search && search.trim().length > 0;
47
+ // If recursive is explicitly 'false', we respect it. Default to true if missing (though frontend should send it)
48
+ // Logic: Optimization works only if NOT recursive and NOT searching.
49
+ const isRecursive = recursive === 'true';
50
+ const useOptimizedListing = !isSearch && !isRecursive;
51
+
47
52
  try {
48
53
  let files = [];
49
- let continuationToken = undefined;
54
+ let folders = []; // For CommonPrefixes (subfolders)
55
+ let nextContinuationToken = null;
50
56
  let usedPrefix = effectivePrefix;
51
57
  let legacyFallback = false;
52
58
 
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);
59
+ if (useOptimizedListing) {
60
+ // --- OPTIMIZED PATH: S3 Delimiter (Folder View) ---
61
+ // We process only one page from S3. This reduces API calls significantly.
62
+
63
+ // FIX: Ensure root listing uses trailing slash to prevent "app-name" matching "app-name-backup"
64
+ // and ensure directory browsing works correctly if ensurePrefix didn't append slash (which it now does if present in input).
65
+ // But for root, input is empty, ensurePrefix returns "app", so we force "app/" here.
66
+ let listPrefix = effectivePrefix;
67
+ if (listPrefix === getAppName() && !listPrefix.endsWith('/')) {
68
+ listPrefix += '/';
58
69
  }
59
- continuationToken = result.nextToken;
60
- } while (continuationToken);
61
-
62
- // If no files found under the app prefixed path, try root as a legacy fallback
63
- if (!files.length) {
64
- try {
65
- const rootResult = await listfiles('');
66
- if ((rootResult.Contents || []).length) {
67
- files = rootResult.Contents || [];
68
- usedPrefix = '';
69
- legacyFallback = true;
70
+
71
+ const result = await listfiles(listPrefix, {
72
+ continuationToken: token || undefined, // Use token from query if provided
73
+ delimiter: '/',
74
+ maxKeys: 1000 // Fetch a healthy batch.
75
+ });
76
+
77
+ files = result.Contents || [];
78
+ // S3 returns CommonPrefixes for folders when delimiter is used
79
+ folders = (result.CommonPrefixes || []).map(p => p.Prefix).filter(p => p !== effectivePrefix);
80
+ nextContinuationToken = result.NextContinuationToken;
81
+
82
+ // Note: In optimized mode, totalFiles is unknown without scanning everything.
83
+ } else {
84
+ // --- LEGACY PATH: Recursive Scan (Search or Flat View) ---
85
+ let continuationToken = undefined;
86
+
87
+ // Fetch all files recursively (handling S3 pagination)
88
+ do {
89
+ const result = await listfiles(effectivePrefix, { continuationToken });
90
+ if (result.Contents) {
91
+ files = files.concat(result.Contents);
70
92
  }
71
- } catch (fallbackErr) {
72
- // ignore fallback errors and continue with empty files
73
- console.warn('Fallback root listing failed:', fallbackErr.message);
93
+ continuationToken = result.nextToken;
94
+ } while (continuationToken);
95
+
96
+ // If no files found under the app prefixed path, try root as a legacy fallback
97
+ if (!files.length && !search) {
98
+ try {
99
+ const rootResult = await listfiles('');
100
+ if ((rootResult.Contents || []).length) {
101
+ files = rootResult.Contents || [];
102
+ usedPrefix = '';
103
+ legacyFallback = true;
104
+ }
105
+ } catch (fallbackErr) {
106
+ console.warn('Fallback root listing failed:', fallbackErr.message);
107
+ }
108
+ }
109
+
110
+ // Apply search filter if provided
111
+ if (isSearch) {
112
+ const searchLower = search.toLowerCase();
113
+ files = files.filter(file => {
114
+ return file.Key.toLowerCase().includes(searchLower);
115
+ });
74
116
  }
75
117
  }
76
118
 
77
- // Apply search filter if provided
78
- if (search.trim()) {
79
- const searchLower = search.toLowerCase();
80
- files = files.filter(file => {
81
- return file.Key.toLowerCase().includes(searchLower);
82
- });
119
+ // Response Construction
120
+ if (useOptimizedListing) {
121
+ res.json({
122
+ success: true,
123
+ files: files,
124
+ folders: folders,
125
+ prefix: effectivePrefix,
126
+ search: search,
127
+ // In optimized mode, we don't support random access "Page X". We support "Next".
128
+ currentPage: 1,
129
+ totalPages: nextContinuationToken ? 9999 : 1,
130
+ hasNextPage: !!nextContinuationToken,
131
+ nextContinuationToken: nextContinuationToken,
132
+ mode: 'optimized',
133
+ totalFiles: -1
134
+ });
135
+
136
+ } else {
137
+ // Legacy Pagination Logic (In-Memory)
138
+ const totalFiles = files.length;
139
+ const totalPages = Math.max(1, Math.ceil(totalFiles / pageSize));
140
+ const currentPage = Math.max(1, parseInt(page, 10) || 1);
141
+ const startIndex = (currentPage - 1) * pageSize;
142
+ const paginatedFiles = files.slice(startIndex, startIndex + pageSize);
143
+
144
+ res.json({
145
+ success: true,
146
+ files: paginatedFiles,
147
+ folders: [], // In recursive mode, frontend calculates folders from file paths
148
+ prefix: effectivePrefix,
149
+ search: search,
150
+ currentPage: currentPage,
151
+ totalPages,
152
+ hasNextPage: currentPage < totalPages,
153
+ hasPrevPage: currentPage > 1,
154
+ nextPage: currentPage + 1,
155
+ prevPage: Math.max(1, currentPage - 1),
156
+ totalFiles
157
+ });
83
158
  }
84
159
 
85
- // Pagination logic
86
- const totalFiles = files.length;
87
- const totalPages = Math.max(1, Math.ceil(totalFiles / pageSize));
88
- const currentPage = Math.max(1, parseInt(page, 10) || 1);
89
- const startIndex = (currentPage - 1) * pageSize;
90
- const paginatedFiles = files.slice(startIndex, startIndex + pageSize);
91
-
92
- res.json({
93
- success: true,
94
- files: paginatedFiles,
95
- prefix: effectivePrefix,
96
- search: search,
97
- currentPage: currentPage,
98
- totalPages,
99
- hasNextPage: currentPage < totalPages,
100
- hasPrevPage: currentPage > 1,
101
- nextPage: currentPage + 1,
102
- prevPage: Math.max(1, currentPage - 1),
103
- totalFiles
104
- });
105
160
  } catch (error) {
106
161
  console.error("Error listing files:", error);
107
162
  res.status(500).json({
@@ -128,7 +183,7 @@ router.post('/mbkbucket/upload', validateSessionAndRole('SuperAdmin'), upload.si
128
183
  return res.status(400).json({ success: false, error: e.message });
129
184
  }
130
185
 
131
- const key = `${effectivePrefix}/${req.file.originalname}`;
186
+ const key = `${effectivePrefix.replace(/\/+$/, '')}/${req.file.originalname}`;
132
187
 
133
188
  // Check if file already exists
134
189
  const exists = await fileExists(key);
@@ -196,7 +251,7 @@ router.post('/mbkbucket/upload-complete', validateSessionAndRole('SuperAdmin'),
196
251
  // Upload to bucket
197
252
  let effectivePrefix;
198
253
  try { effectivePrefix = ensurePrefix(prefix); } catch (e) { return res.status(400).json({ success: false, error: e.message }); }
199
- const key = `${effectivePrefix}/${fileName}`;
254
+ const key = `${effectivePrefix.replace(/\/+$/, '')}/${fileName}`;
200
255
 
201
256
  // Check existence
202
257
  const exists = await fileExists(key);
@@ -226,7 +281,7 @@ router.post('/mbkbucket/create-folder', validateSessionAndRole('SuperAdmin'), as
226
281
  let effectivePrefix;
227
282
  try { effectivePrefix = ensurePrefix(prefix); } catch (e) { return res.status(400).json({ success: false, error: e.message }); }
228
283
 
229
- const folderKey = `${effectivePrefix}/${folderName}/`;
284
+ const folderKey = `${effectivePrefix.replace(/\/+$/, '')}/${folderName}/`;
230
285
 
231
286
  // Check if exists
232
287
  const exists = await fileExists(folderKey);
package/lib/s3.js CHANGED
@@ -66,10 +66,15 @@ export function ensureKeyHasAppPrefix(key = '') {
66
66
  export function ensurePrefix(prefix = '') {
67
67
  const app = getAppName();
68
68
  if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
69
- let p = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/g, '');
69
+ // Only strip LEADING slashes. Preserve trailing slashes for directory listing.
70
+ let p = String(prefix || '').replace(/^\/+/, '');
71
+
70
72
  if (!p) return `${app}`;
73
+
71
74
  // If prefix already equals app or already starts with app/, normalize and return single app prefix
72
- if (p === app) return app;
75
+ if (p === app) return app; // If p is exactly "app", we return "app". (Caller should append / if listing root)
76
+ if (p === `${app}/`) return `${app}/`;
77
+
73
78
  if (p.startsWith(`${app}/`)) {
74
79
  // collapse repeated app/ sequences
75
80
  const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkbucket",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
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",
@@ -43,6 +43,48 @@
43
43
  padding: 1rem;
44
44
  }
45
45
 
46
+ /* Collapsible Upload Section */
47
+ .upload-card-header {
48
+ display: flex;
49
+ justify-content: space-between;
50
+ align-items: center;
51
+ }
52
+
53
+ .collapse-btn {
54
+ background: none;
55
+ border: none;
56
+ color: white;
57
+ padding: 0.5rem;
58
+ border-radius: 4px;
59
+ transition: all 0.3s ease;
60
+ cursor: pointer;
61
+ }
62
+
63
+ .collapse-btn:hover {
64
+ background: rgba(255, 255, 255, 0.1);
65
+ }
66
+
67
+ .collapse-btn i {
68
+ transition: transform 0.3s ease;
69
+ }
70
+
71
+ .collapse-btn.expanded i {
72
+ transform: rotate(180deg);
73
+ }
74
+
75
+ .collapsible-content {
76
+ padding: 0rem !important;
77
+ max-height: 0;
78
+ overflow: hidden;
79
+ transition: max-height 0.3s ease-out;
80
+ }
81
+
82
+ .collapsible-content.expanded {
83
+ padding: 1rem !important;
84
+ max-height: 2000px;
85
+ transition: max-height 0.3s ease-in;
86
+ }
87
+
46
88
  /* File Stats Card - Welcome Style */
47
89
  .file-stats {
48
90
  background: linear-gradient(135deg, var(--primary-color-ml) 0%, var(--primary-dark-color-ml) 100%);
@@ -1077,32 +1119,30 @@
1077
1119
  align-items: center;
1078
1120
  }
1079
1121
 
1080
- /* Skeleton loaders */
1122
+ /* Skeleton loaders - tuned grid to equalize column spacing visually */
1081
1123
  .skeleton-row {
1082
- display: flex;
1124
+ display: grid;
1125
+ grid-template-columns: 48px minmax(120px, 1fr) minmax(120px, 1fr) minmax(120px, 240px); /* icon + 3 columns with balanced min widths */
1083
1126
  gap: 1rem;
1084
1127
  padding: 1rem;
1128
+ align-items: center;
1129
+ justify-items: stretch; /* make cells span their tracks evenly */
1085
1130
  }
1086
1131
 
1132
+ /* make skeleton boxes flexible and responsive */
1087
1133
  .skeleton-box {
1088
1134
  background: linear-gradient(90deg, #f5f7fa, #eef2f6);
1089
1135
  border-radius: 6px;
1090
- }
1091
-
1092
- .skeleton-box.s-80 {
1093
- width: 80px;
1094
1136
  height: 16px;
1137
+ width: 100%;
1138
+ min-width: 0;
1139
+ justify-self: stretch;
1095
1140
  }
1096
1141
 
1097
- .skeleton-box.s-200 {
1098
- width: 200px;
1099
- height: 16px;
1100
- }
1101
-
1102
- .skeleton-box.s-40 {
1103
- width: 40px;
1104
- height: 16px;
1105
- }
1142
+ /* semantic size hints (max widths) and alignment to ensure visual balance */
1143
+ .skeleton-box.s-40 { max-width: 48px; justify-self: start; }
1144
+ .skeleton-box.s-80 { max-width: 160px; justify-self: center; }
1145
+ .skeleton-box.s-200 { max-width: 220px; justify-self: end; }
1106
1146
 
1107
1147
  .skeleton-row .file-icon {
1108
1148
  width: 40px;
@@ -1110,6 +1150,26 @@
1110
1150
  border-radius: 8px;
1111
1151
  }
1112
1152
 
1153
+ /* Responsive adjustments */
1154
+ @media (max-width: 992px) {
1155
+ .skeleton-row {
1156
+ grid-template-columns: 40px minmax(100px, 1fr) minmax(100px, 1fr) minmax(100px, 200px);
1157
+ gap: 0.9rem;
1158
+ }
1159
+ .skeleton-box.s-200 { max-width: 180px; }
1160
+ }
1161
+
1162
+ @media (max-width: 768px) {
1163
+ .skeleton-row {
1164
+ grid-template-columns: 40px 1fr 1fr 1fr;
1165
+ gap: 0.75rem;
1166
+ }
1167
+ .skeleton-box.s-80 { justify-self: start; }
1168
+ .skeleton-box.s-200 { max-width: 140px; justify-self: end; }
1169
+ }
1170
+
1171
+
1172
+
1113
1173
  .btn:disabled {
1114
1174
  opacity: 0.5;
1115
1175
  cursor: not-allowed;
@@ -1187,12 +1247,21 @@
1187
1247
  }
1188
1248
 
1189
1249
  .stats-content {
1190
- flex-direction: column;
1191
- text-align: center;
1250
+ gap: 0.75rem;
1192
1251
  }
1193
1252
 
1194
1253
  .stats-icon {
1195
- margin: 0 auto;
1254
+ width: 50px;
1255
+ height: 50px;
1256
+ font-size: 1.3rem;
1257
+ }
1258
+
1259
+ .stats-text h2 {
1260
+ font-size: 1.3rem;
1261
+ }
1262
+
1263
+ .stats-text p {
1264
+ font-size: 0.85rem;
1196
1265
  }
1197
1266
 
1198
1267
  .filter-form {
@@ -1209,8 +1278,23 @@
1209
1278
  padding: 1rem;
1210
1279
  }
1211
1280
 
1281
+ .stats-content {
1282
+ gap: 0.5rem;
1283
+ }
1284
+
1285
+ .stats-icon {
1286
+ width: 40px;
1287
+ height: 40px;
1288
+ font-size: 1.2rem;
1289
+ }
1290
+
1212
1291
  .stats-text h2 {
1213
- font-size: 1.35rem;
1292
+ font-size: 1.2rem;
1293
+ }
1294
+
1295
+ .stats-text p {
1296
+ font-size: 0.8rem;
1297
+ margin: 0.25rem 0 0;
1214
1298
  }
1215
1299
 
1216
1300
  .btn-group {
@@ -1481,6 +1565,67 @@
1481
1565
  display: none;
1482
1566
  }
1483
1567
 
1568
+ /* Additional utility classes for removing inline styles */
1569
+ .file-list-header {
1570
+ display: flex;
1571
+ justify-content: space-between;
1572
+ align-items: center;
1573
+ flex-wrap: wrap;
1574
+ gap: 1rem;
1575
+ }
1576
+
1577
+ .search-inline {
1578
+ flex: 1;
1579
+ max-width: 640px;
1580
+ display: flex;
1581
+ gap: 0.5rem;
1582
+ align-items: center;
1583
+ justify-content: center;
1584
+ }
1585
+
1586
+ .view-btn-left {
1587
+ border-top-right-radius: 0;
1588
+ border-bottom-right-radius: 0;
1589
+ }
1590
+
1591
+ .view-btn-right {
1592
+ border-top-left-radius: 0;
1593
+ border-bottom-left-radius: 0;
1594
+ }
1595
+
1596
+ .pagination-container {
1597
+ display: none;
1598
+ padding: 1rem;
1599
+ border-top: 1px solid var(--gray-light-color-ml);
1600
+ background: var(--light-color-ml);
1601
+ }
1602
+
1603
+ .modal-body-text-style {
1604
+ color: var(--gray-color-ml);
1605
+ margin-bottom: 0;
1606
+ }
1607
+
1608
+ .modal-body-text-top {
1609
+ color: var(--gray-color-ml);
1610
+ margin-top: 0.5rem;
1611
+ margin-bottom: 0;
1612
+ }
1613
+
1614
+ .modal-header-spacing {
1615
+ margin-top: 0.5rem;
1616
+ color: var(--gray-color-ml);
1617
+ }
1618
+
1619
+ .toast-container {
1620
+ position: fixed;
1621
+ bottom: 20px;
1622
+ right: 20px;
1623
+ z-index: 1050;
1624
+ display: flex;
1625
+ flex-direction: column;
1626
+ gap: 10px;
1627
+ }
1628
+
1484
1629
  @media (max-width: 768px) {
1485
1630
  .dtm {
1486
1631
  display: block;
@@ -7,6 +7,7 @@ let currentOrder = 'asc';
7
7
  let currentViewMode = 'folder'; // 'folder' or 'flat'
8
8
  let pageFiles = [];
9
9
  let pageFolders = [];
10
+ let currentNextToken = null;
10
11
  const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
11
12
  // Bucket is configured centrally on the server; client should not select a bucket.
12
13
 
@@ -70,7 +71,7 @@ function showAlert(message, type = 'success') {
70
71
  }
71
72
 
72
73
  // Fetch and render files
73
- async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort, order = currentOrder) {
74
+ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort, order = currentOrder, token = '') {
74
75
  currentPage = page;
75
76
  currentPrefix = prefix;
76
77
  currentSearch = search;
@@ -109,19 +110,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
109
110
  <div class="skeleton-box s-40"></div>
110
111
  <div class="skeleton-box s-200"></div>
111
112
  <div class="skeleton-box s-80"></div>
112
- <div class="skeleton-box s-80"></div>
113
- </div>
114
- <div class="skeleton-row">
115
- <div class="skeleton-box s-40"></div>
116
113
  <div class="skeleton-box s-200"></div>
117
- <div class="skeleton-box s-80"></div>
118
- <div class="skeleton-box s-80"></div>
119
114
  </div>
120
115
  <div class="skeleton-row">
121
116
  <div class="skeleton-box s-40"></div>
122
117
  <div class="skeleton-box s-200"></div>
123
118
  <div class="skeleton-box s-80"></div>
124
- <div class="skeleton-box s-80"></div>
119
+ <div class="skeleton-box s-200"></div>
125
120
  </div>
126
121
  </div>
127
122
  `;
@@ -129,15 +124,28 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
129
124
  try {
130
125
  const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
131
126
  const sortParam = `&sort=${sort}&order=${order}`;
132
- const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}`);
127
+
128
+ // Determine recursive mode
129
+ const isFolderMode = currentViewMode === 'folder';
130
+ const recursiveParam = `&recursive=${!isFolderMode}`; // false if folder mode, true if flat mode
131
+ const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
132
+
133
+ const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`);
133
134
  const data = await response.json();
134
135
 
135
136
  if (!data.success) {
136
137
  throw new Error(data.error || 'Failed to load files');
137
138
  }
139
+
140
+ // Store next token
141
+ currentNextToken = data.nextContinuationToken || null;
138
142
 
139
143
  // Update stats
140
- totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
144
+ if (data.totalFiles === -1) {
145
+ totalFilesCount.textContent = `${(data.files.length + (data.folders ? data.folders.length : 0))}${data.hasNextPage ? '+' : ''} Items`;
146
+ } else {
147
+ totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
148
+ }
141
149
 
142
150
  if (search) {
143
151
  filterInfo.textContent = `Search results for: "${search}"`;
@@ -149,7 +157,9 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
149
157
  }
150
158
 
151
159
  // Render files or empty state
152
- if (data.files.length === 0 && !prefix) {
160
+ const hasContent = (data.files && data.files.length > 0) || (data.folders && data.folders.length > 0);
161
+
162
+ if (!hasContent && !prefix && !search) {
153
163
  container.innerHTML = `
154
164
  <div class="empty-state">
155
165
  <i class="fas fa-folder-open"></i>
@@ -163,11 +173,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
163
173
  paginationContainer.style.display = 'none';
164
174
  } else {
165
175
  // Store for client-side sorting and view switching
166
- pageFiles = data.files;
176
+ pageFiles = data.files || [];
167
177
 
168
- // Only calculate folders if we are in search or folder mode (for potential future switching)
169
- // But mainly we rely on currentViewMode in the render function
170
- pageFolders = search ? [] : extractFoldersAtCurrentLevel(data.files, prefix);
178
+ if (data.mode === 'optimized') {
179
+ pageFolders = data.folders || [];
180
+ } else {
181
+ pageFolders = search ? [] : extractFoldersAtCurrentLevel(data.files, prefix);
182
+ }
171
183
 
172
184
  renderFilesTable(pageFiles, pageFolders, prefix);
173
185
  renderPagination(data);
@@ -400,7 +412,10 @@ function renderFilesTable(files, folders = [], prefix = '') {
400
412
 
401
413
  // Generate folder rows (only used in folder mode)
402
414
  const folderRows = displayFolders.map(folderPath => {
403
- const folderName = folderPath.split('/').pop();
415
+ // Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
416
+ const cleanPath = folderPath.replace(/\/+$/, '');
417
+ const folderName = cleanPath.split('/').pop();
418
+
404
419
  return `
405
420
  <tr class="folder-row" onclick="loadFiles(1, '${folderPath}')" title="Open folder: ${folderName}">
406
421
  <td style="text-align:center;">
@@ -453,10 +468,10 @@ function renderFilesTable(files, folders = [], prefix = '') {
453
468
  folderPath = fullPath.substring(0, lastSlashIndex + 1); // include trailing slash
454
469
  }
455
470
 
456
- const iconClass = getFileIcon(filename);
457
- const typeCategory = getFileTypeCategory(filename);
458
- const truncatedName = truncateFileName(filename);
459
- const isFileViewable = isViewable(filename);
471
+ const iconClass = window.getFileIcon ? window.getFileIcon(filename) : 'fas fa-file';
472
+ const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(filename) : '';
473
+ const truncatedName = (window.truncateFileName ? window.truncateFileName(filename) : filename) || filename;
474
+ const isFileViewable = window.isViewable ? window.isViewable(filename) : false;
460
475
 
461
476
  // If in flat mode, we might want to show the folder path more prominently
462
477
  const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
@@ -493,6 +508,9 @@ function renderFilesTable(files, folders = [], prefix = '') {
493
508
  data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${!isFileViewable ? 'disabled style="opacity:0.5"' : ''}>
494
509
  <i class="fas fa-eye"></i>
495
510
  </button>
511
+ <button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link" style="color:white;">
512
+ <i class="fas fa-link"></i>
513
+ </button>
496
514
  <a href="/mbkbucket/download/${encodedKey}" class="btn btn-success btn-sm"
497
515
  title="Download file">
498
516
  <i class="fas fa-download"></i>
@@ -587,63 +605,137 @@ function renderFilesTable(files, folders = [], prefix = '') {
587
605
 
588
606
  // Render pagination
589
607
  function renderPagination(data) {
590
- const { currentPage, totalPages, hasPrevPage, hasNextPage } = data;
608
+ const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
591
609
 
592
610
  let paginationHTML = '';
593
611
 
594
- // Previous button
595
- if (hasPrevPage) {
596
- paginationHTML += `
597
- <li class="page-item">
598
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
599
- <i class="fas fa-chevron-left"></i> Previous
600
- </a>
601
- </li>
602
- `;
603
- } else {
612
+ if (mode === 'optimized') {
613
+ // Optimized Mode: Token-based pagination (Next only, limited Prev)
614
+
615
+ // Previous button (Disabled for now as we don't track history stack of tokens)
604
616
  paginationHTML += `
605
617
  <li class="page-item disabled">
606
- <span class="page-link">
618
+ <span class="page-link" title="Previous page not supported in optimized view">
607
619
  <i class="fas fa-chevron-left"></i> Previous
608
620
  </span>
609
621
  </li>
610
622
  `;
611
- }
612
-
613
- // Page numbers
614
- for (let i = 1; i <= totalPages; i++) {
615
- if (i === currentPage) {
616
- paginationHTML += `
623
+
624
+ // Current Page Indicator
625
+ paginationHTML += `
617
626
  <li class="page-item active">
618
- <span class="page-link">${i}</span>
627
+ <span class="page-link">${currentPage}</span>
619
628
  </li>
620
- `;
621
- } else {
622
- paginationHTML += `
629
+ `;
630
+
631
+ // Next button
632
+ if (hasNextPage) {
633
+ // Quote the token correctly
634
+ const tokenStr = currentNextToken ? currentNextToken.replace(/'/g, "\\'") : "";
635
+ paginationHTML += `
623
636
  <li class="page-item">
624
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${i}, '${currentPrefix}', '${currentSearch}')">${i}</a>
637
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}', '${currentSort}', '${currentOrder}', '${tokenStr}')">
638
+ Next <i class="fas fa-chevron-right"></i>
639
+ </a>
625
640
  </li>
626
641
  `;
627
- }
628
- }
642
+ } else {
643
+ paginationHTML += `
644
+ <li class="page-item disabled">
645
+ <span class="page-link">
646
+ Next <i class="fas fa-chevron-right"></i>
647
+ </span>
648
+ </li>
649
+ `;
650
+ }
629
651
 
630
- // Next button
631
- if (hasNextPage) {
632
- paginationHTML += `
633
- <li class="page-item">
634
- <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
635
- Next <i class="fas fa-chevron-right"></i>
636
- </a>
637
- </li>
638
- `;
639
652
  } else {
640
- paginationHTML += `
641
- <li class="page-item disabled">
642
- <span class="page-link">
643
- Next <i class="fas fa-chevron-right"></i>
644
- </span>
645
- </li>
646
- `;
653
+ // Legacy Mode: Numbered pagination
654
+
655
+ // Previous button
656
+ if (hasPrevPage) {
657
+ paginationHTML += `
658
+ <li class="page-item">
659
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
660
+ <i class="fas fa-chevron-left"></i> Previous
661
+ </a>
662
+ </li>
663
+ `;
664
+ } else {
665
+ paginationHTML += `
666
+ <li class="page-item disabled">
667
+ <span class="page-link">
668
+ <i class="fas fa-chevron-left"></i> Previous
669
+ </span>
670
+ </li>
671
+ `;
672
+ }
673
+
674
+ // Smart Page Numbers (limit visible pages)
675
+ const maxPagesToShow = 7;
676
+ let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
677
+ let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
678
+
679
+ if (endPage - startPage + 1 < maxPagesToShow) {
680
+ startPage = Math.max(1, endPage - maxPagesToShow + 1);
681
+ }
682
+
683
+ // First page
684
+ if (startPage > 1) {
685
+ paginationHTML += `
686
+ <li class="page-item">
687
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(1, '${currentPrefix}', '${currentSearch}')">1</a>
688
+ </li>`;
689
+ if (startPage > 2) {
690
+ paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
691
+ }
692
+ }
693
+
694
+ for (let i = startPage; i <= endPage; i++) {
695
+ if (i === currentPage) {
696
+ paginationHTML += `
697
+ <li class="page-item active">
698
+ <span class="page-link">${i}</span>
699
+ </li>
700
+ `;
701
+ } else {
702
+ paginationHTML += `
703
+ <li class="page-item">
704
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${i}, '${currentPrefix}', '${currentSearch}')">${i}</a>
705
+ </li>
706
+ `;
707
+ }
708
+ }
709
+
710
+ // Last page
711
+ if (endPage < totalPages) {
712
+ if (endPage < totalPages - 1) {
713
+ paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
714
+ }
715
+ paginationHTML += `
716
+ <li class="page-item">
717
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${totalPages}, '${currentPrefix}', '${currentSearch}')">${totalPages}</a>
718
+ </li>`;
719
+ }
720
+
721
+ // Next button
722
+ if (hasNextPage) {
723
+ paginationHTML += `
724
+ <li class="page-item">
725
+ <a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
726
+ Next <i class="fas fa-chevron-right"></i>
727
+ </a>
728
+ </li>
729
+ `;
730
+ } else {
731
+ paginationHTML += `
732
+ <li class="page-item disabled">
733
+ <span class="page-link">
734
+ Next <i class="fas fa-chevron-right"></i>
735
+ </span>
736
+ </li>
737
+ `;
738
+ }
647
739
  }
648
740
 
649
741
  document.getElementById('paginationList').innerHTML = paginationHTML;
@@ -915,8 +1007,33 @@ document.getElementById('clearQueueBtn')?.addEventListener('click', function ()
915
1007
 
916
1008
 
917
1009
 
1010
+ // Initialize upload section collapse functionality
1011
+ function initializeUploadCollapse() {
1012
+ const toggleBtn = document.getElementById('uploadToggleBtn');
1013
+ const uploadSection = document.getElementById('uploadSection');
1014
+
1015
+ if (!toggleBtn || !uploadSection) return;
1016
+
1017
+ toggleBtn.addEventListener('click', function() {
1018
+ const isExpanded = uploadSection.classList.contains('expanded');
1019
+
1020
+ if (isExpanded) {
1021
+ // Collapse
1022
+ uploadSection.classList.remove('expanded');
1023
+ toggleBtn.classList.remove('expanded');
1024
+ } else {
1025
+ // Expand
1026
+ uploadSection.classList.add('expanded');
1027
+ toggleBtn.classList.add('expanded');
1028
+ }
1029
+ });
1030
+ }
1031
+
918
1032
  // Add hover effects and animations
919
1033
  document.addEventListener('DOMContentLoaded', function () {
1034
+ // Initialize collapsible upload section
1035
+ initializeUploadCollapse();
1036
+
920
1037
  // Load initial files (support both legacy 'prefix' and new 'folder' query param)
921
1038
  const urlParams = new URLSearchParams(window.location.search);
922
1039
  const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || bucketAppName || '';
@@ -72,7 +72,7 @@
72
72
  }
73
73
 
74
74
  .logo img {
75
- width: 40px
75
+ width: 35px
76
76
  }
77
77
 
78
78
  .main-content {
@@ -40,15 +40,18 @@
40
40
  <!-- Upload Form -->
41
41
  <div class="col-lg-12">
42
42
  <div class="dashboard-card h-100">
43
- <div class="card-header">
43
+ <div class="card-header upload-card-header" id="uploadToggleBtn">
44
44
  <h5 class="card-title">
45
45
  <div class="card-icon">
46
46
  <i class="fas fa-upload"></i>
47
47
  </div>
48
48
  Upload File
49
49
  </h5>
50
+ <button type="button" class="collapse-btn">
51
+ <i class="fas fa-chevron-down"></i>
52
+ </button>
50
53
  </div>
51
- <div class="card-body">
54
+ <div class="card-body collapsible-content" id="uploadSection">
52
55
  <form id="uploadForm" action="/mbkbucket/upload" method="post" enctype="multipart/form-data">
53
56
  <input type="hidden" name="prefix" value="{{prefix}}">
54
57
 
@@ -103,7 +106,7 @@
103
106
 
104
107
  <!-- File List -->
105
108
  <div class="dashboard-card">
106
- <div class="card-header" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:1rem;">
109
+ <div class="card-header file-list-header">
107
110
  <h5 class="card-title">
108
111
  <div>
109
112
  <i class="fas fa-folder-open"></i>
@@ -112,7 +115,7 @@
112
115
  </h5>
113
116
 
114
117
  <!-- 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;">
118
+ <div class="search-inline">
116
119
  <input type="text" class="form-control form-control-sm" id="searchInput" placeholder="Search files and folders..." aria-label="Search files">
117
120
  <button type="button" class="btn btn-sm btn-primary" onclick="performSearch()" aria-label="Search"><i class="fas fa-search"></i></button>
118
121
  <button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSearch()" aria-label="Clear search"><i class="fas fa-times"></i></button>
@@ -128,10 +131,10 @@
128
131
  </div>
129
132
 
130
133
  <div class="btn-group" role="group" aria-label="View Mode">
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;">
134
+ <button type="button" class="btn btn-sm btn-light view-toggle-btn view-btn-left active" id="viewBtnFolder" onclick="changeViewMode('folder')" title="Folder View">
132
135
  <i class="fas fa-folder"></i> Folders
133
136
  </button>
134
- <button type="button" class="btn btn-sm btn-outline-light view-toggle-btn" id="viewBtnFlat" onclick="changeViewMode('flat')" title="Flat List (All Files)" style="border-top-left-radius: 0; border-bottom-left-radius: 0;">
137
+ <button type="button" class="btn btn-sm btn-outline-light view-toggle-btn view-btn-right" id="viewBtnFlat" onclick="changeViewMode('flat')" title="Flat List (All Files)">
135
138
  <i class="fas fa-list-ul"></i> Flat View
136
139
  </button>
137
140
  </div>
@@ -146,8 +149,7 @@
146
149
  </div>
147
150
 
148
151
  <!-- Pagination -->
149
- <div id="paginationContainer"
150
- style="display: none; padding: 1rem; border-top: 1px solid var(--gray-light-color-ml); background: var(--light-color-ml);">
152
+ <div id="paginationContainer" class="pagination-container">
151
153
  <nav aria-label="File pagination">
152
154
  <ul class="pagination" id="paginationList">
153
155
  </ul>
@@ -171,10 +173,10 @@
171
173
  <div class="modal-body">
172
174
  <i class="fas fa-trash-alt fa-3x"></i>
173
175
  <h6>Are you sure you want to delete this file?</h6>
174
- <p style="color: var(--gray-color-ml); margin-bottom: 0;">
176
+ <p class="modal-body-text-style">
175
177
  <strong id="deleteFileName" style="color: var(--dark-color-ml);"></strong>
176
178
  </p>
177
- <p style="color: var(--gray-color-ml); margin-top: 0.5rem; margin-bottom: 0;">
179
+ <p class="modal-body-text-top">
178
180
  <small>This action cannot be undone.</small>
179
181
  </p>
180
182
  </div>
@@ -214,7 +216,7 @@
214
216
  <div class="preview-content" id="previewContent" aria-live="polite">
215
217
  <div class="empty-preview">
216
218
  <i class="fas fa-spinner fa-spin"></i>
217
- <div style="margin-top:0.5rem; color:var(--gray-color-ml);">Loading preview...</div>
219
+ <div class="modal-header-spacing">Loading preview...</div>
218
220
  </div>
219
221
  </div>
220
222
  </div>
@@ -232,6 +234,6 @@
232
234
  </div>
233
235
 
234
236
  <!-- 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>
237
+ <div id="toast-container" class="toast-container"></div>
236
238
 
237
239
  <script src="/mbkbucket/bucketadmin.js?v={{assetVersion}}"></script>