mbkbucket 3.3.0 → 3.3.2
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.js +8 -18
- package/lib/cli.js +642 -0
- package/{public → lib/public}/bucketadmin.js +1 -1
- package/{src → lib/src}/app.js +37 -50
- package/{src → lib/src}/cli/auth-helper.js +24 -103
- package/lib/src/config/index.js +108 -0
- package/{src → lib/src}/config/validation.js +14 -30
- package/{src → lib/src}/controllers/api.controller.js +85 -176
- package/{src → lib/src}/controllers/dashboard.controller.js +12 -23
- package/{src → lib/src}/controllers/info.controller.js +17 -20
- package/{src → lib/src}/controllers/view.controller.js +23 -70
- package/{src → lib/src}/middleware/async-handler.js +1 -16
- package/{src → lib/src}/middleware/bucket-resolver.js +7 -13
- package/{src → lib/src}/middleware/error-handler.js +6 -14
- package/{src → lib/src}/middleware/rate-limiter.js +2 -10
- package/{src → lib/src}/middleware/security.js +1 -17
- package/lib/src/routes/api.routes.js +40 -0
- package/{src → lib/src}/routes/index.js +1 -9
- package/{src → lib/src}/routes/info.routes.js +2 -3
- package/{src → lib/src}/routes/view.routes.js +3 -8
- package/{src → lib/src}/services/s3.service.js +157 -349
- package/lib/src/utils/errors.js +45 -0
- package/lib/src/utils/helpers.js +251 -0
- package/package.json +18 -3
- package/.github/PACKAGE.md +0 -19
- package/.github/workflows/publish.yml +0 -88
- package/cli.js +0 -977
- package/index.d.ts +0 -444
- package/lib/index.js +0 -17
- package/src/config/index.js +0 -133
- package/src/routes/api.routes.js +0 -66
- package/src/utils/errors.js +0 -76
- package/src/utils/helpers.js +0 -197
- package/test/config-validation.test.js +0 -116
- /package/{public → lib/public}/bucketadmin.css +0 -0
- /package/{public → lib/public}/mbkbucket-helper-demo.html +0 -0
- /package/{public → lib/public}/mbkbucket-helper.css +0 -0
- /package/{public → lib/public}/mbkbucket-helper.js +0 -0
- /package/{src → lib/src}/utils/logger.js +0 -0
|
@@ -1,72 +1,56 @@
|
|
|
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 "
|
|
4
|
+
import { createLogger } from "#logger";
|
|
5
|
+
import { nowIso, trimSlashes, trimLeadingSlashes } from "#helpers";
|
|
5
6
|
|
|
6
7
|
dotenv.config();
|
|
7
8
|
const debugS3 = createLogger('s3');
|
|
8
9
|
|
|
9
10
|
function parseJsonEnv(envVar, fallback = {}) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
if (!envVar) return fallback;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(envVar);
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error(`❌ Error parsing JSON for envVar: ${envVar ? String(envVar).slice(0, 200) : 'undefined'} - ${e.message}`);
|
|
16
|
+
console.error(`💡 Tip: Check that inner objects are NOT quoted as strings. Use {"key":{"field":"value"}} not {"key":"{"field":"value"}"}`);
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// Parse BucketConnection JSON that contains all bucket configurations
|
|
21
21
|
const allBucketConfigs = parseJsonEnv(process.env.BucketConnection, {});
|
|
22
22
|
const availableBucketNames = Object.freeze(Object.keys(allBucketConfigs || {}));
|
|
23
23
|
const hasBucketConfigs = availableBucketNames.length > 0;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
27
|
-
}
|
|
25
|
+
const escapeRegExp = value => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
26
|
|
|
29
27
|
export function getAvailableBucketNames() {
|
|
30
28
|
return availableBucketNames;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
// Helper to get default bucket name
|
|
34
31
|
function getDefaultBucketName() {
|
|
35
|
-
|
|
36
|
-
return mbkautheVar.bucket;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (availableBucketNames.length > 0) {
|
|
40
|
-
return availableBucketNames[0];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return null;
|
|
32
|
+
return mbkautheVar?.bucket || (availableBucketNames.length > 0 ? availableBucketNames[0] : null);
|
|
44
33
|
}
|
|
45
34
|
|
|
46
35
|
export function resolveBucketName(bucketName) {
|
|
47
|
-
const
|
|
48
|
-
const candidate =
|
|
49
|
-
? getDefaultBucketName()
|
|
50
|
-
: String(rawCandidate);
|
|
36
|
+
const raw = typeof bucketName === 'string' ? bucketName.trim() : bucketName;
|
|
37
|
+
const candidate = !raw ? getDefaultBucketName() : String(raw);
|
|
51
38
|
|
|
52
39
|
if (!candidate) {
|
|
53
40
|
throw new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
|
|
54
41
|
}
|
|
55
|
-
|
|
56
42
|
if (!hasBucketConfigs) {
|
|
57
43
|
throw new Error('BucketConnection environment variable is not set or empty. Please configure it with your bucket connections.');
|
|
58
44
|
}
|
|
59
|
-
|
|
60
45
|
if (!allBucketConfigs[candidate]) {
|
|
61
46
|
throw new Error(`Bucket '${candidate}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
|
|
62
47
|
}
|
|
63
|
-
|
|
64
48
|
return candidate;
|
|
65
49
|
}
|
|
66
50
|
|
|
67
51
|
export function getAppName() {
|
|
68
|
-
const app =
|
|
69
|
-
return
|
|
52
|
+
const app = mbkautheVar?.APP_NAME;
|
|
53
|
+
return typeof app === 'string' && app.trim() ? app.trim() : '';
|
|
70
54
|
}
|
|
71
55
|
|
|
72
56
|
function isRootModeApp(appName) {
|
|
@@ -74,110 +58,73 @@ function isRootModeApp(appName) {
|
|
|
74
58
|
return app === 'portal' || app === 'mbkbucket';
|
|
75
59
|
}
|
|
76
60
|
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
61
|
+
function validateKeySafety(str, isKey = false) {
|
|
62
|
+
if (isKey && !str) throw new Error('Key is required');
|
|
63
|
+
if (str && (str.includes('../') || str.includes('..\\') || /\.\.[\\/]/.test(str))) {
|
|
64
|
+
throw new Error(`Path traversal detected: invalid ${isKey ? 'key' : 'prefix'}`);
|
|
80
65
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (key.includes('../') || key.includes('..\\') || /\.\.[\\/]/.test(key)) {
|
|
85
|
-
throw new Error('Path traversal detected: invalid key');
|
|
66
|
+
if (str && /[\x00-\x1f\x7f]/.test(str)) {
|
|
67
|
+
throw new Error(`Invalid characters in ${isKey ? 'key' : 'prefix'}`);
|
|
86
68
|
}
|
|
69
|
+
}
|
|
87
70
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let cleaned = String(key).replace(/^\/+/, '');
|
|
93
|
-
|
|
94
|
-
if (isRootModeApp(app)) {
|
|
95
|
-
return cleaned;
|
|
96
|
-
}
|
|
71
|
+
export function ensureKeyHasAppPrefix(key = '') {
|
|
72
|
+
if (Array.isArray(key)) key = key.join('/');
|
|
73
|
+
validateKeySafety(key, true);
|
|
97
74
|
|
|
75
|
+
const app = getAppName();
|
|
76
|
+
let cleaned = trimLeadingSlashes(key);
|
|
77
|
+
if (isRootModeApp(app)) return cleaned;
|
|
98
78
|
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
99
79
|
|
|
100
80
|
const appPrefixRegex = new RegExp(`^(?:${escapeRegExp(app)}\/)+`);
|
|
101
|
-
|
|
102
|
-
cleaned = `${app}/${cleaned}`;
|
|
103
|
-
} else {
|
|
104
|
-
cleaned = cleaned.replace(appPrefixRegex, `${app}/`);
|
|
105
|
-
}
|
|
106
|
-
return cleaned;
|
|
81
|
+
return cleaned.startsWith(`${app}/`) ? cleaned.replace(appPrefixRegex, `${app}/`) : `${app}/${cleaned}`;
|
|
107
82
|
}
|
|
108
83
|
|
|
109
84
|
export function ensurePrefix(prefix = '') {
|
|
110
|
-
if (Array.isArray(prefix))
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
const app = getAppName();
|
|
114
|
-
|
|
115
|
-
if (prefix && (prefix.includes('../') || prefix.includes('..\\') || /\.\.[\\/]/.test(prefix))) {
|
|
116
|
-
throw new Error('Path traversal detected: invalid prefix');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (prefix && /[\x00-\x1f\x7f]/.test(prefix)) {
|
|
120
|
-
throw new Error('Invalid characters in prefix');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
let p = String(prefix || '').replace(/^\/+/, '');
|
|
124
|
-
|
|
125
|
-
if (isRootModeApp(app)) {
|
|
126
|
-
return p;
|
|
127
|
-
}
|
|
85
|
+
if (Array.isArray(prefix)) prefix = prefix.join('/');
|
|
86
|
+
validateKeySafety(prefix, false);
|
|
128
87
|
|
|
88
|
+
const app = getAppName();
|
|
89
|
+
let p = trimLeadingSlashes(prefix);
|
|
90
|
+
if (isRootModeApp(app)) return p;
|
|
129
91
|
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
if (p === app) return app;
|
|
134
|
-
if (p === `${app}/`) return `${app}/`;
|
|
92
|
+
if (!p) return app;
|
|
93
|
+
if (p === app || p === `${app}/`) return p;
|
|
135
94
|
|
|
136
95
|
if (p.startsWith(`${app}/`)) {
|
|
137
|
-
|
|
138
|
-
return p.replace(appPrefixRegex, `${app}/`);
|
|
96
|
+
return p.replace(new RegExp(`^(?:${escapeRegExp(app)}\/)+`), `${app}/`);
|
|
139
97
|
}
|
|
140
98
|
return `${app}/${p}`;
|
|
141
99
|
}
|
|
142
100
|
|
|
143
101
|
// ---------------------------------------------------------------------------
|
|
144
|
-
//
|
|
102
|
+
// S3 Client & Config Cache
|
|
145
103
|
// ---------------------------------------------------------------------------
|
|
146
104
|
const _clientCache = new Map();
|
|
147
105
|
const _configCache = new Map();
|
|
148
106
|
|
|
149
107
|
export function getBucketConfig(bucketName) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const cacheKey = bucketName;
|
|
153
|
-
if (_configCache.has(cacheKey)) return _configCache.get(cacheKey);
|
|
108
|
+
const resolved = resolveBucketName(bucketName);
|
|
109
|
+
if (_configCache.has(resolved)) return _configCache.get(resolved);
|
|
154
110
|
|
|
155
111
|
if (!hasBucketConfigs) {
|
|
156
|
-
console.error(
|
|
157
|
-
|
|
158
|
-
console.error(`[mbkbucket] BucketConnection={"r2":{"BUCKET_NAME":"...","ACCESS_KEY_ID":"...","SECRET_ACCESS_KEY":"...","ENDPOINT":"https://..."}}`);
|
|
159
|
-
console.error(`[mbkbucket] ⚠️ Important: Inner objects must NOT be quoted as strings!`);
|
|
160
|
-
console.error(`[mbkbucket] Wrong: {"r2":"{\\"BUCKET_NAME\\":\\"...\\"}"} ❌`);
|
|
161
|
-
console.error(`[mbkbucket] Right: {"r2":{"BUCKET_NAME":"..."}} ✓`);
|
|
162
|
-
throw new Error(`BucketConnection environment variable is not set or empty. Please configure it with your bucket connections.`);
|
|
112
|
+
console.error('[mbkbucket] ❌ BucketConnection is not configured or failed to parse');
|
|
113
|
+
throw new Error('BucketConnection environment variable is not set or empty. Please configure it with your bucket connections.');
|
|
163
114
|
}
|
|
164
115
|
|
|
165
|
-
const cfg = allBucketConfigs[
|
|
116
|
+
const cfg = allBucketConfigs[resolved];
|
|
117
|
+
if (!cfg) throw new Error(`Bucket '${resolved}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
|
|
166
118
|
|
|
167
|
-
|
|
168
|
-
throw new Error(`Bucket '${bucketName}' not found in BucketConnection. Available buckets: ${availableBucketNames.join(', ')}`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
_configCache.set(cacheKey, cfg);
|
|
119
|
+
_configCache.set(resolved, cfg);
|
|
172
120
|
return cfg;
|
|
173
121
|
}
|
|
174
122
|
|
|
175
123
|
export function getBucketClient(bucketName) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (_clientCache.has(bucketName)) return _clientCache.get(bucketName);
|
|
124
|
+
const resolved = resolveBucketName(bucketName);
|
|
125
|
+
if (_clientCache.has(resolved)) return _clientCache.get(resolved);
|
|
179
126
|
|
|
180
|
-
const config = getBucketConfig(
|
|
127
|
+
const config = getBucketConfig(resolved);
|
|
181
128
|
const client = new S3Client({
|
|
182
129
|
region: config.region || 'auto',
|
|
183
130
|
endpoint: config.ENDPOINT,
|
|
@@ -199,14 +146,10 @@ export function getBucketClient(bucketName) {
|
|
|
199
146
|
forcePathStyle: true,
|
|
200
147
|
});
|
|
201
148
|
|
|
202
|
-
_clientCache.set(
|
|
149
|
+
_clientCache.set(resolved, client);
|
|
203
150
|
return client;
|
|
204
151
|
}
|
|
205
152
|
|
|
206
|
-
/**
|
|
207
|
-
* Resolve both client and config in a single call — avoids redundant
|
|
208
|
-
* resolveBucketName + cache lookups in every S3 operation.
|
|
209
|
-
*/
|
|
210
153
|
export function getBucketClientAndConfig(bucketName) {
|
|
211
154
|
const resolved = resolveBucketName(bucketName);
|
|
212
155
|
return {
|
|
@@ -216,64 +159,62 @@ export function getBucketClientAndConfig(bucketName) {
|
|
|
216
159
|
};
|
|
217
160
|
}
|
|
218
161
|
|
|
219
|
-
// Default client singleton (lazily created on first access, not at import time)
|
|
220
162
|
let _defaultClient = null;
|
|
221
163
|
export const bucketClient = new Proxy({}, {
|
|
222
164
|
get(_, prop) {
|
|
223
|
-
|
|
165
|
+
_defaultClient ??= getBucketClient();
|
|
224
166
|
const val = _defaultClient[prop];
|
|
225
167
|
return typeof val === 'function' ? val.bind(_defaultClient) : val;
|
|
226
168
|
}
|
|
227
169
|
});
|
|
228
170
|
|
|
229
|
-
//
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Health Checks
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
230
175
|
export async function checkHealth() {
|
|
231
176
|
try {
|
|
232
177
|
const startTime = Date.now();
|
|
233
178
|
const { client, config } = getBucketClientAndConfig();
|
|
234
|
-
const
|
|
179
|
+
const { BUCKET_NAME: bucket, region = 'auto' } = config;
|
|
235
180
|
|
|
236
|
-
await client.send(new ListObjectsV2Command({ Bucket:
|
|
181
|
+
await client.send(new ListObjectsV2Command({ Bucket: bucket, MaxKeys: 1 }));
|
|
237
182
|
return {
|
|
238
183
|
status: 'healthy',
|
|
239
184
|
responseTime: Date.now() - startTime,
|
|
240
|
-
bucket
|
|
241
|
-
region
|
|
242
|
-
checkedAt:
|
|
185
|
+
bucket,
|
|
186
|
+
region,
|
|
187
|
+
checkedAt: nowIso()
|
|
243
188
|
};
|
|
244
189
|
} catch (error) {
|
|
245
190
|
let bucket = 'unknown';
|
|
246
191
|
try { bucket = getBucketConfig().BUCKET_NAME; } catch {}
|
|
247
|
-
return { status: 'unhealthy', error: error.message, bucket, checkedAt:
|
|
192
|
+
return { status: 'unhealthy', error: error.message, bucket, checkedAt: nowIso() };
|
|
248
193
|
}
|
|
249
194
|
}
|
|
250
195
|
|
|
251
|
-
/**
|
|
252
|
-
* Run S3 health check and log results. Should be called explicitly at app startup.
|
|
253
|
-
*/
|
|
254
196
|
export async function runHealthCheck() {
|
|
255
197
|
try {
|
|
256
|
-
debugS3('mbkautheVar snapshot: %O', { APP_NAME: mbkautheVar?.APP_NAME, bucket: mbkautheVar?.bucket });
|
|
257
198
|
const health = await checkHealth();
|
|
258
199
|
if (health.status === 'healthy') {
|
|
259
200
|
debugS3('Connected to bucket: %s (%sms)', health.bucket, health.responseTime);
|
|
260
201
|
} else {
|
|
261
|
-
console.error(
|
|
202
|
+
console.error('[mbkbucket] S3 connection failed:', health.error);
|
|
262
203
|
}
|
|
263
204
|
} catch (err) {
|
|
264
205
|
console.error("[mbkbucket] S3 connection test error:", err.message);
|
|
265
206
|
}
|
|
266
207
|
}
|
|
267
208
|
|
|
268
|
-
//
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Core S3 Operations
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
269
213
|
export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
270
214
|
try {
|
|
271
|
-
if (!key || !fileBuffer)
|
|
272
|
-
throw new Error('Key and file buffer are required');
|
|
273
|
-
}
|
|
215
|
+
if (!key || !fileBuffer) throw new Error('Key and file buffer are required');
|
|
274
216
|
|
|
275
217
|
const keyToUpload = ensureKeyHasAppPrefix(key);
|
|
276
|
-
|
|
277
218
|
const {
|
|
278
219
|
metadata = {},
|
|
279
220
|
cacheControl = 'public, max-age=31536000',
|
|
@@ -284,37 +225,27 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
|
284
225
|
} = options;
|
|
285
226
|
|
|
286
227
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
debugS3('Uploading file to bucket=%s key=%s', bucket, keyToUpload);
|
|
290
|
-
|
|
291
|
-
const uploadedAt = new Date().toISOString();
|
|
292
|
-
const defaultMetadata = {
|
|
293
|
-
'uploaded-at': uploadedAt,
|
|
294
|
-
'file-size': fileBuffer.length.toString(),
|
|
295
|
-
'upload-source': 'web-portal',
|
|
296
|
-
...metadata
|
|
297
|
-
};
|
|
228
|
+
debugS3('Uploading file to bucket=%s key=%s', config.BUCKET_NAME, keyToUpload);
|
|
298
229
|
|
|
230
|
+
const uploadedAt = nowIso();
|
|
299
231
|
const commandParams = {
|
|
300
|
-
Bucket:
|
|
232
|
+
Bucket: config.BUCKET_NAME,
|
|
301
233
|
Key: keyToUpload,
|
|
302
234
|
Body: fileBuffer,
|
|
303
235
|
ContentType: contentType,
|
|
304
236
|
CacheControl: cacheControl,
|
|
305
|
-
Metadata:
|
|
237
|
+
Metadata: {
|
|
238
|
+
'uploaded-at': uploadedAt,
|
|
239
|
+
'file-size': fileBuffer.length.toString(),
|
|
240
|
+
'upload-source': 'web-portal',
|
|
241
|
+
...metadata
|
|
242
|
+
},
|
|
306
243
|
ServerSideEncryption: serverSideEncryption,
|
|
307
244
|
StorageClass: storageClass,
|
|
245
|
+
...(preventOverwrite && { IfNoneMatch: '*' })
|
|
308
246
|
};
|
|
309
247
|
|
|
310
|
-
|
|
311
|
-
commandParams.IfNoneMatch = '*';
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const command = new PutObjectCommand(commandParams);
|
|
315
|
-
|
|
316
|
-
const result = await client.send(command);
|
|
317
|
-
|
|
248
|
+
const result = await client.send(new PutObjectCommand(commandParams));
|
|
318
249
|
return {
|
|
319
250
|
...result,
|
|
320
251
|
fileSize: fileBuffer.length,
|
|
@@ -324,114 +255,72 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
|
324
255
|
};
|
|
325
256
|
} catch (error) {
|
|
326
257
|
console.error(`Upload failed for key ${key}:`, error);
|
|
327
|
-
|
|
328
258
|
if (error.name === 'PreconditionFailed' || error.$metadata?.httpStatusCode === 412) {
|
|
329
259
|
throw new Error('File already exists');
|
|
330
260
|
}
|
|
331
|
-
|
|
332
261
|
throw new Error(`Upload failed: ${error.message}`);
|
|
333
262
|
}
|
|
334
263
|
}
|
|
335
264
|
|
|
336
|
-
// Download file with enhanced features
|
|
337
265
|
export async function downloadFile(key, options = {}) {
|
|
338
266
|
let keyToDownload;
|
|
339
267
|
try {
|
|
340
|
-
if (!key)
|
|
341
|
-
throw new Error('Key is required');
|
|
342
|
-
}
|
|
343
|
-
|
|
268
|
+
if (!key) throw new Error('Key is required');
|
|
344
269
|
keyToDownload = ensureKeyHasAppPrefix(key);
|
|
345
270
|
|
|
346
|
-
const {
|
|
347
|
-
range = null,
|
|
348
|
-
ifNoneMatch = null,
|
|
349
|
-
ifModifiedSince = null,
|
|
350
|
-
responseCacheControl = null,
|
|
351
|
-
responseContentType = null,
|
|
352
|
-
bucketName
|
|
353
|
-
} = options;
|
|
354
|
-
|
|
271
|
+
const { range, ifNoneMatch, ifModifiedSince, responseCacheControl, responseContentType, bucketName } = options;
|
|
355
272
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
356
|
-
const bucket = config.BUCKET_NAME;
|
|
357
273
|
|
|
358
|
-
if (range)
|
|
359
|
-
debugS3('Range request for %s: %s', keyToDownload, range);
|
|
360
|
-
}
|
|
274
|
+
if (range) debugS3('Range request for %s: %s', keyToDownload, range);
|
|
361
275
|
|
|
362
|
-
const
|
|
363
|
-
Bucket:
|
|
276
|
+
const result = await client.send(new GetObjectCommand({
|
|
277
|
+
Bucket: config.BUCKET_NAME,
|
|
364
278
|
Key: keyToDownload,
|
|
365
279
|
...(range && { Range: range }),
|
|
366
280
|
...(ifNoneMatch && { IfNoneMatch: ifNoneMatch }),
|
|
367
281
|
...(ifModifiedSince && { IfModifiedSince: ifModifiedSince }),
|
|
368
282
|
...(responseCacheControl && { ResponseCacheControl: responseCacheControl }),
|
|
369
283
|
...(responseContentType && { ResponseContentType: responseContentType }),
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
const result = await client.send(command);
|
|
284
|
+
}));
|
|
373
285
|
|
|
374
|
-
return {
|
|
375
|
-
...result,
|
|
376
|
-
key: keyToDownload,
|
|
377
|
-
};
|
|
286
|
+
return { ...result, key: keyToDownload };
|
|
378
287
|
} catch (error) {
|
|
379
|
-
if (error
|
|
288
|
+
if (error?.$metadata?.httpStatusCode === 304 || error?.name === '304' || error?.name === 'NotModified') {
|
|
380
289
|
debugS3('File not modified (304) for key %s', keyToDownload);
|
|
381
290
|
return { notModified: true, key: keyToDownload };
|
|
382
291
|
}
|
|
383
|
-
|
|
384
292
|
console.error(`Download failed for key ${key}:`, error);
|
|
385
|
-
|
|
386
|
-
if (error.name === '
|
|
387
|
-
throw new Error(`File not found: ${key}`);
|
|
388
|
-
} else if (error.name === 'AccessDenied') {
|
|
389
|
-
throw new Error(`Access denied for file: ${key}`);
|
|
390
|
-
}
|
|
391
|
-
|
|
293
|
+
if (error.name === 'NoSuchKey') throw new Error(`File not found: ${key}`);
|
|
294
|
+
if (error.name === 'AccessDenied') throw new Error(`Access denied for file: ${key}`);
|
|
392
295
|
throw new Error(`Download failed: ${error.message}`);
|
|
393
296
|
}
|
|
394
297
|
}
|
|
395
298
|
|
|
396
|
-
// Delete file with enhanced error handling
|
|
397
299
|
export async function deleteFile(key, bucketName) {
|
|
398
300
|
try {
|
|
399
|
-
if (!key)
|
|
400
|
-
throw new Error('Key is required');
|
|
401
|
-
}
|
|
301
|
+
if (!key) throw new Error('Key is required');
|
|
402
302
|
const keyToDelete = ensureKeyHasAppPrefix(key);
|
|
403
|
-
|
|
404
303
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
405
|
-
const bucket = config.BUCKET_NAME;
|
|
406
304
|
|
|
407
|
-
const
|
|
408
|
-
Bucket:
|
|
305
|
+
const result = await client.send(new DeleteObjectCommand({
|
|
306
|
+
Bucket: config.BUCKET_NAME,
|
|
409
307
|
Key: keyToDelete,
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
const result = await client.send(command);
|
|
308
|
+
}));
|
|
413
309
|
|
|
414
|
-
return {
|
|
415
|
-
...result,
|
|
416
|
-
key: keyToDelete,
|
|
417
|
-
deletedAt: new Date().toISOString()
|
|
418
|
-
};
|
|
310
|
+
return { ...result, key: keyToDelete, deletedAt: nowIso() };
|
|
419
311
|
} catch (error) {
|
|
420
|
-
console.error(
|
|
312
|
+
console.error('Delete failed:', error);
|
|
421
313
|
throw new Error(`Delete failed: ${error.message}`);
|
|
422
314
|
}
|
|
423
315
|
}
|
|
424
316
|
|
|
425
|
-
// Batch delete multiple files
|
|
426
317
|
export async function deleteFiles(keys, bucketName) {
|
|
427
318
|
try {
|
|
428
|
-
if (!keys || !Array.isArray(keys) || keys.length
|
|
319
|
+
if (!keys || !Array.isArray(keys) || !keys.length) {
|
|
429
320
|
throw new Error('Keys array is required and must not be empty');
|
|
430
321
|
}
|
|
431
322
|
|
|
432
323
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
433
|
-
const bucket = config.BUCKET_NAME;
|
|
434
|
-
|
|
435
324
|
const maxBatchSize = 1000;
|
|
436
325
|
const results = [];
|
|
437
326
|
let deletedCount = 0;
|
|
@@ -439,88 +328,60 @@ export async function deleteFiles(keys, bucketName) {
|
|
|
439
328
|
|
|
440
329
|
for (let i = 0; i < keys.length; i += maxBatchSize) {
|
|
441
330
|
const batch = keys.slice(i, i + maxBatchSize).map(k => ({ Key: ensureKeyHasAppPrefix(k) }));
|
|
331
|
+
const result = await client.send(new DeleteObjectsCommand({
|
|
332
|
+
Bucket: config.BUCKET_NAME,
|
|
333
|
+
Delete: { Objects: batch, Quiet: false }
|
|
334
|
+
}));
|
|
442
335
|
|
|
443
|
-
const command = new DeleteObjectsCommand({
|
|
444
|
-
Bucket: bucket,
|
|
445
|
-
Delete: {
|
|
446
|
-
Objects: batch,
|
|
447
|
-
Quiet: false
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
const result = await client.send(command);
|
|
452
336
|
results.push(result);
|
|
453
337
|
deletedCount += result.Deleted?.length || 0;
|
|
454
338
|
if (result.Errors?.length) errors.push(...result.Errors);
|
|
455
339
|
}
|
|
456
340
|
|
|
457
|
-
return {
|
|
458
|
-
results,
|
|
459
|
-
deletedCount,
|
|
460
|
-
errors,
|
|
461
|
-
deletedAt: new Date().toISOString()
|
|
462
|
-
};
|
|
341
|
+
return { results, deletedCount, errors, deletedAt: nowIso() };
|
|
463
342
|
} catch (error) {
|
|
464
343
|
console.error('Batch delete failed:', error);
|
|
465
344
|
throw new Error(`Batch delete failed: ${error.message}`);
|
|
466
345
|
}
|
|
467
346
|
}
|
|
468
347
|
|
|
469
|
-
// Delete all objects under a folder prefix (recursive)
|
|
470
348
|
export async function deleteFolder(prefix, bucketName) {
|
|
471
349
|
try {
|
|
472
|
-
if (!prefix)
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const cleaned = String(prefix).replace(/^\/+/, '').replace(/\/+$/g, '');
|
|
477
|
-
const effectivePrefix = ensurePrefix(cleaned) + '/';
|
|
478
|
-
|
|
350
|
+
if (!prefix) throw new Error('Prefix is required');
|
|
351
|
+
const effectivePrefix = `${ensurePrefix(trimSlashes(prefix))}/`;
|
|
479
352
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
480
|
-
const bucket = config.BUCKET_NAME;
|
|
481
353
|
|
|
482
354
|
const keysToDelete = [];
|
|
483
355
|
let continuationToken = null;
|
|
484
356
|
|
|
485
357
|
do {
|
|
486
|
-
const
|
|
487
|
-
Bucket:
|
|
358
|
+
const resp = await client.send(new ListObjectsV2Command({
|
|
359
|
+
Bucket: config.BUCKET_NAME,
|
|
488
360
|
Prefix: effectivePrefix,
|
|
489
361
|
ContinuationToken: continuationToken,
|
|
490
362
|
MaxKeys: 1000
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
const contents = resp.Contents || [];
|
|
494
|
-
for (const obj of contents) {
|
|
495
|
-
if (obj.Key) keysToDelete.push(obj.Key);
|
|
496
|
-
}
|
|
363
|
+
}));
|
|
364
|
+
(resp.Contents || []).forEach(obj => { if (obj.Key) keysToDelete.push(obj.Key); });
|
|
497
365
|
continuationToken = resp.IsTruncated ? resp.NextContinuationToken : null;
|
|
498
366
|
} while (continuationToken);
|
|
499
367
|
|
|
500
368
|
if (!keysToDelete.length) {
|
|
501
|
-
const
|
|
502
|
-
const exists = await fileExists(folderMarkerKey, bucketName);
|
|
369
|
+
const exists = await fileExists(effectivePrefix, bucketName);
|
|
503
370
|
if (exists) {
|
|
504
|
-
await deleteFile(
|
|
505
|
-
return { deletedCount: 1, deletedAt:
|
|
371
|
+
await deleteFile(effectivePrefix, bucketName);
|
|
372
|
+
return { deletedCount: 1, deletedAt: nowIso(), prefix: effectivePrefix };
|
|
506
373
|
}
|
|
507
|
-
return { deletedCount: 0, deletedAt:
|
|
374
|
+
return { deletedCount: 0, deletedAt: nowIso(), prefix: effectivePrefix };
|
|
508
375
|
}
|
|
509
376
|
|
|
510
377
|
const result = await deleteFiles(keysToDelete, bucketName);
|
|
511
|
-
|
|
512
|
-
return {
|
|
513
|
-
...result,
|
|
514
|
-
prefix: effectivePrefix,
|
|
515
|
-
deletedAt: new Date().toISOString()
|
|
516
|
-
};
|
|
378
|
+
return { ...result, prefix: effectivePrefix, deletedAt: nowIso() };
|
|
517
379
|
} catch (error) {
|
|
518
380
|
console.error('Delete folder failed:', error);
|
|
519
381
|
throw new Error(`Delete folder failed: ${error.message}`);
|
|
520
382
|
}
|
|
521
383
|
}
|
|
522
384
|
|
|
523
|
-
// List files with enhanced pagination and filtering
|
|
524
385
|
export async function listfiles(prefix = '', options = {}) {
|
|
525
386
|
try {
|
|
526
387
|
const {
|
|
@@ -533,25 +394,21 @@ export async function listfiles(prefix = '', options = {}) {
|
|
|
533
394
|
} = options;
|
|
534
395
|
|
|
535
396
|
const effectivePrefix = ensurePrefix(prefix);
|
|
536
|
-
|
|
537
397
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
538
|
-
const bucket = config.BUCKET_NAME;
|
|
539
398
|
|
|
540
|
-
const
|
|
541
|
-
Bucket:
|
|
399
|
+
const result = await client.send(new ListObjectsV2Command({
|
|
400
|
+
Bucket: config.BUCKET_NAME,
|
|
542
401
|
Prefix: effectivePrefix,
|
|
543
402
|
MaxKeys: maxKeys,
|
|
544
403
|
...(continuationToken && { ContinuationToken: continuationToken }),
|
|
545
404
|
...(delimiter && { Delimiter: delimiter }),
|
|
546
405
|
...(fetchOwner && { FetchOwner: fetchOwner }),
|
|
547
406
|
...(startAfter && { StartAfter: startAfter }),
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
const result = await client.send(command);
|
|
407
|
+
}));
|
|
551
408
|
|
|
552
409
|
return {
|
|
553
410
|
...result,
|
|
554
|
-
requestedAt:
|
|
411
|
+
requestedAt: nowIso(),
|
|
555
412
|
totalFiles: result.KeyCount || 0,
|
|
556
413
|
hasMore: result.IsTruncated || false,
|
|
557
414
|
nextToken: result.NextContinuationToken || null
|
|
@@ -562,100 +419,77 @@ export async function listfiles(prefix = '', options = {}) {
|
|
|
562
419
|
}
|
|
563
420
|
}
|
|
564
421
|
|
|
565
|
-
// Get file metadata without downloading
|
|
566
422
|
export async function getFileMetadata(key, bucketName) {
|
|
567
423
|
try {
|
|
568
|
-
if (!key)
|
|
569
|
-
throw new Error('Key is required');
|
|
570
|
-
}
|
|
424
|
+
if (!key) throw new Error('Key is required');
|
|
571
425
|
const keyToCheck = ensureKeyHasAppPrefix(key);
|
|
572
|
-
|
|
573
426
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
574
|
-
const bucket = config.BUCKET_NAME;
|
|
575
427
|
|
|
576
|
-
const
|
|
577
|
-
Bucket:
|
|
428
|
+
const result = await client.send(new HeadObjectCommand({
|
|
429
|
+
Bucket: config.BUCKET_NAME,
|
|
578
430
|
Key: keyToCheck,
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
const result = await client.send(command);
|
|
582
|
-
const queriedAt = new Date().toISOString();
|
|
431
|
+
}));
|
|
583
432
|
|
|
584
|
-
return {
|
|
585
|
-
...result,
|
|
586
|
-
key: keyToCheck,
|
|
587
|
-
exists: true,
|
|
588
|
-
queriedAt
|
|
589
|
-
};
|
|
433
|
+
return { ...result, key: keyToCheck, exists: true, queriedAt: nowIso() };
|
|
590
434
|
} catch (error) {
|
|
591
|
-
const queriedAt =
|
|
435
|
+
const queriedAt = nowIso();
|
|
592
436
|
if (error.name === 'NotFound' || error.name === 'NoSuchKey') {
|
|
593
|
-
return {
|
|
594
|
-
key,
|
|
595
|
-
exists: false,
|
|
596
|
-
queriedAt
|
|
597
|
-
};
|
|
437
|
+
return { key, exists: false, queriedAt };
|
|
598
438
|
}
|
|
599
|
-
|
|
600
439
|
console.error(`Get metadata failed for key ${key}:`, error);
|
|
601
440
|
throw new Error(`Get metadata failed: ${error.message}`);
|
|
602
441
|
}
|
|
603
442
|
}
|
|
604
443
|
|
|
605
|
-
// Check if file exists
|
|
606
444
|
export async function fileExists(key, bucketName) {
|
|
607
445
|
try {
|
|
608
|
-
const
|
|
609
|
-
return
|
|
610
|
-
} catch
|
|
446
|
+
const meta = await getFileMetadata(key, bucketName);
|
|
447
|
+
return !!meta.exists;
|
|
448
|
+
} catch {
|
|
611
449
|
return false;
|
|
612
450
|
}
|
|
613
451
|
}
|
|
614
452
|
|
|
615
|
-
// Get file size without downloading
|
|
616
453
|
export async function getFileSize(key, bucketName) {
|
|
617
454
|
try {
|
|
618
|
-
const
|
|
619
|
-
return
|
|
620
|
-
} catch
|
|
455
|
+
const meta = await getFileMetadata(key, bucketName);
|
|
456
|
+
return meta.exists ? meta.ContentLength : null;
|
|
457
|
+
} catch {
|
|
621
458
|
return null;
|
|
622
459
|
}
|
|
623
460
|
}
|
|
624
461
|
|
|
625
462
|
// ---------------------------------------------------------------------------
|
|
626
|
-
//
|
|
463
|
+
// Multipart Upload Helpers
|
|
627
464
|
// ---------------------------------------------------------------------------
|
|
628
465
|
|
|
629
466
|
export async function createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}, bucketName) {
|
|
630
467
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
631
468
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
632
|
-
const
|
|
633
|
-
const command = new CreateMultipartUploadCommand({
|
|
469
|
+
const result = await client.send(new CreateMultipartUploadCommand({
|
|
634
470
|
Bucket: config.BUCKET_NAME,
|
|
635
471
|
Key: keyToUse,
|
|
636
472
|
ContentType: contentType,
|
|
637
473
|
Metadata: {
|
|
638
474
|
'upload-source': 'web-portal',
|
|
639
|
-
'uploaded-at':
|
|
475
|
+
'uploaded-at': nowIso(),
|
|
640
476
|
...metadata
|
|
641
477
|
}
|
|
642
|
-
});
|
|
643
|
-
const result = await client.send(command);
|
|
478
|
+
}));
|
|
644
479
|
return { uploadId: result.UploadId, key: keyToUse };
|
|
645
480
|
}
|
|
646
481
|
|
|
647
482
|
export async function uploadPart(key, uploadId, partNumber, buffer, bucketName) {
|
|
648
483
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
649
484
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
650
|
-
const
|
|
485
|
+
const result = await client.send(new UploadPartCommand({
|
|
651
486
|
Bucket: config.BUCKET_NAME,
|
|
652
487
|
Key: keyToUse,
|
|
653
488
|
UploadId: uploadId,
|
|
654
489
|
PartNumber: partNumber,
|
|
655
490
|
Body: buffer,
|
|
656
491
|
ContentLength: buffer.length
|
|
657
|
-
});
|
|
658
|
-
const result = await client.send(command);
|
|
492
|
+
}));
|
|
659
493
|
return { ETag: result.ETag, partNumber };
|
|
660
494
|
}
|
|
661
495
|
|
|
@@ -663,59 +497,52 @@ export async function completeMultipartUpload(key, uploadId, parts, bucketName)
|
|
|
663
497
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
664
498
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
665
499
|
const sorted = [...parts].sort((a, b) => a.partNumber - b.partNumber);
|
|
666
|
-
|
|
500
|
+
await client.send(new CompleteMultipartUploadCommand({
|
|
667
501
|
Bucket: config.BUCKET_NAME,
|
|
668
502
|
Key: keyToUse,
|
|
669
503
|
UploadId: uploadId,
|
|
670
504
|
MultipartUpload: {
|
|
671
505
|
Parts: sorted.map(p => ({ PartNumber: p.partNumber, ETag: p.ETag }))
|
|
672
506
|
}
|
|
673
|
-
});
|
|
674
|
-
await client.send(command);
|
|
507
|
+
}));
|
|
675
508
|
return { key: keyToUse };
|
|
676
509
|
}
|
|
677
510
|
|
|
678
511
|
export async function abortMultipartUpload(key, uploadId, bucketName) {
|
|
679
512
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
680
513
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
681
|
-
|
|
514
|
+
await client.send(new AbortMultipartUploadCommand({
|
|
682
515
|
Bucket: config.BUCKET_NAME,
|
|
683
516
|
Key: keyToUse,
|
|
684
517
|
UploadId: uploadId
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
return { key: keyToUse, abortedAt: new Date().toISOString() };
|
|
518
|
+
}));
|
|
519
|
+
return { key: keyToUse, abortedAt: nowIso() };
|
|
688
520
|
}
|
|
689
521
|
|
|
690
|
-
// Generate signed URL for temporary access
|
|
691
522
|
export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, bucketName) {
|
|
692
523
|
try {
|
|
693
524
|
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
694
525
|
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
695
|
-
|
|
696
|
-
let command;
|
|
697
526
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
698
|
-
const
|
|
699
|
-
switch (operation) {
|
|
700
|
-
case 'getObject':
|
|
701
|
-
command = new GetObjectCommand({ Bucket: resolvedBucketName, Key: keyToUse });
|
|
702
|
-
break;
|
|
703
|
-
case 'putObject':
|
|
704
|
-
command = new PutObjectCommand({ Bucket: resolvedBucketName, Key: keyToUse });
|
|
705
|
-
break;
|
|
706
|
-
default:
|
|
707
|
-
throw new Error(`Unsupported operation: ${operation}`);
|
|
708
|
-
}
|
|
527
|
+
const bucket = config.BUCKET_NAME;
|
|
709
528
|
|
|
710
|
-
const
|
|
529
|
+
const command = operation === 'getObject'
|
|
530
|
+
? new GetObjectCommand({ Bucket: bucket, Key: keyToUse })
|
|
531
|
+
: operation === 'putObject'
|
|
532
|
+
? new PutObjectCommand({ Bucket: bucket, Key: keyToUse })
|
|
533
|
+
: null;
|
|
534
|
+
|
|
535
|
+
if (!command) throw new Error(`Unsupported operation: ${operation}`);
|
|
711
536
|
|
|
537
|
+
const signedUrl = await getSignedUrl(client, command, { expiresIn });
|
|
538
|
+
const now = Date.now();
|
|
712
539
|
return {
|
|
713
540
|
url: signedUrl,
|
|
714
541
|
key,
|
|
715
542
|
operation,
|
|
716
543
|
expiresIn,
|
|
717
|
-
expiresAt: new Date(
|
|
718
|
-
generatedAt: new Date().toISOString()
|
|
544
|
+
expiresAt: new Date(now + expiresIn * 1000).toISOString(),
|
|
545
|
+
generatedAt: new Date(now).toISOString()
|
|
719
546
|
};
|
|
720
547
|
} catch (error) {
|
|
721
548
|
console.error(`Generate signed URL failed for key ${key}:`, error);
|
|
@@ -723,21 +550,14 @@ export async function generateSignedUrl(key, operation = 'getObject', expiresIn
|
|
|
723
550
|
}
|
|
724
551
|
}
|
|
725
552
|
|
|
726
|
-
// ---------------------------------------------------------------------------
|
|
727
|
-
// Multipart Upload Cleanup
|
|
728
|
-
// ---------------------------------------------------------------------------
|
|
729
|
-
|
|
730
553
|
export async function listIncompleteMultipartUploads(prefix = '', bucketName) {
|
|
731
554
|
try {
|
|
732
555
|
const effectivePrefix = ensurePrefix(prefix || '');
|
|
733
556
|
const { client, config } = getBucketClientAndConfig(bucketName);
|
|
734
|
-
|
|
735
|
-
const command = new ListMultipartUploadsCommand({
|
|
557
|
+
const result = await client.send(new ListMultipartUploadsCommand({
|
|
736
558
|
Bucket: config.BUCKET_NAME,
|
|
737
559
|
Prefix: effectivePrefix
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
const result = await client.send(command);
|
|
560
|
+
}));
|
|
741
561
|
return result.Uploads || [];
|
|
742
562
|
} catch (error) {
|
|
743
563
|
console.error('Failed to list incomplete multipart uploads:', error);
|
|
@@ -748,7 +568,6 @@ export async function listIncompleteMultipartUploads(prefix = '', bucketName) {
|
|
|
748
568
|
export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefix = '', bucketName) {
|
|
749
569
|
try {
|
|
750
570
|
const uploads = await listIncompleteMultipartUploads(prefix, bucketName);
|
|
751
|
-
|
|
752
571
|
if (!uploads.length) {
|
|
753
572
|
debugS3('No incomplete multipart uploads found');
|
|
754
573
|
return { abortedCount: 0, uploads: [] };
|
|
@@ -757,37 +576,26 @@ export async function cleanupIncompleteMultipartUploads(olderThanDays = 7, prefi
|
|
|
757
576
|
const cutoffDate = new Date();
|
|
758
577
|
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
|
|
759
578
|
|
|
760
|
-
const uploadsToAbort = uploads.filter(
|
|
761
|
-
return upload.Initiated && new Date(upload.Initiated) < cutoffDate;
|
|
762
|
-
});
|
|
763
|
-
|
|
579
|
+
const uploadsToAbort = uploads.filter(u => u.Initiated && new Date(u.Initiated) < cutoffDate);
|
|
764
580
|
if (!uploadsToAbort.length) {
|
|
765
581
|
debugS3('No incomplete uploads older than %s days', olderThanDays);
|
|
766
582
|
return { abortedCount: 0, uploads: [] };
|
|
767
583
|
}
|
|
768
584
|
|
|
769
585
|
debugS3('Found %s incomplete uploads to clean up', uploadsToAbort.length);
|
|
770
|
-
|
|
771
586
|
const aborted = [];
|
|
587
|
+
|
|
772
588
|
for (const upload of uploadsToAbort) {
|
|
773
589
|
try {
|
|
774
590
|
await abortMultipartUpload(upload.Key, upload.UploadId, bucketName);
|
|
775
|
-
aborted.push({
|
|
776
|
-
key: upload.Key,
|
|
777
|
-
uploadId: upload.UploadId,
|
|
778
|
-
initiated: upload.Initiated
|
|
779
|
-
});
|
|
591
|
+
aborted.push({ key: upload.Key, uploadId: upload.UploadId, initiated: upload.Initiated });
|
|
780
592
|
debugS3('Aborted incomplete upload: %s (initiated: %s)', upload.Key, upload.Initiated);
|
|
781
593
|
} catch (err) {
|
|
782
594
|
console.error(`[mbkbucket] Failed to abort upload ${upload.Key}:`, err);
|
|
783
595
|
}
|
|
784
596
|
}
|
|
785
597
|
|
|
786
|
-
return {
|
|
787
|
-
abortedCount: aborted.length,
|
|
788
|
-
uploads: aborted,
|
|
789
|
-
cleanedAt: new Date().toISOString()
|
|
790
|
-
};
|
|
598
|
+
return { abortedCount: aborted.length, uploads: aborted, cleanedAt: nowIso() };
|
|
791
599
|
} catch (error) {
|
|
792
600
|
console.error('Cleanup of incomplete multipart uploads failed:', error);
|
|
793
601
|
throw new Error(`Cleanup failed: ${error.message}`);
|