mbkbucket 1.2.0 → 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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Type definitions for mbkbucket 1.1.0
1
+ // Type definitions for mbkbucket 1.2.1
2
2
  // Project: https://github.com/MIbnEKhalid/mbkbucket
3
3
  // Definitions by: Muhammad Bin Khalid <https://github.com/MIbnEKhalid">
4
4
 
package/index.js CHANGED
@@ -40,8 +40,22 @@ const generalLimiter = rateLimit({
40
40
  server.use(express.json());
41
41
  server.use(express.urlencoded({ extended: true }));
42
42
 
43
- // Serve static assets from /public (CSS/JS/images)
44
- server.use(express.static(path.join(__dirname, 'public'), { maxAge: '365d', etag: true }));
43
+ // Serve only specific static assets (limit exposure): CSS and JS used by the dashboard
44
+ // This replaces serving the entire /public directory.
45
+ const staticMaxAgeSeconds = 90 * 24 * 60 * 60; // 90 days
46
+ server.get('/mbkbucket/bucketadmin.css', (req, res) => {
47
+ res.set('Cache-Control', `public, max-age=${staticMaxAgeSeconds}, immutable`);
48
+ res.sendFile(path.join(__dirname, 'public', 'bucketadmin.css'), (err) => {
49
+ if (err) res.status(err.status || 404).end();
50
+ });
51
+ });
52
+
53
+ server.get('/mbkbucket/bucketadmin.js', (req, res) => {
54
+ res.set('Cache-Control', `public, max-age=${staticMaxAgeSeconds}, immutable`);
55
+ res.sendFile(path.join(__dirname, 'public', 'bucketadmin.js'), (err) => {
56
+ if (err) res.status(err.status || 404).end();
57
+ });
58
+ });
45
59
 
46
60
  // Configure Handlebars (single setup)
47
61
  server.engine("handlebars", engine({
package/lib/bucket.js CHANGED
@@ -18,7 +18,7 @@ fs.mkdirSync(CHUNKS_DIR, { recursive: true });
18
18
  // Key/prefix helpers are provided by `s3.js`: `ensurePrefix`, `ensureKeyHasAppPrefix`, `getAppName`.
19
19
 
20
20
  router.get('/mbkbucket', validateSessionAndRole('SuperAdmin'), async (req, res) => {
21
- renderPage(req, res, "bucket.handlebars", false, { page: "Admin Bucket", bucketvar: mbkautheVar?.bucket, message: req.query.message, error: req.query.error });
21
+ renderPage(req, res, "bucket.handlebars", false, { page: "Admin Bucket", bucketvar: mbkautheVar?.bucket, APP_NAME: mbkautheVar?.APP_NAME, message: req.query.message, error: req.query.error });
22
22
  });
23
23
 
24
24
  router.get(['/mbkbucket/info', "/mbkbucket/i"], validateSessionAndRole('SuperAdmin'), async (req, res) => {
@@ -28,14 +28,12 @@ router.get(['/mbkbucket/info', "/mbkbucket/i"], validateSessionAndRole('SuperAdm
28
28
  } catch (err) {
29
29
  console.error("[mbkauthe] Error fetching package-lock.json:", err);
30
30
  }
31
- renderPage(req, res, "info.handlebars", false, { page: "MBK Bucket Info", appVersion: appVersion, latestVersion, APP_NAME: mbkautheVar.APP_NAME });
31
+ renderPage(req, res, "info.handlebars", false, { page: "MBK Bucket Info", appVersion, latestVersion, APP_NAME: mbkautheVar?.APP_NAME });
32
32
  });
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,56 +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
- const result = await listfiles(effectivePrefix);
49
- let files = result.Contents || [];
53
+ let files = [];
54
+ let folders = []; // For CommonPrefixes (subfolders)
55
+ let nextContinuationToken = null;
50
56
  let usedPrefix = effectivePrefix;
51
57
  let legacyFallback = false;
52
58
 
53
- // If no files found under the app prefixed path, try root as a legacy fallback
54
- if (!files.length) {
55
- try {
56
- const rootResult = await listfiles('');
57
- if ((rootResult.Contents || []).length) {
58
- files = rootResult.Contents || [];
59
- usedPrefix = '';
60
- legacyFallback = true;
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 += '/';
69
+ }
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);
92
+ }
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);
61
107
  }
62
- } catch (fallbackErr) {
63
- // ignore fallback errors and continue with empty files
64
- console.warn('Fallback root listing failed:', fallbackErr.message);
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
+ });
65
116
  }
66
117
  }
67
118
 
68
- // Apply search filter if provided
69
- if (search.trim()) {
70
- const searchLower = search.toLowerCase();
71
- files = files.filter(file => {
72
- const fileName = file.Key.split('/').pop();
73
- return fileName.toLowerCase().includes(searchLower);
74
- });
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
+ });
75
158
  }
76
159
 
77
- // Pagination logic
78
- const totalFiles = files.length;
79
- const totalPages = Math.max(1, Math.ceil(totalFiles / pageSize));
80
- const currentPage = Math.max(1, parseInt(page, 10) || 1);
81
- const startIndex = (currentPage - 1) * pageSize;
82
- const paginatedFiles = files.slice(startIndex, startIndex + pageSize);
83
-
84
- res.json({
85
- success: true,
86
- files: paginatedFiles,
87
- prefix: effectivePrefix,
88
- search: search,
89
- currentPage: currentPage,
90
- totalPages,
91
- hasNextPage: currentPage < totalPages,
92
- hasPrevPage: currentPage > 1,
93
- nextPage: currentPage + 1,
94
- prevPage: Math.max(1, currentPage - 1),
95
- totalFiles
96
- });
97
160
  } catch (error) {
98
161
  console.error("Error listing files:", error);
99
162
  res.status(500).json({
@@ -120,7 +183,7 @@ router.post('/mbkbucket/upload', validateSessionAndRole('SuperAdmin'), upload.si
120
183
  return res.status(400).json({ success: false, error: e.message });
121
184
  }
122
185
 
123
- const key = `${effectivePrefix}/${req.file.originalname}`;
186
+ const key = `${effectivePrefix.replace(/\/+$/, '')}/${req.file.originalname}`;
124
187
 
125
188
  // Check if file already exists
126
189
  const exists = await fileExists(key);
@@ -188,7 +251,7 @@ router.post('/mbkbucket/upload-complete', validateSessionAndRole('SuperAdmin'),
188
251
  // Upload to bucket
189
252
  let effectivePrefix;
190
253
  try { effectivePrefix = ensurePrefix(prefix); } catch (e) { return res.status(400).json({ success: false, error: e.message }); }
191
- const key = `${effectivePrefix}/${fileName}`;
254
+ const key = `${effectivePrefix.replace(/\/+$/, '')}/${fileName}`;
192
255
 
193
256
  // Check existence
194
257
  const exists = await fileExists(key);
@@ -218,7 +281,7 @@ router.post('/mbkbucket/create-folder', validateSessionAndRole('SuperAdmin'), as
218
281
  let effectivePrefix;
219
282
  try { effectivePrefix = ensurePrefix(prefix); } catch (e) { return res.status(400).json({ success: false, error: e.message }); }
220
283
 
221
- const folderKey = `${effectivePrefix}/${folderName}/`;
284
+ const folderKey = `${effectivePrefix.replace(/\/+$/, '')}/${folderName}/`;
222
285
 
223
286
  // Check if exists
224
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}\/)+`);
@@ -216,13 +221,14 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
216
221
 
217
222
  // Download file with enhanced features
218
223
  export async function downloadFile(key, options = {}) {
224
+ let keyToDownload;
219
225
  try {
220
226
  if (!key) {
221
227
  throw new Error('Key is required');
222
228
  }
223
229
 
224
230
  // Ensure key is app-prefixed
225
- const keyToDownload = ensureKeyHasAppPrefix(key);
231
+ keyToDownload = ensureKeyHasAppPrefix(key);
226
232
 
227
233
  const {
228
234
  range = null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkbucket",
3
- "version": "1.2.0",
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",
@@ -19,7 +19,7 @@
19
19
  "express": "^4.22.1",
20
20
  "express-handlebars": "^8.0.3",
21
21
  "express-rate-limit": "^7.0.1",
22
- "mbkauthe": "^4.3.0",
22
+ "mbkauthe": "latest",
23
23
  "multer": "^2.0.2",
24
24
  "pg": "^8.16.3"
25
25
  },