mbkbucket 3.1.0 → 3.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkbucket",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Flexible S3/R2 bucket management — library, Express router, and CLI for mbktech.org applications. Provides upload, download, listing, multipart utilities, signed URLs, and a command-line interface with built-in configuration validation and health checks",
5
5
  "author": "Muhammad Bin Khalid",
6
6
  "homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
package/src/app.js CHANGED
@@ -9,6 +9,7 @@ import { notFoundHandler, globalErrorHandler } from "./middleware/error-handler.
9
9
  import bucketRoutes from "./routes/index.js";
10
10
  import { packageJson } from "./config/index.js";
11
11
  import { createLogger } from "./utils/logger.js";
12
+ import { commonHandlebarsHelpers } from "./utils/helpers.js";
12
13
 
13
14
  const debugServer = createLogger('server');
14
15
 
@@ -46,8 +47,7 @@ export function createApp() {
46
47
  // Handlebars configuration
47
48
  server.engine("handlebars", engine({
48
49
  extname: ".handlebars",
49
- defaultLayout: "main",
50
- layoutsDir: path.join(__dirname, "..", "views", "layouts"),
50
+ defaultLayout: false,
51
51
  partialsDir: [
52
52
  path.join(__dirname, "..", "views", "templates"),
53
53
  path.join(__dirname, "..", "views", "templates", "notice"),
@@ -58,9 +58,7 @@ export function createApp() {
58
58
  ],
59
59
  cache: process.env.NODE_ENV === "production",
60
60
  helpers: {
61
- eq: function (a, b) {
62
- return a === b;
63
- },
61
+ ...commonHandlebarsHelpers,
64
62
  mbkbucket_cachebuster: function () {
65
63
  return "?v=" + packageJson.version;
66
64
  }
@@ -425,9 +425,11 @@ export async function downloadFileHandler(req, res) {
425
425
 
426
426
  try {
427
427
  const bucketName = req.activeBucket;
428
+ const rawKey = req.params.key;
429
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
428
430
  let keyToUse;
429
431
  try {
430
- keyToUse = ensureKeyHasAppPrefix(req.params.key);
432
+ keyToUse = ensureKeyHasAppPrefix(key);
431
433
  } catch (e) {
432
434
  return res.status(400).json({ success: false, error: e.message });
433
435
  }
@@ -1,5 +1,7 @@
1
+ import path from "path";
1
2
  import { resolveBucketName, getAvailableBucketNames } from "../services/s3.service.js";
2
3
  import { renderPage, mbkautheVar } from "mbkauthe";
4
+ import { commonHandlebarsHelpers } from "../utils/helpers.js";
3
5
 
4
6
  /**
5
7
  * Render the main bucket dashboard page.
@@ -7,13 +9,9 @@ import { renderPage, mbkautheVar } from "mbkauthe";
7
9
  export function renderBucketDashboard(req, res) {
8
10
  const allBuckets = getAvailableBucketNames();
9
11
  const configuredDefaultBucket = mbkautheVar?.bucket;
10
-
11
- let bucketfilePath;
12
- if (mbkautheVar?.APP_NAME === "portal") {
13
- bucketfilePath = "bucketportal.handlebars";
14
- } else {
15
- bucketfilePath = "bucket.handlebars";
16
- }
12
+ const isPortal = mbkautheVar?.APP_NAME === "portal";
13
+ const bucketfilePath = isPortal ? "bucketportal.handlebars" : "bucket.handlebars";
14
+ const portalLayout = isPortal ? path.resolve(process.cwd(), "views", "layouts", "main") : false;
17
15
 
18
16
  let selectedBucket;
19
17
 
@@ -26,9 +24,10 @@ export function renderBucketDashboard(req, res) {
26
24
  isSelected: name === fallbackSelected,
27
25
  isDefault: name === configuredDefaultBucket
28
26
  }));
29
- return renderPage(req, res, bucketfilePath, mbkautheVar?.APP_NAME === "portal" ? true : false, {
27
+ return renderPage(req, res, bucketfilePath, isPortal, {
30
28
  page: "Admin Bucket",
31
- layout: true,
29
+ helpers: commonHandlebarsHelpers,
30
+ ...(isPortal ? { layout: portalLayout } : {}),
32
31
  bucketvar: mbkautheVar?.bucket,
33
32
  selectedBucket: fallbackSelected,
34
33
  bucketOptions,
@@ -45,8 +44,10 @@ export function renderBucketDashboard(req, res) {
45
44
  isDefault: name === configuredDefaultBucket
46
45
  }));
47
46
 
48
- renderPage(req, res, bucketfilePath, mbkautheVar?.APP_NAME === "portal" ? true : false, {
47
+ renderPage(req, res, bucketfilePath, isPortal, {
49
48
  page: "Admin Bucket",
49
+ helpers: commonHandlebarsHelpers,
50
+ ...(isPortal ? { layout: portalLayout } : {}),
50
51
  bucketvar: selectedBucket,
51
52
  selectedBucket,
52
53
  bucketOptions,
@@ -192,9 +192,11 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
192
192
  export async function viewFile(req, res) {
193
193
  try {
194
194
  const bucketName = req.activeBucket;
195
+ const rawKey = req.params.key;
196
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
195
197
  let keyToUse;
196
198
  try {
197
- keyToUse = ensureKeyHasAppPrefix(req.params.key);
199
+ keyToUse = ensureKeyHasAppPrefix(key);
198
200
  } catch (e) {
199
201
  return res.status(400).json({ message: e.message });
200
202
  }
@@ -216,7 +218,8 @@ export async function viewFile(req, res) {
216
218
  export async function playerPage(req, res) {
217
219
  try {
218
220
  const bucketName = req.activeBucket;
219
- const key = req.params.key;
221
+ const rawKey = req.params.key;
222
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
220
223
  let keyToUse;
221
224
  try {
222
225
  keyToUse = ensureKeyHasAppPrefix(key);
@@ -240,7 +243,8 @@ export async function playerPage(req, res) {
240
243
  export async function publicView(req, res) {
241
244
  try {
242
245
  const bucketName = resolveBucketName();
243
- const key = req.params.key;
246
+ const rawKey = req.params.key;
247
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
244
248
 
245
249
  // No directory indexing
246
250
  if (!key || key.endsWith('/')) {
@@ -61,6 +61,6 @@ router.post('/create-folder', sessRole('SuperAdmin'), createFolder);
61
61
  router.post('/delete', sessRole('SuperAdmin'), deleteItems);
62
62
 
63
63
  // Download
64
- router.get('/download/:key(*)', sessRole('SuperAdmin'), downloadFileHandler);
64
+ router.get('/download/*key', sessRole('SuperAdmin'), downloadFileHandler);
65
65
 
66
66
  export default router;
@@ -12,14 +12,14 @@ const router = express.Router();
12
12
  router.use(requireBucketView);
13
13
 
14
14
  // Player page for video/audio
15
- router.get('/player/:key(*)', sessRole('SuperAdmin'), playerPage);
15
+ router.get('/player/*key', sessRole('SuperAdmin'), playerPage);
16
16
 
17
17
  // Inline file view
18
- router.get('/view/:key(*)', sessRole('SuperAdmin'), viewFile);
18
+ router.get('/view/*key', sessRole('SuperAdmin'), viewFile);
19
19
 
20
20
  // Public view (conditionally enabled)
21
21
  if (mbkbucketVar?.publiView_enabled) {
22
- router.get('/p_view/:key(*)', pviewRateLimit, pviewSecurity, publicView);
22
+ router.get('/p_view/*key', pviewRateLimit, pviewSecurity, publicView);
23
23
  }
24
24
 
25
25
  export default router;
@@ -74,6 +74,9 @@ function isRootModeApp(appName) {
74
74
  }
75
75
 
76
76
  export function ensureKeyHasAppPrefix(key = '') {
77
+ if (Array.isArray(key)) {
78
+ key = key.join('/');
79
+ }
77
80
  const app = getAppName();
78
81
  if (!key) throw new Error('Key is required');
79
82
 
@@ -103,6 +106,9 @@ export function ensureKeyHasAppPrefix(key = '') {
103
106
  }
104
107
 
105
108
  export function ensurePrefix(prefix = '') {
109
+ if (Array.isArray(prefix)) {
110
+ prefix = prefix.join('/');
111
+ }
106
112
  const app = getAppName();
107
113
 
108
114
  if (prefix && (prefix.includes('../') || prefix.includes('..\\') || /\.\.[\\/]/.test(prefix))) {
@@ -369,12 +375,13 @@ export async function downloadFile(key, options = {}) {
369
375
  key: keyToDownload,
370
376
  };
371
377
  } catch (error) {
372
- console.error(`Download failed for key ${key}:`, error);
373
-
374
- if (error && error.$metadata && error.$metadata.httpStatusCode === 304) {
378
+ if (error && (error.$metadata?.httpStatusCode === 304 || error.name === '304' || error.name === 'NotModified')) {
379
+ debugS3('File not modified (304) for key %s', keyToDownload);
375
380
  return { notModified: true, key: keyToDownload };
376
381
  }
377
382
 
383
+ console.error(`Download failed for key ${key}:`, error);
384
+
378
385
  if (error.name === 'NoSuchKey') {
379
386
  throw new Error(`File not found: ${key}`);
380
387
  } else if (error.name === 'AccessDenied') {
@@ -171,3 +171,27 @@ export function renderPlayerPage(safeFileName, encodedKey, bucketQuery) {
171
171
  </body>
172
172
  </html>`;
173
173
  }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Common Handlebars helpers
177
+ // ---------------------------------------------------------------------------
178
+
179
+ export const commonHandlebarsHelpers = {
180
+ eq: (a, b) => a === b,
181
+ neq: (a, b) => a !== b,
182
+ or: (...args) => args.slice(0, -1).some(Boolean),
183
+ and: (...args) => args.slice(0, -1).every(Boolean),
184
+ not: (val) => !val,
185
+ gt: (a, b) => a > b,
186
+ gte: (a, b) => a >= b,
187
+ lt: (a, b) => a < b,
188
+ lte: (a, b) => a <= b,
189
+ includes: (arr, val) => (Array.isArray(arr) ? arr.includes(val) : false),
190
+ json: (obj) => JSON.stringify(obj, null, 2),
191
+ jsonStringify: (obj) => JSON.stringify(obj),
192
+ getInitials: (username) => {
193
+ if (!username) return '?';
194
+ const parts = username.split(/[._]/);
195
+ return parts.map(p => p.charAt(0)).join('').toUpperCase().substring(0, 2);
196
+ },
197
+ };