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 +1 -1
- package/index.js +16 -2
- package/lib/bucket.js +111 -48
- package/lib/s3.js +9 -3
- package/package.json +2 -2
- package/public/{css/bucketadmin.css → bucketadmin.css} +271 -51
- package/public/{js/bucketadmincontent.js → bucketadmin.js} +359 -127
- package/views/bucket.handlebars +2 -2
- package/views/bucketadmincontent.handlebars +44 -58
package/index.d.ts
CHANGED
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
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
let
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
//
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
22
|
+
"mbkauthe": "latest",
|
|
23
23
|
"multer": "^2.0.2",
|
|
24
24
|
"pg": "^8.16.3"
|
|
25
25
|
},
|