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 +1 -1
- package/src/app.js +3 -5
- package/src/controllers/api.controller.js +3 -1
- package/src/controllers/dashboard.controller.js +11 -10
- package/src/controllers/view.controller.js +7 -3
- package/src/routes/api.routes.js +1 -1
- package/src/routes/view.routes.js +3 -3
- package/src/services/s3.service.js +10 -3
- package/src/utils/helpers.js +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkbucket",
|
|
3
|
-
"version": "3.
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
12
|
-
|
|
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,
|
|
27
|
+
return renderPage(req, res, bucketfilePath, isPortal, {
|
|
30
28
|
page: "Admin Bucket",
|
|
31
|
-
|
|
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,
|
|
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(
|
|
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
|
|
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
|
|
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('/')) {
|
package/src/routes/api.routes.js
CHANGED
|
@@ -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
|
|
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
|
|
15
|
+
router.get('/player/*key', sessRole('SuperAdmin'), playerPage);
|
|
16
16
|
|
|
17
17
|
// Inline file view
|
|
18
|
-
router.get('/view
|
|
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
|
|
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
|
-
|
|
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') {
|
package/src/utils/helpers.js
CHANGED
|
@@ -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
|
+
};
|