mbkbucket 1.1.0 → 1.1.2
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/README.md +42 -2
- package/index.d.ts +16 -0
- package/index.js +4 -1
- package/lib/bucket.js +8 -2
- package/lib/index.js +17 -0
- package/lib/s3.js +63 -0
- package/package.json +2 -2
- package/views/bucketadmincontent.handlebars +39 -57
package/README.md
CHANGED
|
@@ -3,10 +3,50 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/mbkbucket)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
|
-
[](https://github.com/MIbnEKhalid/mbkbucket/actions/workflows/publish.yml)
|
|
7
|
+
|
|
8
|
+
## Usage ✅
|
|
9
|
+
|
|
10
|
+
Install:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install mbkbucket
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Quick examples
|
|
17
|
+
|
|
18
|
+
- Named imports from package root:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { uploadFile, downloadFile, listfiles, getBucketConfig, packageJson, bucket } from 'mbkbucket';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- Default aggregate import:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import mbkbucket from 'mbkbucket';
|
|
28
|
+
// mbkbucket.uploadFile(...)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Direct subpath imports (optional):
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { uploadFile } from 'mbkbucket/lib/s3';
|
|
35
|
+
import { packageJson } from 'mbkbucket/lib/config/index';
|
|
36
|
+
import bucketRouter from 'mbkbucket/lib/bucket';
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Notes
|
|
40
|
+
|
|
41
|
+
- All functions, objects, and variables exported from `lib/` are re-exported at the package root for convenience.
|
|
42
|
+
- The package includes TypeScript declarations (`index.d.ts`) so IDEs and TypeScript projects get proper typings.
|
|
43
|
+
- The default export of the package is the configured Express `server` instance; the `bucket` router is available as `bucket` (named export).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
7
47
|
|
|
8
48
|
## Contact
|
|
9
49
|
|
|
10
50
|
For questions or contributions, please contact Muhammad Bin Khalid at [mbktechstudio.com/Support](https://mbktechstudio.com/Support/?Project=MIbnEKhalidWeb), [support@mbktechstudio.com](mailto:support@mbktechstudio.com) or [chmuhammadbinkhalid28@gmail.com](mailto:chmuhammadbinkhalid28@gmail.com).
|
|
11
51
|
|
|
12
|
-
Developed by [Muhammad Bin Khalid](https://github.com/MIbnEKhalid) at [MBK Tech Studio](https://mbktech.org/).
|
|
52
|
+
Developed by [Muhammad Bin Khalid](https://github.com/MIbnEKhalid) at [MBK Tech Studio](https://mbktech.org/).
|
package/index.d.ts
CHANGED
|
@@ -73,6 +73,14 @@ export function deleteFiles(keys: string[], bucketName?: string): Promise<{
|
|
|
73
73
|
deletedAt: string;
|
|
74
74
|
}>;
|
|
75
75
|
|
|
76
|
+
export function deleteFolder(prefix: string, bucketName?: string): Promise<{
|
|
77
|
+
results?: any[];
|
|
78
|
+
deletedCount: number;
|
|
79
|
+
errors?: any[];
|
|
80
|
+
deletedAt: string;
|
|
81
|
+
prefix?: string;
|
|
82
|
+
}>;
|
|
83
|
+
|
|
76
84
|
export function listfiles(
|
|
77
85
|
prefix?: string,
|
|
78
86
|
options?: {
|
|
@@ -186,6 +194,14 @@ declare module "mbkbucket/lib/s3" {
|
|
|
186
194
|
errors: any[];
|
|
187
195
|
deletedAt: string;
|
|
188
196
|
}>;
|
|
197
|
+
|
|
198
|
+
export function deleteFolder(prefix: string, bucketName?: string): Promise<{
|
|
199
|
+
results?: any[];
|
|
200
|
+
deletedCount: number;
|
|
201
|
+
errors?: any[];
|
|
202
|
+
deletedAt: string;
|
|
203
|
+
prefix?: string;
|
|
204
|
+
}>;
|
|
189
205
|
export function listfiles(
|
|
190
206
|
prefix?: string,
|
|
191
207
|
options?: {
|
package/index.js
CHANGED
|
@@ -44,6 +44,7 @@ server.engine("handlebars", engine({
|
|
|
44
44
|
path.join(__dirname, "views"),
|
|
45
45
|
path.join(__dirname, "views/partial"),
|
|
46
46
|
path.join(__dirname, "node_modules/mbkauthe/views"),
|
|
47
|
+
path.join(__dirname, "../mbkauthe/views")
|
|
47
48
|
],
|
|
48
49
|
cache: process.env.NODE_ENV === "production",
|
|
49
50
|
helpers: {
|
|
@@ -132,13 +133,14 @@ server.set("view engine", "handlebars");
|
|
|
132
133
|
server.set("views", [
|
|
133
134
|
path.join(__dirname, "views"),
|
|
134
135
|
path.join(__dirname, "node_modules/mbkauthe/views"),
|
|
136
|
+
path.join(__dirname, "../mbkauthe/views")
|
|
135
137
|
]);
|
|
136
138
|
|
|
137
139
|
server.use(mbkauthe);
|
|
138
140
|
server.use(generalLimiter);
|
|
139
141
|
server.use(bucket);
|
|
140
142
|
|
|
141
|
-
if (process.env.
|
|
143
|
+
if (process.env.mbkbucket_test === "dev") {
|
|
142
144
|
|
|
143
145
|
console.log("[mbkbucket] Dev mode is enabled. Starting server in dev mode.");
|
|
144
146
|
|
|
@@ -195,4 +197,5 @@ if (process.env.test === "dev") {
|
|
|
195
197
|
await checkVersion();
|
|
196
198
|
|
|
197
199
|
|
|
200
|
+
export * from "./lib/index.js";
|
|
198
201
|
export default server;
|
package/lib/bucket.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { validateSessionAndRole, renderPage, mbkautheVar } from "mbkauthe";
|
|
3
|
-
import { listfiles, uploadFile, deleteFile, deleteFiles, downloadFile, getFileMetadata, fileExists, ensureKeyHasAppPrefix, ensurePrefix, getAppName, getBucketConfig } from "./s3.js"
|
|
3
|
+
import { listfiles, uploadFile, deleteFile, deleteFiles, deleteFolder, downloadFile, getFileMetadata, fileExists, ensureKeyHasAppPrefix, ensurePrefix, getAppName, getBucketConfig } from "./s3.js";
|
|
4
4
|
import multer from "multer";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import fs from 'fs';
|
|
@@ -225,7 +225,7 @@ router.post('/mbkbucket/create-folder', validateSessionAndRole('SuperAdmin'), as
|
|
|
225
225
|
|
|
226
226
|
router.post('/mbkbucket/delete', validateSessionAndRole('SuperAdmin'), async (req, res) => {
|
|
227
227
|
try {
|
|
228
|
-
const { key, keys } = req.body;
|
|
228
|
+
const { key, keys, folder } = req.body;
|
|
229
229
|
|
|
230
230
|
if (keys && Array.isArray(keys) && keys.length) {
|
|
231
231
|
// Bulk delete
|
|
@@ -238,6 +238,12 @@ router.post('/mbkbucket/delete', validateSessionAndRole('SuperAdmin'), async (re
|
|
|
238
238
|
let keyToDelete;
|
|
239
239
|
try { keyToDelete = ensureKeyHasAppPrefix(key); } catch (e) { return res.status(400).json({ success: false, error: e.message }); }
|
|
240
240
|
|
|
241
|
+
// If caller indicates it's a folder (or key ends with '/'), delete all objects under the prefix
|
|
242
|
+
if (folder === true || String(keyToDelete).endsWith('/')) {
|
|
243
|
+
await deleteFolder(keyToDelete);
|
|
244
|
+
return res.json({ success: true, message: 'Folder deleted successfully' });
|
|
245
|
+
}
|
|
246
|
+
|
|
241
247
|
await deleteFile(keyToDelete);
|
|
242
248
|
res.json({ success: true, message: 'File deleted successfully' });
|
|
243
249
|
} catch (error) {
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Barrel file: re-export all public items from lib
|
|
2
|
+
export * from './s3.js';
|
|
3
|
+
export * from './config/index.js';
|
|
4
|
+
export { default as bucket } from './bucket.js';
|
|
5
|
+
|
|
6
|
+
import * as s3 from './s3.js';
|
|
7
|
+
import * as config from './config/index.js';
|
|
8
|
+
import bucket from './bucket.js';
|
|
9
|
+
|
|
10
|
+
// Default aggregate export for convenience
|
|
11
|
+
const defaultExport = {
|
|
12
|
+
...s3,
|
|
13
|
+
...config,
|
|
14
|
+
bucket,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default defaultExport;
|
package/lib/s3.js
CHANGED
|
@@ -359,6 +359,69 @@ export async function deleteFiles(keys, bucketName = 'r2') {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
// List files with enhanced pagination and filtering
|
|
363
|
+
/**
|
|
364
|
+
* Delete all objects under a folder prefix (recursive).
|
|
365
|
+
* Accepts either a folder key (may include trailing slash) or a relative prefix.
|
|
366
|
+
* Normalizes the prefix to include the configured APP_NAME and ensures trailing slash.
|
|
367
|
+
*/
|
|
368
|
+
export async function deleteFolder(prefix, bucketName = 'r2') {
|
|
369
|
+
try {
|
|
370
|
+
if (!prefix) {
|
|
371
|
+
throw new Error('Prefix is required');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const cleaned = String(prefix).replace(/^\/+/, '').replace(/\/+$/g, '');
|
|
375
|
+
const effectivePrefix = ensurePrefix(cleaned) + '/';
|
|
376
|
+
|
|
377
|
+
const client = getBucketClient(bucketName);
|
|
378
|
+
const config = getBucketConfig(bucketName);
|
|
379
|
+
const bucket = config.BUCKET_NAME;
|
|
380
|
+
|
|
381
|
+
const keysToDelete = [];
|
|
382
|
+
let continuationToken = null;
|
|
383
|
+
|
|
384
|
+
// Paginate through all objects under the prefix
|
|
385
|
+
do {
|
|
386
|
+
const listCmd = new ListObjectsV2Command({
|
|
387
|
+
Bucket: bucket,
|
|
388
|
+
Prefix: effectivePrefix,
|
|
389
|
+
ContinuationToken: continuationToken,
|
|
390
|
+
MaxKeys: 1000
|
|
391
|
+
});
|
|
392
|
+
const resp = await client.send(listCmd);
|
|
393
|
+
const contents = resp.Contents || [];
|
|
394
|
+
for (const obj of contents) {
|
|
395
|
+
if (obj.Key) keysToDelete.push(obj.Key);
|
|
396
|
+
}
|
|
397
|
+
continuationToken = resp.IsTruncated ? resp.NextContinuationToken : null;
|
|
398
|
+
} while (continuationToken);
|
|
399
|
+
|
|
400
|
+
if (!keysToDelete.length) {
|
|
401
|
+
// Nothing found under this prefix; attempt to remove a zero-byte folder marker if present
|
|
402
|
+
const folderMarkerKey = effectivePrefix;
|
|
403
|
+
const exists = await fileExists(folderMarkerKey, bucketName);
|
|
404
|
+
if (exists) {
|
|
405
|
+
await deleteFile(folderMarkerKey, bucketName);
|
|
406
|
+
return { deletedCount: 1, deletedAt: new Date().toISOString(), prefix: effectivePrefix };
|
|
407
|
+
}
|
|
408
|
+
return { deletedCount: 0, deletedAt: new Date().toISOString(), prefix: effectivePrefix };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Use the existing deleteFiles helper which handles batching and error aggregation
|
|
412
|
+
const result = await deleteFiles(keysToDelete, bucketName);
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
...result,
|
|
416
|
+
prefix: effectivePrefix,
|
|
417
|
+
deletedAt: new Date().toISOString()
|
|
418
|
+
};
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error('Delete folder failed:', error);
|
|
421
|
+
throw new Error(`Delete folder failed: ${error.message}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
362
425
|
// List files with enhanced pagination and filtering
|
|
363
426
|
export async function listfiles(prefix = '', options = {}) {
|
|
364
427
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkbucket",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"types": "index.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"dev": "cross-env
|
|
12
|
+
"dev": "cross-env mbkbucket_test=dev nodemon index.js",
|
|
13
13
|
"test": "echo \\\"Error: no test specified\\\" && exit 1"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
@@ -1223,8 +1223,7 @@
|
|
|
1223
1223
|
|
|
1224
1224
|
/* Metadata row: folder / size / modified shown as compact badges */
|
|
1225
1225
|
.file-row td:nth-child(3),
|
|
1226
|
-
.file-row td:nth-child(4)
|
|
1227
|
-
.file-row td:nth-child(5) {
|
|
1226
|
+
.file-row td:nth-child(4) {
|
|
1228
1227
|
padding: 0rem 1rem !important;
|
|
1229
1228
|
display: flex;
|
|
1230
1229
|
align-items: center;
|
|
@@ -1234,20 +1233,13 @@
|
|
|
1234
1233
|
}
|
|
1235
1234
|
|
|
1236
1235
|
.file-row td:nth-child(3)::before {
|
|
1237
|
-
content: "Folder:";
|
|
1238
|
-
font-weight: 600;
|
|
1239
|
-
color: var(--dark-color-ml);
|
|
1240
|
-
margin-right: 0.35rem;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
.file-row td:nth-child(4)::before {
|
|
1244
1236
|
content: "Size:";
|
|
1245
1237
|
font-weight: 600;
|
|
1246
1238
|
color: var(--dark-color-ml);
|
|
1247
1239
|
margin-right: 0.35rem;
|
|
1248
1240
|
}
|
|
1249
1241
|
|
|
1250
|
-
.file-row td:nth-child(
|
|
1242
|
+
.file-row td:nth-child(4)::before {
|
|
1251
1243
|
content: "Modified:";
|
|
1252
1244
|
font-weight: 600;
|
|
1253
1245
|
color: var(--dark-color-ml);
|
|
@@ -1597,9 +1589,10 @@
|
|
|
1597
1589
|
<form id="deleteForm" method="post" action="/mbkbucket/delete" style="display: inline;">
|
|
1598
1590
|
<input type="hidden" name="key" id="deleteKey">
|
|
1599
1591
|
<input type="hidden" name="prefix" value="{{prefix}}">
|
|
1600
|
-
<
|
|
1592
|
+
<input type="hidden" name="folder" id="deleteIsFolder" value="">
|
|
1593
|
+
<button type="submit" class="btn btn-danger" id="deleteConfirmButton">
|
|
1601
1594
|
<i class="fas fa-trash"></i>
|
|
1602
|
-
Delete File
|
|
1595
|
+
<span id="deleteConfirmLabel">Delete File</span>
|
|
1603
1596
|
</button>
|
|
1604
1597
|
</form>
|
|
1605
1598
|
</div>
|
|
@@ -1879,7 +1872,7 @@
|
|
|
1879
1872
|
<td style="text-align:center;">
|
|
1880
1873
|
<!-- no selection for folders -->
|
|
1881
1874
|
</td>
|
|
1882
|
-
<td style="padding-left: 1.5rem;"
|
|
1875
|
+
<td style="padding-left: 1.5rem;">
|
|
1883
1876
|
<div style="display: flex; align-items: center;">
|
|
1884
1877
|
<div class="file-icon">
|
|
1885
1878
|
<i class="fas fa-folder"></i>
|
|
@@ -1892,13 +1885,17 @@
|
|
|
1892
1885
|
</div>
|
|
1893
1886
|
</td>
|
|
1894
1887
|
<td>
|
|
1895
|
-
<span style="color: var(--gray-color-ml);
|
|
1888
|
+
<span style="color: var(--gray-color-ml);">\u2014</span>
|
|
1896
1889
|
</td>
|
|
1897
|
-
<td>
|
|
1890
|
+
<td style="text-align: center;">
|
|
1898
1891
|
<span style="color: var(--gray-color-ml);">\u2014</span>
|
|
1899
1892
|
</td>
|
|
1900
1893
|
<td style="text-align: center;">
|
|
1901
|
-
<
|
|
1894
|
+
<div class="btn-group" role="group">
|
|
1895
|
+
<button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
|
|
1896
|
+
<i class="fas fa-trash"></i>
|
|
1897
|
+
</button>
|
|
1898
|
+
</div>
|
|
1902
1899
|
</td>
|
|
1903
1900
|
</tr>
|
|
1904
1901
|
`;
|
|
@@ -1922,9 +1919,6 @@
|
|
|
1922
1919
|
</div>
|
|
1923
1920
|
</div>
|
|
1924
1921
|
</td>
|
|
1925
|
-
<td>
|
|
1926
|
-
<span class="folder-path" data-full-path="${file.Key}"></span>
|
|
1927
|
-
</td>
|
|
1928
1922
|
<td>
|
|
1929
1923
|
${file.Size ? `<span class="file-size">${formatFileSize(file.Size)}</span>` : '<span style="color: var(--gray-color-ml);">N/A</span>'}
|
|
1930
1924
|
</td>
|
|
@@ -1994,12 +1988,9 @@
|
|
|
1994
1988
|
<th style="padding-left: 1.5rem;">
|
|
1995
1989
|
<i class="fas fa-file-alt"></i> Name
|
|
1996
1990
|
</th>
|
|
1997
|
-
<th>
|
|
1998
|
-
<i class="fas fa-folder"></i> Folder
|
|
1999
|
-
</th>
|
|
2000
1991
|
<th>
|
|
2001
1992
|
<i class="fas fa-weight"></i> Size
|
|
2002
|
-
</th>
|
|
1993
|
+
</th>
|
|
2003
1994
|
<th>
|
|
2004
1995
|
<i class="fas fa-calendar"></i> Last Modified
|
|
2005
1996
|
</th>
|
|
@@ -2085,9 +2076,26 @@
|
|
|
2085
2076
|
document.getElementById('paginationList').innerHTML = paginationHTML;
|
|
2086
2077
|
}
|
|
2087
2078
|
|
|
2088
|
-
function confirmDelete(key) {
|
|
2079
|
+
function confirmDelete(key, isFolder = false) {
|
|
2089
2080
|
document.getElementById('deleteKey').value = key;
|
|
2081
|
+
document.getElementById('deleteIsFolder').value = isFolder ? 'true' : '';
|
|
2090
2082
|
document.getElementById('deleteFileName').textContent = key;
|
|
2083
|
+
|
|
2084
|
+
// Update modal message and button label for folders
|
|
2085
|
+
const modalHeading = document.querySelector('#deleteModal .modal-body h6');
|
|
2086
|
+
const smallText = document.querySelector('#deleteModal .modal-body small');
|
|
2087
|
+
const confirmLabel = document.getElementById('deleteConfirmLabel');
|
|
2088
|
+
|
|
2089
|
+
if (isFolder) {
|
|
2090
|
+
if (modalHeading) modalHeading.textContent = 'Are you sure you want to delete this folder and all its contents?';
|
|
2091
|
+
if (smallText) smallText.textContent = 'This action will remove the folder and all files within it and cannot be undone.';
|
|
2092
|
+
if (confirmLabel) confirmLabel.textContent = 'Delete Folder';
|
|
2093
|
+
} else {
|
|
2094
|
+
if (modalHeading) modalHeading.textContent = 'Are you sure you want to delete this file?';
|
|
2095
|
+
if (smallText) smallText.textContent = 'This action cannot be undone.';
|
|
2096
|
+
if (confirmLabel) confirmLabel.textContent = 'Delete File';
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2091
2099
|
document.getElementById('deleteModal').classList.add('show');
|
|
2092
2100
|
document.getElementById('deleteModal').style.display = 'flex';
|
|
2093
2101
|
}
|
|
@@ -2102,6 +2110,7 @@
|
|
|
2102
2110
|
e.preventDefault();
|
|
2103
2111
|
|
|
2104
2112
|
const key = document.getElementById('deleteKey').value;
|
|
2113
|
+
const isFolder = document.getElementById('deleteIsFolder').value === 'true';
|
|
2105
2114
|
|
|
2106
2115
|
try {
|
|
2107
2116
|
const response = await fetch('/mbkbucket/delete', {
|
|
@@ -2109,7 +2118,7 @@
|
|
|
2109
2118
|
headers: {
|
|
2110
2119
|
'Content-Type': 'application/json'
|
|
2111
2120
|
},
|
|
2112
|
-
body: JSON.stringify({ key })
|
|
2121
|
+
body: JSON.stringify({ key, folder: isFolder })
|
|
2113
2122
|
});
|
|
2114
2123
|
|
|
2115
2124
|
const data = await response.json();
|
|
@@ -2382,21 +2391,14 @@
|
|
|
2382
2391
|
|
|
2383
2392
|
const dbtn = e.target.closest('.delete-btn');
|
|
2384
2393
|
if (dbtn) {
|
|
2394
|
+
e.stopPropagation();
|
|
2385
2395
|
const key = dbtn.dataset.key;
|
|
2386
|
-
|
|
2396
|
+
const isFolder = dbtn.dataset.isFolder === 'true';
|
|
2397
|
+
if (key) confirmDelete(key, isFolder);
|
|
2387
2398
|
return;
|
|
2388
2399
|
}
|
|
2389
2400
|
|
|
2390
|
-
|
|
2391
|
-
const folderPath = e.target.closest('.folder-path.clickable');
|
|
2392
|
-
if (folderPath) {
|
|
2393
|
-
const folder = folderPath.getAttribute('data-folder');
|
|
2394
|
-
if (folder) {
|
|
2395
|
-
// Load files with new prefix
|
|
2396
|
-
loadFiles(1, folder);
|
|
2397
|
-
}
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2401
|
+
|
|
2400
2402
|
});
|
|
2401
2403
|
});
|
|
2402
2404
|
|
|
@@ -2622,27 +2624,7 @@
|
|
|
2622
2624
|
fileNameElement.textContent = truncatedName;
|
|
2623
2625
|
fileNameElement.title = filename; // Full name on hover
|
|
2624
2626
|
|
|
2625
|
-
|
|
2626
|
-
const displayFolder = folder || '/';
|
|
2627
|
-
folderPathElement.textContent = displayFolder;
|
|
2628
|
-
folderPathElement.title = folder ? `Click to filter by folder: ${folder}` : 'Root folder'; // Full path on hover
|
|
2629
|
-
|
|
2630
|
-
// Make folder clickable if not root and add keyboard accessibility
|
|
2631
|
-
if (folder) {
|
|
2632
|
-
folderPathElement.classList.add('clickable');
|
|
2633
|
-
folderPathElement.setAttribute('data-folder', folder);
|
|
2634
|
-
folderPathElement.setAttribute('role', 'button');
|
|
2635
|
-
folderPathElement.setAttribute('tabindex', '0');
|
|
2636
|
-
// allow Enter/Space to trigger folder navigation
|
|
2637
|
-
folderPathElement.onkeydown = function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); loadFiles(1, folder); } };
|
|
2638
|
-
} else {
|
|
2639
|
-
folderPathElement.classList.remove('clickable');
|
|
2640
|
-
folderPathElement.removeAttribute('data-folder');
|
|
2641
|
-
folderPathElement.removeAttribute('role');
|
|
2642
|
-
folderPathElement.removeAttribute('tabindex');
|
|
2643
|
-
folderPathElement.onkeydown = null;
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2627
|
+
|
|
2646
2628
|
|
|
2647
2629
|
const iconClass = getFileIcon(filename);
|
|
2648
2630
|
const typeCategory = getFileTypeCategory(filename);
|