mbkbucket 1.1.1 → 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/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.mbkbucket_testl === "dev") {
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
 
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/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.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",
@@ -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(5)::before {
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
- <button type="submit" class="btn btn-danger">
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;" colspan="2">
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); font-style: italic;">Folder</span>
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
- <span style="color: var(--gray-color-ml); font-style: italic;">Click to open</span>
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
- if (key) confirmDelete(key);
2396
+ const isFolder = dbtn.dataset.isFolder === 'true';
2397
+ if (key) confirmDelete(key, isFolder);
2387
2398
  return;
2388
2399
  }
2389
2400
 
2390
- // Handle folder path clicks
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
- if (folderPathElement) {
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);