mbkbucket 1.5.0 → 2.0.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/.env.example +2 -2
- package/README.md +72 -0
- package/index.d.ts +87 -49
- package/index.js +15 -13
- package/lib/config/index.js +9 -89
- package/lib/config/validation.js +86 -0
- package/lib/routes/api.js +139 -45
- package/lib/routes/index.js +70 -1
- package/lib/routes/view.js +71 -18
- package/lib/s3.js +64 -32
- package/package.json +2 -2
- package/public/bucketadmin.css +447 -17
- package/public/bucketadmin.js +272 -82
- package/test/config-validation.test.js +116 -0
- package/views/bucket.handlebars +66 -31
- package/views/bucketadmincontent.handlebars +30 -9
- package/views/bucketportal.handlebars +1 -0
package/lib/routes/view.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { validateSessionAndRole } from "mbkauthe";
|
|
3
|
-
import { downloadFile, getFileMetadata, ensureKeyHasAppPrefix, generateSignedUrl } from "../s3.js";
|
|
3
|
+
import { downloadFile, getFileMetadata, ensureKeyHasAppPrefix, generateSignedUrl, resolveBucketName } from "../s3.js";
|
|
4
4
|
import { mbkbucketVar } from "../config/index.js";
|
|
5
5
|
import rateLimit from "express-rate-limit";
|
|
6
6
|
|
|
@@ -63,9 +63,44 @@ function pviewSecurity(req, res, next) {
|
|
|
63
63
|
|
|
64
64
|
const router = express.Router();
|
|
65
65
|
|
|
66
|
+
function classifyViewError(error, fallbackMessage = 'Request failed') {
|
|
67
|
+
const message = String(error?.message || fallbackMessage);
|
|
68
|
+
const lower = message.toLowerCase();
|
|
69
|
+
|
|
70
|
+
if (lower.includes('no bucket selected') || lower.includes('not found in bucketconnection')) {
|
|
71
|
+
return { status: 400, code: 'INVALID_BUCKET', message };
|
|
72
|
+
}
|
|
73
|
+
if (lower.includes('access denied')) {
|
|
74
|
+
return { status: 403, code: 'ACCESS_DENIED', message };
|
|
75
|
+
}
|
|
76
|
+
if (lower.includes('not found')) {
|
|
77
|
+
return { status: 404, code: 'NOT_FOUND', message };
|
|
78
|
+
}
|
|
79
|
+
if (lower.includes('invalid') || lower.includes('required')) {
|
|
80
|
+
return { status: 400, code: 'VALIDATION_ERROR', message };
|
|
81
|
+
}
|
|
82
|
+
return { status: 500, code: 'INTERNAL_ERROR', message };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sendViewError(res, error, fallbackMessage = 'Request failed') {
|
|
86
|
+
const mapped = classifyViewError(error, fallbackMessage);
|
|
87
|
+
return res.status(mapped.status).json({ code: mapped.code, message: mapped.message });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Central bucket guard for view routes except public view (forced default bucket).
|
|
91
|
+
router.use((req, res, next) => {
|
|
92
|
+
if (req.path.startsWith('/p_view/')) return next();
|
|
93
|
+
if (req.bucketResolveError) return sendViewError(res, req.bucketResolveError, 'Invalid bucket selection');
|
|
94
|
+
if (!req.activeBucket) {
|
|
95
|
+
return sendViewError(res, new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.'));
|
|
96
|
+
}
|
|
97
|
+
next();
|
|
98
|
+
});
|
|
99
|
+
|
|
66
100
|
// New: lightweight player page for robust playback in browsers
|
|
67
101
|
router.get('/player/:key(*)', validateSessionAndRole('SuperAdmin'), async (req, res) => {
|
|
68
102
|
try {
|
|
103
|
+
const bucketName = req.activeBucket;
|
|
69
104
|
const key = req.params.key;
|
|
70
105
|
let keyToUse;
|
|
71
106
|
try {
|
|
@@ -76,6 +111,7 @@ router.get('/player/:key(*)', validateSessionAndRole('SuperAdmin'), async (req,
|
|
|
76
111
|
const encoded = encodeURIComponent(keyToUse);
|
|
77
112
|
const fileName = keyToUse.split('/').pop();
|
|
78
113
|
|
|
114
|
+
const bucketQuery = `?bucket=${encodeURIComponent(bucketName)}`;
|
|
79
115
|
const html = `<!doctype html>
|
|
80
116
|
<html lang="en">
|
|
81
117
|
<head>
|
|
@@ -93,7 +129,7 @@ router.get('/player/:key(*)', validateSessionAndRole('SuperAdmin'), async (req,
|
|
|
93
129
|
<body>
|
|
94
130
|
<div class="file-name">${fileName}</div>
|
|
95
131
|
<div class="player-container">
|
|
96
|
-
<video controls preload="metadata" src="/mbkbucket/view/${encoded}">
|
|
132
|
+
<video controls preload="metadata" src="/mbkbucket/view/${encoded}${bucketQuery}">
|
|
97
133
|
Your browser does not support the video tag.
|
|
98
134
|
</video>
|
|
99
135
|
</div>
|
|
@@ -160,7 +196,28 @@ const MIME_TYPES = {
|
|
|
160
196
|
// Options:
|
|
161
197
|
// noindex {boolean} – adds X-Robots-Tag: noindex, nofollow (default false)
|
|
162
198
|
// ---------------------------------------------------------------------------
|
|
163
|
-
|
|
199
|
+
function isSensitiveTextType(contentType = '') {
|
|
200
|
+
const type = String(contentType || '').toLowerCase();
|
|
201
|
+
return type.startsWith('text/')
|
|
202
|
+
|| type.includes('json')
|
|
203
|
+
|| type.includes('xml')
|
|
204
|
+
|| type.includes('javascript')
|
|
205
|
+
|| type.includes('typescript');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildCacheControl(contentType, { isStaticAsset = false, supportsRanges = false, publicCache = false } = {}) {
|
|
209
|
+
if (publicCache) {
|
|
210
|
+
if (isStaticAsset) return 'public, max-age=3600, immutable';
|
|
211
|
+
if (supportsRanges) return 'public, max-age=86400, must-revalidate';
|
|
212
|
+
return 'public, max-age=300, must-revalidate';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isSensitiveTextType(contentType)) return 'private, no-store';
|
|
216
|
+
if (supportsRanges) return 'private, max-age=1800, must-revalidate';
|
|
217
|
+
return 'private, max-age=300, must-revalidate';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName, publicCache = false } = {}) {
|
|
164
221
|
const fileName = keyToUse.split('/').pop();
|
|
165
222
|
const fileExtension = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : '';
|
|
166
223
|
|
|
@@ -193,7 +250,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true } = {}) {
|
|
|
193
250
|
console.log(`[mbkbucket] Processing range request: ${rangeHeader} for ${fileName}`);
|
|
194
251
|
let meta;
|
|
195
252
|
try {
|
|
196
|
-
meta = await getFileMetadata(keyToUse);
|
|
253
|
+
meta = await getFileMetadata(keyToUse, bucketName);
|
|
197
254
|
} catch (err) {
|
|
198
255
|
console.error('[mbkbucket] serveFileInline metadata error:', err);
|
|
199
256
|
return res.status(500).json({ message: 'Failed to fetch file metadata', error: err.message });
|
|
@@ -249,7 +306,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true } = {}) {
|
|
|
249
306
|
const chunksize = end - start + 1;
|
|
250
307
|
|
|
251
308
|
try {
|
|
252
|
-
const rangeResult = await downloadFile(keyToUse, { range: `bytes=${start}-${end}
|
|
309
|
+
const rangeResult = await downloadFile(keyToUse, { range: `bytes=${start}-${end}`, bucketName });
|
|
253
310
|
|
|
254
311
|
if (typeof rangeResult.ContentLength === 'number' && rangeResult.ContentLength !== chunksize) {
|
|
255
312
|
console.warn(`Range size mismatch for ${keyToUse}: expected=${chunksize} got=${rangeResult.ContentLength}`);
|
|
@@ -261,7 +318,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true } = {}) {
|
|
|
261
318
|
res.setHeader('Content-Range', `bytes ${start}-${end}/${total}`);
|
|
262
319
|
res.setHeader('Content-Length', chunksize);
|
|
263
320
|
res.setHeader('Content-Type', contentType);
|
|
264
|
-
res.setHeader('Cache-Control',
|
|
321
|
+
res.setHeader('Cache-Control', buildCacheControl(contentType, { supportsRanges: true, publicCache }));
|
|
265
322
|
res.setHeader('ETag', etag);
|
|
266
323
|
res.setHeader('Accept-Ranges', 'bytes');
|
|
267
324
|
if (noindex) res.setHeader('X-Robots-Tag', 'noindex, nofollow');
|
|
@@ -308,7 +365,8 @@ async function serveFileInline(req, res, keyToUse, { noindex = true } = {}) {
|
|
|
308
365
|
// ---------------------------------------------------------------------------
|
|
309
366
|
const downloadOptions = {
|
|
310
367
|
...(ifNoneMatch && { ifNoneMatch }),
|
|
311
|
-
...(ifModifiedSince && { ifModifiedSince: new Date(ifModifiedSince) })
|
|
368
|
+
...(ifModifiedSince && { ifModifiedSince: new Date(ifModifiedSince) }),
|
|
369
|
+
bucketName
|
|
312
370
|
};
|
|
313
371
|
|
|
314
372
|
if (isPdf) {
|
|
@@ -346,14 +404,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true } = {}) {
|
|
|
346
404
|
}
|
|
347
405
|
if (supportsRanges) res.setHeader('Accept-Ranges', 'bytes');
|
|
348
406
|
|
|
349
|
-
|
|
350
|
-
res.setHeader('Cache-Control', 'public, max-age=3600, immutable');
|
|
351
|
-
} else if (supportsRanges) {
|
|
352
|
-
// For video, audio, and PDF - enable caching with revalidation
|
|
353
|
-
res.setHeader('Cache-Control', 'public, max-age=86400, must-revalidate');
|
|
354
|
-
} else {
|
|
355
|
-
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
356
|
-
}
|
|
407
|
+
res.setHeader('Cache-Control', buildCacheControl(contentType, { isStaticAsset, supportsRanges, publicCache }));
|
|
357
408
|
|
|
358
409
|
res.status(200);
|
|
359
410
|
|
|
@@ -393,6 +444,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true } = {}) {
|
|
|
393
444
|
|
|
394
445
|
router.get('/view/:key(*)', validateSessionAndRole('SuperAdmin'), async (req, res) => {
|
|
395
446
|
try {
|
|
447
|
+
const bucketName = req.activeBucket;
|
|
396
448
|
let keyToUse;
|
|
397
449
|
try {
|
|
398
450
|
keyToUse = ensureKeyHasAppPrefix(req.params.key);
|
|
@@ -400,7 +452,7 @@ router.get('/view/:key(*)', validateSessionAndRole('SuperAdmin'), async (req, re
|
|
|
400
452
|
return res.status(400).json({ message: e.message });
|
|
401
453
|
}
|
|
402
454
|
|
|
403
|
-
await serveFileInline(req, res, keyToUse, { noindex: true });
|
|
455
|
+
await serveFileInline(req, res, keyToUse, { noindex: true, bucketName, publicCache: false });
|
|
404
456
|
|
|
405
457
|
} catch (error) {
|
|
406
458
|
console.error("Error viewing file:", error);
|
|
@@ -417,6 +469,7 @@ router.get('/view/:key(*)', validateSessionAndRole('SuperAdmin'), async (req, re
|
|
|
417
469
|
if (mbkbucketVar?.publiView_enabled) {
|
|
418
470
|
router.get('/p_view/:key(*)', pviewRateLimit, pviewSecurity, async (req, res) => {
|
|
419
471
|
try {
|
|
472
|
+
const bucketName = resolveBucketName();
|
|
420
473
|
const key = req.params.key;
|
|
421
474
|
|
|
422
475
|
// No directory indexing — reject folder-like or empty keys
|
|
@@ -438,7 +491,7 @@ if (mbkbucketVar?.publiView_enabled) {
|
|
|
438
491
|
if (!p_view_inline) {
|
|
439
492
|
// Signed URL mode — redirect to a pre-signed S3 URL (1 hour expiry)
|
|
440
493
|
try {
|
|
441
|
-
const signed = await generateSignedUrl(keyToUse, 'getObject', 3600);
|
|
494
|
+
const signed = await generateSignedUrl(keyToUse, 'getObject', 3600, bucketName);
|
|
442
495
|
return res.redirect(302, signed.url);
|
|
443
496
|
} catch (err) {
|
|
444
497
|
console.error('[mbkbucket] p_view signed URL error:', err);
|
|
@@ -447,7 +500,7 @@ if (mbkbucketVar?.publiView_enabled) {
|
|
|
447
500
|
}
|
|
448
501
|
|
|
449
502
|
// Direct serve mode — stream file inline (with noindex header)
|
|
450
|
-
await serveFileInline(req, res, keyToUse, { noindex: true });
|
|
503
|
+
await serveFileInline(req, res, keyToUse, { noindex: true, bucketName, publicCache: true });
|
|
451
504
|
} catch (error) {
|
|
452
505
|
console.error('[mbkbucket] p_view error:', error);
|
|
453
506
|
res.status(500).json({ message: 'Internal server error', error: error.message });
|
package/lib/s3.js
CHANGED
|
@@ -19,6 +19,10 @@ function parseJsonEnv(envVar, fallback = {}) {
|
|
|
19
19
|
// Parse BucketConnection JSON that contains all bucket configurations
|
|
20
20
|
const allBucketConfigs = parseJsonEnv(process.env.BucketConnection, {});
|
|
21
21
|
|
|
22
|
+
export function getAvailableBucketNames() {
|
|
23
|
+
return Object.keys(allBucketConfigs || {});
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
// Helper to get default bucket name
|
|
23
27
|
function getDefaultBucketName() {
|
|
24
28
|
// First priority: use mbkautheVar.bucket if configured
|
|
@@ -36,6 +40,26 @@ function getDefaultBucketName() {
|
|
|
36
40
|
return null;
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
export function resolveBucketName(bucketName) {
|
|
44
|
+
const candidate = (bucketName === undefined || bucketName === null)
|
|
45
|
+
? getDefaultBucketName()
|
|
46
|
+
: String(bucketName).trim();
|
|
47
|
+
|
|
48
|
+
if (!candidate) {
|
|
49
|
+
throw new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!allBucketConfigs || Object.keys(allBucketConfigs).length === 0) {
|
|
53
|
+
throw new Error('BucketConnection environment variable is not set or empty. Please configure it with your bucket connections.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!allBucketConfigs[candidate]) {
|
|
57
|
+
throw new Error(`Bucket '${candidate}' not found in BucketConnection. Available buckets: ${Object.keys(allBucketConfigs).join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
// (bucketClient singleton exported below, after cache setup)
|
|
40
64
|
|
|
41
65
|
// Helper: get configured APP_NAME; prefer mbkautheVar, then env var
|
|
@@ -44,9 +68,12 @@ export function getAppName() {
|
|
|
44
68
|
return (typeof app === 'string' && app.trim()) ? app.trim() : '';
|
|
45
69
|
}
|
|
46
70
|
|
|
71
|
+
function isRootModeApp(appName) {
|
|
72
|
+
return String(appName || '').trim().toLowerCase() === 'portal';
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
export function ensureKeyHasAppPrefix(key = '') {
|
|
48
76
|
const app = getAppName();
|
|
49
|
-
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
50
77
|
if (!key) throw new Error('Key is required');
|
|
51
78
|
|
|
52
79
|
// Security: Detect path traversal attempts
|
|
@@ -60,6 +87,14 @@ export function ensureKeyHasAppPrefix(key = '') {
|
|
|
60
87
|
}
|
|
61
88
|
|
|
62
89
|
let cleaned = String(key).replace(/^\/+/, '');
|
|
90
|
+
|
|
91
|
+
// Special case: APP_NAME=portal means operate from bucket root.
|
|
92
|
+
if (isRootModeApp(app)) {
|
|
93
|
+
return cleaned;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
97
|
+
|
|
63
98
|
// Collapse repeated app prefixes (app/app/... -> app/...)
|
|
64
99
|
const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
|
|
65
100
|
if (!cleaned.startsWith(`${app}/`)) {
|
|
@@ -73,7 +108,6 @@ export function ensureKeyHasAppPrefix(key = '') {
|
|
|
73
108
|
|
|
74
109
|
export function ensurePrefix(prefix = '') {
|
|
75
110
|
const app = getAppName();
|
|
76
|
-
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
77
111
|
|
|
78
112
|
// Security: Detect path traversal attempts
|
|
79
113
|
if (prefix && (prefix.includes('../') || prefix.includes('..\\') || /\.\.[\\/]/.test(prefix))) {
|
|
@@ -87,6 +121,13 @@ export function ensurePrefix(prefix = '') {
|
|
|
87
121
|
|
|
88
122
|
// Only strip LEADING slashes. Preserve trailing slashes for directory listing.
|
|
89
123
|
let p = String(prefix || '').replace(/^\/+/, '');
|
|
124
|
+
|
|
125
|
+
// Special case: APP_NAME=portal means operate from bucket root.
|
|
126
|
+
if (isRootModeApp(app)) {
|
|
127
|
+
return p;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
90
131
|
|
|
91
132
|
if (!p) return `${app}`;
|
|
92
133
|
|
|
@@ -110,13 +151,7 @@ const _clientCache = new Map();
|
|
|
110
151
|
const _configCache = new Map();
|
|
111
152
|
|
|
112
153
|
export function getBucketConfig(bucketName) {
|
|
113
|
-
|
|
114
|
-
if (!bucketName) {
|
|
115
|
-
bucketName = getDefaultBucketName();
|
|
116
|
-
if (!bucketName) {
|
|
117
|
-
throw new Error('No bucket name provided and no default bucket configured. Please set mbkautheVar.bucket or configure BucketConnection.');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
154
|
+
bucketName = resolveBucketName(bucketName);
|
|
120
155
|
|
|
121
156
|
const cacheKey = bucketName;
|
|
122
157
|
if (_configCache.has(cacheKey)) return _configCache.get(cacheKey);
|
|
@@ -144,10 +179,7 @@ export function getBucketConfig(bucketName) {
|
|
|
144
179
|
|
|
145
180
|
// Function to get S3 client for specific bucket — returns cached singleton
|
|
146
181
|
export function getBucketClient(bucketName) {
|
|
147
|
-
|
|
148
|
-
if (!bucketName) {
|
|
149
|
-
bucketName = getDefaultBucketName();
|
|
150
|
-
}
|
|
182
|
+
bucketName = resolveBucketName(bucketName);
|
|
151
183
|
|
|
152
184
|
if (_clientCache.has(bucketName)) return _clientCache.get(bucketName);
|
|
153
185
|
|
|
@@ -619,10 +651,10 @@ export { checkHealth };
|
|
|
619
651
|
/**
|
|
620
652
|
* Initiate a multipart upload. Returns the S3 UploadId.
|
|
621
653
|
*/
|
|
622
|
-
export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}) {
|
|
654
|
+
export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}, bucketName) {
|
|
623
655
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
624
|
-
const cfg = getBucketConfig();
|
|
625
|
-
const client = getBucketClient();
|
|
656
|
+
const cfg = getBucketConfig(bucketName);
|
|
657
|
+
const client = getBucketClient(bucketName);
|
|
626
658
|
const command = new CreateMultipartUploadCommand({
|
|
627
659
|
Bucket: cfg.BUCKET_NAME,
|
|
628
660
|
Key: keyToUse,
|
|
@@ -641,10 +673,10 @@ export async function createMultipartUpload(key, contentType = 'application/octe
|
|
|
641
673
|
* Upload a single part. partNumber is 1-based.
|
|
642
674
|
* Returns the ETag needed for CompleteMultipartUpload.
|
|
643
675
|
*/
|
|
644
|
-
export async function uploadPart(key, uploadId, partNumber, buffer) {
|
|
676
|
+
export async function uploadPart(key, uploadId, partNumber, buffer, bucketName) {
|
|
645
677
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
646
|
-
const cfg = getBucketConfig();
|
|
647
|
-
const client = getBucketClient();
|
|
678
|
+
const cfg = getBucketConfig(bucketName);
|
|
679
|
+
const client = getBucketClient(bucketName);
|
|
648
680
|
const command = new UploadPartCommand({
|
|
649
681
|
Bucket: cfg.BUCKET_NAME,
|
|
650
682
|
Key: keyToUse,
|
|
@@ -661,10 +693,10 @@ export async function uploadPart(key, uploadId, partNumber, buffer) {
|
|
|
661
693
|
* Complete a multipart upload.
|
|
662
694
|
* parts must be an array of { partNumber, ETag } sorted ascending by partNumber.
|
|
663
695
|
*/
|
|
664
|
-
export async function completeMultipartUpload(key, uploadId, parts) {
|
|
696
|
+
export async function completeMultipartUpload(key, uploadId, parts, bucketName) {
|
|
665
697
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
666
|
-
const cfg = getBucketConfig();
|
|
667
|
-
const client = getBucketClient();
|
|
698
|
+
const cfg = getBucketConfig(bucketName);
|
|
699
|
+
const client = getBucketClient(bucketName);
|
|
668
700
|
const sorted = [...parts].sort((a, b) => a.partNumber - b.partNumber);
|
|
669
701
|
const command = new CompleteMultipartUploadCommand({
|
|
670
702
|
Bucket: cfg.BUCKET_NAME,
|
|
@@ -681,10 +713,10 @@ export async function completeMultipartUpload(key, uploadId, parts) {
|
|
|
681
713
|
/**
|
|
682
714
|
* Abort an in-progress multipart upload, freeing all stored parts.
|
|
683
715
|
*/
|
|
684
|
-
export async function abortMultipartUpload(key, uploadId) {
|
|
716
|
+
export async function abortMultipartUpload(key, uploadId, bucketName) {
|
|
685
717
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
686
|
-
const cfg = getBucketConfig();
|
|
687
|
-
const client = getBucketClient();
|
|
718
|
+
const cfg = getBucketConfig(bucketName);
|
|
719
|
+
const client = getBucketClient(bucketName);
|
|
688
720
|
const command = new AbortMultipartUploadCommand({
|
|
689
721
|
Bucket: cfg.BUCKET_NAME,
|
|
690
722
|
Key: keyToUse,
|
|
@@ -695,26 +727,26 @@ export async function abortMultipartUpload(key, uploadId) {
|
|
|
695
727
|
}
|
|
696
728
|
|
|
697
729
|
// Generate signed URL for temporary access
|
|
698
|
-
export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600) {
|
|
730
|
+
export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, bucketName) {
|
|
699
731
|
try {
|
|
700
732
|
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
701
733
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
702
734
|
|
|
703
735
|
let command;
|
|
704
|
-
const cfg = getBucketConfig();
|
|
705
|
-
const
|
|
736
|
+
const cfg = getBucketConfig(bucketName);
|
|
737
|
+
const resolvedBucketName = cfg.BUCKET_NAME;
|
|
706
738
|
switch (operation) {
|
|
707
739
|
case 'getObject':
|
|
708
|
-
command = new GetObjectCommand({ Bucket:
|
|
740
|
+
command = new GetObjectCommand({ Bucket: resolvedBucketName, Key: keyToUse });
|
|
709
741
|
break;
|
|
710
742
|
case 'putObject':
|
|
711
|
-
command = new PutObjectCommand({ Bucket:
|
|
743
|
+
command = new PutObjectCommand({ Bucket: resolvedBucketName, Key: keyToUse });
|
|
712
744
|
break;
|
|
713
745
|
default:
|
|
714
746
|
throw new Error(`Unsupported operation: ${operation}`);
|
|
715
747
|
}
|
|
716
748
|
|
|
717
|
-
const signedUrl = await getSignedUrl(
|
|
749
|
+
const signedUrl = await getSignedUrl(getBucketClient(bucketName), command, { expiresIn });
|
|
718
750
|
|
|
719
751
|
return {
|
|
720
752
|
url: signedUrl,
|
|
@@ -742,7 +774,7 @@ export async function generateSignedUrl(key, operation = 'getObject', expiresIn
|
|
|
742
774
|
*/
|
|
743
775
|
export async function listIncompleteMultipartUploads(prefix = '', bucketName) {
|
|
744
776
|
try {
|
|
745
|
-
const effectivePrefix =
|
|
777
|
+
const effectivePrefix = ensurePrefix(prefix || '');
|
|
746
778
|
const cfg = getBucketConfig(bucketName);
|
|
747
779
|
const client = getBucketClient(bucketName);
|
|
748
780
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkbucket",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Flexible S3/R2 bucket management library and Express router for mbktech.org applications, providing upload, download, listing, and multipart utilities with built-in configuration validation and health checks",
|
|
5
5
|
"author": "Muhammad Bin Khalid",
|
|
6
6
|
"homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"types": "index.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "cross-env mbkbucket_test=dev nodemon index.js",
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "node --test"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@aws-sdk/client-s3": "^3.968.0",
|