mbkbucket 1.5.1 → 2.1.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 +20 -20
- package/lib/config/index.js +34 -98
- package/lib/config/validation.js +89 -0
- package/lib/debug.js +12 -0
- package/lib/routes/api.js +147 -88
- package/lib/routes/index.js +70 -1
- package/lib/routes/view.js +108 -35
- package/lib/s3.js +102 -57
- package/package.json +6 -5
- package/public/bucketadmin.css +571 -18
- package/public/bucketadmin.js +431 -150
- package/test/config-validation.test.js +116 -0
- package/views/bucket.handlebars +66 -31
- package/views/bucketadmincontent.handlebars +40 -19
- package/views/bucketportal.handlebars +1 -0
package/lib/s3.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, ListMultipartUploadsCommand } from '@aws-sdk/client-s3';
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
3
|
import { mbkautheVar } from "mbkauthe";
|
|
4
|
+
import { createLogger } from "./debug.js";
|
|
4
5
|
|
|
5
6
|
dotenv.config();
|
|
7
|
+
const debugS3 = createLogger('s3');
|
|
6
8
|
|
|
7
9
|
function parseJsonEnv(envVar, fallback = {}) {
|
|
8
10
|
if (!envVar) return fallback;
|
|
@@ -18,6 +20,16 @@ function parseJsonEnv(envVar, fallback = {}) {
|
|
|
18
20
|
|
|
19
21
|
// Parse BucketConnection JSON that contains all bucket configurations
|
|
20
22
|
const allBucketConfigs = parseJsonEnv(process.env.BucketConnection, {});
|
|
23
|
+
const availableBucketNames = Object.freeze(Object.keys(allBucketConfigs || {}));
|
|
24
|
+
const hasBucketConfigs = availableBucketNames.length > 0;
|
|
25
|
+
|
|
26
|
+
function escapeRegExp(value) {
|
|
27
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getAvailableBucketNames() {
|
|
31
|
+
return availableBucketNames;
|
|
32
|
+
}
|
|
21
33
|
|
|
22
34
|
// Helper to get default bucket name
|
|
23
35
|
function getDefaultBucketName() {
|
|
@@ -27,15 +39,34 @@ function getDefaultBucketName() {
|
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
// Second priority: use first bucket in BucketConnection
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return buckets[0];
|
|
42
|
+
if (availableBucketNames.length > 0) {
|
|
43
|
+
return availableBucketNames[0];
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
// Fallback: return null (will cause error in getBucketConfig)
|
|
36
47
|
return null;
|
|
37
48
|
}
|
|
38
49
|
|
|
50
|
+
export function resolveBucketName(bucketName) {
|
|
51
|
+
const candidate = (bucketName === undefined || bucketName === null)
|
|
52
|
+
? getDefaultBucketName()
|
|
53
|
+
: String(bucketName).trim();
|
|
54
|
+
|
|
55
|
+
if (!candidate) {
|
|
56
|
+
throw new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!hasBucketConfigs) {
|
|
60
|
+
throw new Error('BucketConnection environment variable is not set or empty. Please configure it with your bucket connections.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!allBucketConfigs[candidate]) {
|
|
64
|
+
throw new Error(`Bucket '${candidate}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
|
|
39
70
|
// (bucketClient singleton exported below, after cache setup)
|
|
40
71
|
|
|
41
72
|
// Helper: get configured APP_NAME; prefer mbkautheVar, then env var
|
|
@@ -44,9 +75,12 @@ export function getAppName() {
|
|
|
44
75
|
return (typeof app === 'string' && app.trim()) ? app.trim() : '';
|
|
45
76
|
}
|
|
46
77
|
|
|
78
|
+
function isRootModeApp(appName) {
|
|
79
|
+
return String(appName || '').trim().toLowerCase() === 'portal';
|
|
80
|
+
}
|
|
81
|
+
|
|
47
82
|
export function ensureKeyHasAppPrefix(key = '') {
|
|
48
83
|
const app = getAppName();
|
|
49
|
-
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
50
84
|
if (!key) throw new Error('Key is required');
|
|
51
85
|
|
|
52
86
|
// Security: Detect path traversal attempts
|
|
@@ -60,8 +94,16 @@ export function ensureKeyHasAppPrefix(key = '') {
|
|
|
60
94
|
}
|
|
61
95
|
|
|
62
96
|
let cleaned = String(key).replace(/^\/+/, '');
|
|
97
|
+
|
|
98
|
+
// Special case: APP_NAME=portal means operate from bucket root.
|
|
99
|
+
if (isRootModeApp(app)) {
|
|
100
|
+
return cleaned;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
104
|
+
|
|
63
105
|
// Collapse repeated app prefixes (app/app/... -> app/...)
|
|
64
|
-
const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
|
|
106
|
+
const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
|
|
65
107
|
if (!cleaned.startsWith(`${app}/`)) {
|
|
66
108
|
cleaned = `${app}/${cleaned}`;
|
|
67
109
|
} else {
|
|
@@ -73,7 +115,6 @@ export function ensureKeyHasAppPrefix(key = '') {
|
|
|
73
115
|
|
|
74
116
|
export function ensurePrefix(prefix = '') {
|
|
75
117
|
const app = getAppName();
|
|
76
|
-
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
77
118
|
|
|
78
119
|
// Security: Detect path traversal attempts
|
|
79
120
|
if (prefix && (prefix.includes('../') || prefix.includes('..\\') || /\.\.[\\/]/.test(prefix))) {
|
|
@@ -87,6 +128,13 @@ export function ensurePrefix(prefix = '') {
|
|
|
87
128
|
|
|
88
129
|
// Only strip LEADING slashes. Preserve trailing slashes for directory listing.
|
|
89
130
|
let p = String(prefix || '').replace(/^\/+/, '');
|
|
131
|
+
|
|
132
|
+
// Special case: APP_NAME=portal means operate from bucket root.
|
|
133
|
+
if (isRootModeApp(app)) {
|
|
134
|
+
return p;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
90
138
|
|
|
91
139
|
if (!p) return `${app}`;
|
|
92
140
|
|
|
@@ -96,7 +144,7 @@ export function ensurePrefix(prefix = '') {
|
|
|
96
144
|
|
|
97
145
|
if (p.startsWith(`${app}/`)) {
|
|
98
146
|
// collapse repeated app/ sequences
|
|
99
|
-
const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
|
|
147
|
+
const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
|
|
100
148
|
return p.replace(appPrefixRegex, `${app}/`);
|
|
101
149
|
}
|
|
102
150
|
return `${app}/${p}`;
|
|
@@ -110,19 +158,13 @@ const _clientCache = new Map();
|
|
|
110
158
|
const _configCache = new Map();
|
|
111
159
|
|
|
112
160
|
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
|
-
}
|
|
161
|
+
bucketName = resolveBucketName(bucketName);
|
|
120
162
|
|
|
121
163
|
const cacheKey = bucketName;
|
|
122
164
|
if (_configCache.has(cacheKey)) return _configCache.get(cacheKey);
|
|
123
165
|
|
|
124
166
|
// Look up bucket config from BucketConnection JSON
|
|
125
|
-
if (!
|
|
167
|
+
if (!hasBucketConfigs) {
|
|
126
168
|
console.error(`[mbkbucket] ❌ BucketConnection is not configured or failed to parse`);
|
|
127
169
|
console.error(`[mbkbucket] Please set BucketConnection in your .env file with this format:`);
|
|
128
170
|
console.error(`[mbkbucket] BucketConnection={"r2":{"BUCKET_NAME":"...","ACCESS_KEY_ID":"...","SECRET_ACCESS_KEY":"...","ENDPOINT":"https://..."}}`);
|
|
@@ -135,7 +177,7 @@ export function getBucketConfig(bucketName) {
|
|
|
135
177
|
const cfg = allBucketConfigs[bucketName];
|
|
136
178
|
|
|
137
179
|
if (!cfg) {
|
|
138
|
-
throw new Error(`Bucket '${bucketName}' not found in BucketConnection. Available buckets: ${
|
|
180
|
+
throw new Error(`Bucket '${bucketName}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
|
|
139
181
|
}
|
|
140
182
|
|
|
141
183
|
_configCache.set(cacheKey, cfg);
|
|
@@ -144,10 +186,7 @@ export function getBucketConfig(bucketName) {
|
|
|
144
186
|
|
|
145
187
|
// Function to get S3 client for specific bucket — returns cached singleton
|
|
146
188
|
export function getBucketClient(bucketName) {
|
|
147
|
-
|
|
148
|
-
if (!bucketName) {
|
|
149
|
-
bucketName = getDefaultBucketName();
|
|
150
|
-
}
|
|
189
|
+
bucketName = resolveBucketName(bucketName);
|
|
151
190
|
|
|
152
191
|
if (_clientCache.has(bucketName)) return _clientCache.get(bucketName);
|
|
153
192
|
|
|
@@ -218,10 +257,10 @@ async function checkHealth() {
|
|
|
218
257
|
// Quick diagnostics at startup
|
|
219
258
|
(async () => {
|
|
220
259
|
try {
|
|
221
|
-
|
|
260
|
+
debugS3('mbkautheVar snapshot: %O', { APP_NAME: mbkautheVar?.APP_NAME, bucket: mbkautheVar?.bucket });
|
|
222
261
|
const health = await checkHealth();
|
|
223
262
|
if (health.status === 'healthy') {
|
|
224
|
-
|
|
263
|
+
debugS3('Connected to bucket: %s (%sms)', health.bucket, health.responseTime);
|
|
225
264
|
} else {
|
|
226
265
|
console.error(`[mbkbucket] S3 connection failed:`, health.error);
|
|
227
266
|
}
|
|
@@ -254,11 +293,12 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
|
254
293
|
const config = getBucketConfig(bucketName);
|
|
255
294
|
const bucket = config.BUCKET_NAME;
|
|
256
295
|
|
|
257
|
-
|
|
296
|
+
debugS3('Uploading file to bucket=%s key=%s', bucket, keyToUpload);
|
|
258
297
|
|
|
259
298
|
// Add default metadata (supports metadata)
|
|
299
|
+
const uploadedAt = new Date().toISOString();
|
|
260
300
|
const defaultMetadata = {
|
|
261
|
-
'uploaded-at':
|
|
301
|
+
'uploaded-at': uploadedAt,
|
|
262
302
|
'file-size': fileBuffer.length.toString(),
|
|
263
303
|
'upload-source': 'web-portal',
|
|
264
304
|
...metadata
|
|
@@ -289,7 +329,7 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
|
289
329
|
fileSize: fileBuffer.length,
|
|
290
330
|
key: keyToUpload,
|
|
291
331
|
contentType,
|
|
292
|
-
uploadedAt
|
|
332
|
+
uploadedAt
|
|
293
333
|
};
|
|
294
334
|
} catch (error) {
|
|
295
335
|
console.error(`Upload failed for key ${key}:`, error);
|
|
@@ -329,7 +369,7 @@ export async function downloadFile(key, options = {}) {
|
|
|
329
369
|
|
|
330
370
|
// Log range requests for debugging
|
|
331
371
|
if (range) {
|
|
332
|
-
|
|
372
|
+
debugS3('Range request for %s: %s', keyToDownload, range);
|
|
333
373
|
}
|
|
334
374
|
|
|
335
375
|
const command = new GetObjectCommand({
|
|
@@ -342,9 +382,7 @@ export async function downloadFile(key, options = {}) {
|
|
|
342
382
|
...(responseContentType && { ResponseContentType: responseContentType }),
|
|
343
383
|
});
|
|
344
384
|
|
|
345
|
-
const startTime = Date.now();
|
|
346
385
|
const result = await client.send(command);
|
|
347
|
-
const downloadTime = Date.now() - startTime;
|
|
348
386
|
|
|
349
387
|
return {
|
|
350
388
|
...result,
|
|
@@ -414,6 +452,8 @@ export async function deleteFiles(keys, bucketName) {
|
|
|
414
452
|
// S3 allows max 1000 objects per delete request
|
|
415
453
|
const maxBatchSize = 1000;
|
|
416
454
|
const results = [];
|
|
455
|
+
let deletedCount = 0;
|
|
456
|
+
const errors = [];
|
|
417
457
|
|
|
418
458
|
for (let i = 0; i < keys.length; i += maxBatchSize) {
|
|
419
459
|
const batch = keys.slice(i, i + maxBatchSize).map(k => ({ Key: ensureKeyHasAppPrefix(k) }));
|
|
@@ -428,12 +468,14 @@ export async function deleteFiles(keys, bucketName) {
|
|
|
428
468
|
|
|
429
469
|
const result = await client.send(command);
|
|
430
470
|
results.push(result);
|
|
471
|
+
deletedCount += result.Deleted?.length || 0;
|
|
472
|
+
if (result.Errors?.length) errors.push(...result.Errors);
|
|
431
473
|
}
|
|
432
474
|
|
|
433
475
|
return {
|
|
434
476
|
results,
|
|
435
|
-
deletedCount
|
|
436
|
-
errors
|
|
477
|
+
deletedCount,
|
|
478
|
+
errors,
|
|
437
479
|
deletedAt: new Date().toISOString()
|
|
438
480
|
};
|
|
439
481
|
} catch (error) {
|
|
@@ -568,19 +610,21 @@ export async function getFileMetadata(key, bucketName) {
|
|
|
568
610
|
});
|
|
569
611
|
|
|
570
612
|
const result = await client.send(command);
|
|
613
|
+
const queriedAt = new Date().toISOString();
|
|
571
614
|
|
|
572
615
|
return {
|
|
573
616
|
...result,
|
|
574
617
|
key: keyToCheck,
|
|
575
618
|
exists: true,
|
|
576
|
-
queriedAt
|
|
619
|
+
queriedAt
|
|
577
620
|
};
|
|
578
621
|
} catch (error) {
|
|
622
|
+
const queriedAt = new Date().toISOString();
|
|
579
623
|
if (error.name === 'NotFound' || error.name === 'NoSuchKey') {
|
|
580
624
|
return {
|
|
581
625
|
key,
|
|
582
626
|
exists: false,
|
|
583
|
-
queriedAt
|
|
627
|
+
queriedAt
|
|
584
628
|
};
|
|
585
629
|
}
|
|
586
630
|
|
|
@@ -619,17 +663,18 @@ export { checkHealth };
|
|
|
619
663
|
/**
|
|
620
664
|
* Initiate a multipart upload. Returns the S3 UploadId.
|
|
621
665
|
*/
|
|
622
|
-
export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}) {
|
|
666
|
+
export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}, bucketName) {
|
|
623
667
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
624
|
-
const cfg = getBucketConfig();
|
|
625
|
-
const client = getBucketClient();
|
|
668
|
+
const cfg = getBucketConfig(bucketName);
|
|
669
|
+
const client = getBucketClient(bucketName);
|
|
670
|
+
const uploadedAt = new Date().toISOString();
|
|
626
671
|
const command = new CreateMultipartUploadCommand({
|
|
627
672
|
Bucket: cfg.BUCKET_NAME,
|
|
628
673
|
Key: keyToUse,
|
|
629
674
|
ContentType: contentType,
|
|
630
675
|
Metadata: {
|
|
631
676
|
'upload-source': 'web-portal',
|
|
632
|
-
'uploaded-at':
|
|
677
|
+
'uploaded-at': uploadedAt,
|
|
633
678
|
...metadata
|
|
634
679
|
}
|
|
635
680
|
});
|
|
@@ -641,10 +686,10 @@ export async function createMultipartUpload(key, contentType = 'application/octe
|
|
|
641
686
|
* Upload a single part. partNumber is 1-based.
|
|
642
687
|
* Returns the ETag needed for CompleteMultipartUpload.
|
|
643
688
|
*/
|
|
644
|
-
export async function uploadPart(key, uploadId, partNumber, buffer) {
|
|
689
|
+
export async function uploadPart(key, uploadId, partNumber, buffer, bucketName) {
|
|
645
690
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
646
|
-
const cfg = getBucketConfig();
|
|
647
|
-
const client = getBucketClient();
|
|
691
|
+
const cfg = getBucketConfig(bucketName);
|
|
692
|
+
const client = getBucketClient(bucketName);
|
|
648
693
|
const command = new UploadPartCommand({
|
|
649
694
|
Bucket: cfg.BUCKET_NAME,
|
|
650
695
|
Key: keyToUse,
|
|
@@ -661,10 +706,10 @@ export async function uploadPart(key, uploadId, partNumber, buffer) {
|
|
|
661
706
|
* Complete a multipart upload.
|
|
662
707
|
* parts must be an array of { partNumber, ETag } sorted ascending by partNumber.
|
|
663
708
|
*/
|
|
664
|
-
export async function completeMultipartUpload(key, uploadId, parts) {
|
|
709
|
+
export async function completeMultipartUpload(key, uploadId, parts, bucketName) {
|
|
665
710
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
666
|
-
const cfg = getBucketConfig();
|
|
667
|
-
const client = getBucketClient();
|
|
711
|
+
const cfg = getBucketConfig(bucketName);
|
|
712
|
+
const client = getBucketClient(bucketName);
|
|
668
713
|
const sorted = [...parts].sort((a, b) => a.partNumber - b.partNumber);
|
|
669
714
|
const command = new CompleteMultipartUploadCommand({
|
|
670
715
|
Bucket: cfg.BUCKET_NAME,
|
|
@@ -681,10 +726,10 @@ export async function completeMultipartUpload(key, uploadId, parts) {
|
|
|
681
726
|
/**
|
|
682
727
|
* Abort an in-progress multipart upload, freeing all stored parts.
|
|
683
728
|
*/
|
|
684
|
-
export async function abortMultipartUpload(key, uploadId) {
|
|
729
|
+
export async function abortMultipartUpload(key, uploadId, bucketName) {
|
|
685
730
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
686
|
-
const cfg = getBucketConfig();
|
|
687
|
-
const client = getBucketClient();
|
|
731
|
+
const cfg = getBucketConfig(bucketName);
|
|
732
|
+
const client = getBucketClient(bucketName);
|
|
688
733
|
const command = new AbortMultipartUploadCommand({
|
|
689
734
|
Bucket: cfg.BUCKET_NAME,
|
|
690
735
|
Key: keyToUse,
|
|
@@ -695,26 +740,26 @@ export async function abortMultipartUpload(key, uploadId) {
|
|
|
695
740
|
}
|
|
696
741
|
|
|
697
742
|
// Generate signed URL for temporary access
|
|
698
|
-
export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600) {
|
|
743
|
+
export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, bucketName) {
|
|
699
744
|
try {
|
|
700
745
|
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
701
746
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
702
747
|
|
|
703
748
|
let command;
|
|
704
|
-
const cfg = getBucketConfig();
|
|
705
|
-
const
|
|
749
|
+
const cfg = getBucketConfig(bucketName);
|
|
750
|
+
const resolvedBucketName = cfg.BUCKET_NAME;
|
|
706
751
|
switch (operation) {
|
|
707
752
|
case 'getObject':
|
|
708
|
-
command = new GetObjectCommand({ Bucket:
|
|
753
|
+
command = new GetObjectCommand({ Bucket: resolvedBucketName, Key: keyToUse });
|
|
709
754
|
break;
|
|
710
755
|
case 'putObject':
|
|
711
|
-
command = new PutObjectCommand({ Bucket:
|
|
756
|
+
command = new PutObjectCommand({ Bucket: resolvedBucketName, Key: keyToUse });
|
|
712
757
|
break;
|
|
713
758
|
default:
|
|
714
759
|
throw new Error(`Unsupported operation: ${operation}`);
|
|
715
760
|
}
|
|
716
761
|
|
|
717
|
-
const signedUrl = await getSignedUrl(
|
|
762
|
+
const signedUrl = await getSignedUrl(getBucketClient(bucketName), command, { expiresIn });
|
|
718
763
|
|
|
719
764
|
return {
|
|
720
765
|
url: signedUrl,
|
|
@@ -742,7 +787,7 @@ export async function generateSignedUrl(key, operation = 'getObject', expiresIn
|
|
|
742
787
|
*/
|
|
743
788
|
export async function listIncompleteMultipartUploads(prefix = '', bucketName) {
|
|
744
789
|
try {
|
|
745
|
-
const effectivePrefix =
|
|
790
|
+
const effectivePrefix = ensurePrefix(prefix || '');
|
|
746
791
|
const cfg = getBucketConfig(bucketName);
|
|
747
792
|
const client = getBucketClient(bucketName);
|
|
748
793
|
|
|
@@ -771,7 +816,7 @@ export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefi
|
|
|
771
816
|
const uploads = await listIncompleteMultipartUploads(prefix, bucketName);
|
|
772
817
|
|
|
773
818
|
if (!uploads.length) {
|
|
774
|
-
|
|
819
|
+
debugS3('No incomplete multipart uploads found');
|
|
775
820
|
return { abortedCount: 0, uploads: [] };
|
|
776
821
|
}
|
|
777
822
|
|
|
@@ -783,22 +828,22 @@ export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefi
|
|
|
783
828
|
});
|
|
784
829
|
|
|
785
830
|
if (!uploadsToAbort.length) {
|
|
786
|
-
|
|
831
|
+
debugS3('No incomplete uploads older than %s days', olderThanDays);
|
|
787
832
|
return { abortedCount: 0, uploads: [] };
|
|
788
833
|
}
|
|
789
834
|
|
|
790
|
-
|
|
835
|
+
debugS3('Found %s incomplete uploads to clean up', uploadsToAbort.length);
|
|
791
836
|
|
|
792
837
|
const aborted = [];
|
|
793
838
|
for (const upload of uploadsToAbort) {
|
|
794
839
|
try {
|
|
795
|
-
await abortMultipartUpload(upload.Key, upload.UploadId);
|
|
840
|
+
await abortMultipartUpload(upload.Key, upload.UploadId, bucketName);
|
|
796
841
|
aborted.push({
|
|
797
842
|
key: upload.Key,
|
|
798
843
|
uploadId: upload.UploadId,
|
|
799
844
|
initiated: upload.Initiated
|
|
800
845
|
});
|
|
801
|
-
|
|
846
|
+
debugS3('Aborted incomplete upload: %s (initiated: %s)', upload.Key, upload.Initiated);
|
|
802
847
|
} catch (err) {
|
|
803
848
|
console.error(`[mbkbucket] Failed to abort upload ${upload.Key}:`, err);
|
|
804
849
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkbucket",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.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",
|
|
@@ -9,18 +9,19 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"types": "index.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"dev": "cross-env
|
|
13
|
-
"test": "
|
|
12
|
+
"dev": "cross-env NODE_ENV=dev nodemon index.js",
|
|
13
|
+
"test": "node --test"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@aws-sdk/client-s3": "^3.968.0",
|
|
17
17
|
"@aws-sdk/s3-request-presigner": "^3.1000.0",
|
|
18
18
|
"compression": "^1.8.0",
|
|
19
|
+
"debug": "^2.6.9",
|
|
19
20
|
"dotenv": "^16.6.1",
|
|
20
21
|
"express": "^4.22.1",
|
|
21
22
|
"express-handlebars": "^8.0.3",
|
|
22
23
|
"express-rate-limit": "^7.0.1",
|
|
23
|
-
"mbkauthe": "
|
|
24
|
+
"mbkauthe": "5.0.4",
|
|
24
25
|
"multer": "^2.0.2",
|
|
25
26
|
"pg": "^8.16.3"
|
|
26
27
|
},
|
|
@@ -49,4 +50,4 @@
|
|
|
49
50
|
"mbkauthe",
|
|
50
51
|
"mbktech.org"
|
|
51
52
|
]
|
|
52
|
-
}
|
|
53
|
+
}
|