mbkbucket 1.0.2 → 1.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 +8 -1
- package/README.md +7 -1
- package/index.d.ts +235 -4
- package/index.js +193 -4
- package/lib/bucket.js +646 -0
- package/lib/s3.js +504 -0
- package/package.json +32 -20
- package/views/bucket.handlebars +226 -0
- package/views/bucketadmincontent.handlebars +2933 -0
package/lib/s3.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
import { mbkautheVar } from "mbkauthe";
|
|
4
|
+
|
|
5
|
+
dotenv.config();
|
|
6
|
+
|
|
7
|
+
function parseJsonEnv(envVar, fallback = {}) {
|
|
8
|
+
if (!envVar) return fallback;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(envVar);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
// Log a concise message including the error and a short excerpt of the env value when available
|
|
13
|
+
console.error(`❌ Error parsing JSON for envVar: ${envVar ? String(envVar).slice(0, 200) : 'undefined'} - ${e.message}`);
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const r2BucketConfig = parseJsonEnv(process.env.R2_Bucket, {});
|
|
19
|
+
const e2BucketConfig = parseJsonEnv(process.env.E2_Bucket, {});
|
|
20
|
+
const e2_1BucketConfig = parseJsonEnv(process.env.E2_1_Bucket, {});
|
|
21
|
+
|
|
22
|
+
// Function to get bucket config by name or from single configured env var key via mbkautheVar.bucket
|
|
23
|
+
export function getBucketConfig(bucketName = 'r2') {
|
|
24
|
+
// If a single configured bucket is set (the value is the ENV VAR name that contains the JSON config)
|
|
25
|
+
const configuredEnvKey = (mbkautheVar && mbkautheVar.bucket);
|
|
26
|
+
if (configuredEnvKey) {
|
|
27
|
+
const envVal = process.env[configuredEnvKey];
|
|
28
|
+
if (!envVal) throw new Error(`Configured bucket env var '${configuredEnvKey}' is not set`);
|
|
29
|
+
const parsed = parseJsonEnv(envVal, null);
|
|
30
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
31
|
+
throw new Error(`Configured bucket env var '${configuredEnvKey}' contains invalid JSON`);
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (bucketName === 'e2') return e2BucketConfig;
|
|
37
|
+
if (bucketName === 'e2_1') return e2_1BucketConfig;
|
|
38
|
+
return r2BucketConfig;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Default client uses getBucketClient() which respects the configured bucket
|
|
42
|
+
export const bucketClient = getBucketClient();
|
|
43
|
+
|
|
44
|
+
// Helper: get configured APP_NAME; prefer mbkautheVar, then env var
|
|
45
|
+
export function getAppName() {
|
|
46
|
+
const app = (mbkautheVar && mbkautheVar.APP_NAME);
|
|
47
|
+
return (typeof app === 'string' && app.trim()) ? app.trim() : '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function ensureKeyHasAppPrefix(key = '') {
|
|
51
|
+
const app = getAppName();
|
|
52
|
+
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
53
|
+
if (!key) throw new Error('Key is required');
|
|
54
|
+
let cleaned = String(key).replace(/^\/+/, '');
|
|
55
|
+
// Collapse repeated app prefixes (app/app/... -> app/...)
|
|
56
|
+
const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
|
|
57
|
+
if (!cleaned.startsWith(`${app}/`)) {
|
|
58
|
+
cleaned = `${app}/${cleaned}`;
|
|
59
|
+
} else {
|
|
60
|
+
cleaned = cleaned.replace(appPrefixRegex, `${app}/`);
|
|
61
|
+
}
|
|
62
|
+
return cleaned;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
export function ensurePrefix(prefix = '') {
|
|
67
|
+
const app = getAppName();
|
|
68
|
+
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
69
|
+
let p = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/g, '');
|
|
70
|
+
if (!p) return `${app}`;
|
|
71
|
+
// If prefix already equals app or already starts with app/, normalize and return single app prefix
|
|
72
|
+
if (p === app) return app;
|
|
73
|
+
if (p.startsWith(`${app}/`)) {
|
|
74
|
+
// collapse repeated app/ sequences
|
|
75
|
+
const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
|
|
76
|
+
return p.replace(appPrefixRegex, `${app}/`);
|
|
77
|
+
}
|
|
78
|
+
return `${app}/${p}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Function to get S3 client for specific bucket
|
|
82
|
+
export function getBucketClient(bucketName = 'r2') {
|
|
83
|
+
const config = getBucketConfig(bucketName);
|
|
84
|
+
|
|
85
|
+
return new S3Client({
|
|
86
|
+
region: config.region || 'auto',
|
|
87
|
+
endpoint: config.ENDPOINT,
|
|
88
|
+
credentials: {
|
|
89
|
+
accessKeyId: config.ACCESS_KEY_ID,
|
|
90
|
+
secretAccessKey: config.SECRET_ACCESS_KEY,
|
|
91
|
+
},
|
|
92
|
+
maxAttempts: 3,
|
|
93
|
+
retryMode: 'adaptive',
|
|
94
|
+
requestTimeout: 120000,
|
|
95
|
+
requestHandler: {
|
|
96
|
+
connectionTimeout: 5000,
|
|
97
|
+
socketTimeout: 120000,
|
|
98
|
+
maxSockets: 50,
|
|
99
|
+
keepAlive: true,
|
|
100
|
+
keepAliveMsecs: 1000,
|
|
101
|
+
},
|
|
102
|
+
useAccelerateEndpoint: false,
|
|
103
|
+
forcePathStyle: true,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Health check for R2 connection (moved up for startup test)
|
|
108
|
+
async function checkR2Health() {
|
|
109
|
+
try {
|
|
110
|
+
const startTime = Date.now();
|
|
111
|
+
|
|
112
|
+
const config = getBucketConfig();
|
|
113
|
+
const bucketName = config.BUCKET_NAME;
|
|
114
|
+
|
|
115
|
+
// Try to list objects with minimal result
|
|
116
|
+
const command = new ListObjectsV2Command({
|
|
117
|
+
Bucket: bucketName,
|
|
118
|
+
MaxKeys: 1,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const client = getBucketClient();
|
|
122
|
+
const result = await client.send(command);
|
|
123
|
+
const responseTime = Date.now() - startTime;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
status: 'healthy',
|
|
127
|
+
responseTime,
|
|
128
|
+
bucket: bucketName,
|
|
129
|
+
region: config.region || 'auto',
|
|
130
|
+
checkedAt: new Date().toISOString()
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return {
|
|
134
|
+
status: 'unhealthy',
|
|
135
|
+
error: error.message,
|
|
136
|
+
bucket: (function(){try{return getBucketConfig().BUCKET_NAME}catch(e){return 'unknown'}})(),
|
|
137
|
+
checkedAt: new Date().toISOString()
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Quick diagnostics at startup
|
|
143
|
+
(async () => {
|
|
144
|
+
try {
|
|
145
|
+
console.log('[mbkbucket] mbkautheVar snapshot:', { APP_NAME: mbkautheVar?.APP_NAME, bucket: mbkautheVar?.bucket });
|
|
146
|
+
const health = await checkR2Health();
|
|
147
|
+
if (health.status === 'healthy') {
|
|
148
|
+
console.log(`[mbkbucket] Connected to bucket: ${health.bucket} (${health.responseTime}ms)`);
|
|
149
|
+
} else {
|
|
150
|
+
console.error(`[mbkbucket] R2 connection failed:`, health.error);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error("[mbkbucket] R2 connection test error:", err.message);
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
|
|
157
|
+
// Upload file with enhanced features
|
|
158
|
+
export async function uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
159
|
+
try {
|
|
160
|
+
// Validate inputs
|
|
161
|
+
if (!key || !fileBuffer) {
|
|
162
|
+
throw new Error('Key and file buffer are required');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Enforce app prefix (never allow root keys)
|
|
166
|
+
const keyToUpload = ensureKeyHasAppPrefix(key);
|
|
167
|
+
|
|
168
|
+
const {
|
|
169
|
+
metadata = {},
|
|
170
|
+
cacheControl = 'public, max-age=31536000', // 1 year default
|
|
171
|
+
storageClass = 'STANDARD',
|
|
172
|
+
serverSideEncryption = 'AES256',
|
|
173
|
+
bucketName = 'r2'
|
|
174
|
+
} = options;
|
|
175
|
+
|
|
176
|
+
const client = getBucketClient(bucketName);
|
|
177
|
+
const config = getBucketConfig(bucketName);
|
|
178
|
+
const bucket = config.BUCKET_NAME;
|
|
179
|
+
|
|
180
|
+
console.log(`[mbkbucket] Uploading file to bucket=${bucket} key=${keyToUpload}`);
|
|
181
|
+
|
|
182
|
+
// Add default metadata (R2 supports metadata)
|
|
183
|
+
const defaultMetadata = {
|
|
184
|
+
'uploaded-at': new Date().toISOString(),
|
|
185
|
+
'file-size': fileBuffer.length.toString(),
|
|
186
|
+
'upload-source': 'web-portal',
|
|
187
|
+
...metadata
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const command = new PutObjectCommand({
|
|
191
|
+
Bucket: bucket,
|
|
192
|
+
Key: keyToUpload,
|
|
193
|
+
Body: fileBuffer,
|
|
194
|
+
ContentType: contentType,
|
|
195
|
+
CacheControl: cacheControl,
|
|
196
|
+
Metadata: defaultMetadata,
|
|
197
|
+
ServerSideEncryption: serverSideEncryption,
|
|
198
|
+
StorageClass: storageClass,
|
|
199
|
+
// Note: R2 doesn't support object tagging, so we remove the Tagging parameter
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const result = await client.send(command);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
...result,
|
|
206
|
+
fileSize: fileBuffer.length,
|
|
207
|
+
key: keyToUpload,
|
|
208
|
+
contentType,
|
|
209
|
+
uploadedAt: new Date().toISOString()
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error(`Upload failed for key ${key}:`, error);
|
|
213
|
+
throw new Error(`Upload failed: ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Download file with enhanced features
|
|
218
|
+
export async function downloadFile(key, options = {}) {
|
|
219
|
+
try {
|
|
220
|
+
if (!key) {
|
|
221
|
+
throw new Error('Key is required');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Ensure key is app-prefixed
|
|
225
|
+
const keyToDownload = ensureKeyHasAppPrefix(key);
|
|
226
|
+
|
|
227
|
+
const {
|
|
228
|
+
range = null,
|
|
229
|
+
ifNoneMatch = null,
|
|
230
|
+
ifModifiedSince = null,
|
|
231
|
+
responseCacheControl = null,
|
|
232
|
+
responseContentType = null,
|
|
233
|
+
bucketName = 'r2'
|
|
234
|
+
} = options;
|
|
235
|
+
|
|
236
|
+
const client = getBucketClient(bucketName);
|
|
237
|
+
const config = getBucketConfig(bucketName);
|
|
238
|
+
const bucket = config.BUCKET_NAME;
|
|
239
|
+
|
|
240
|
+
// Log range requests for debugging
|
|
241
|
+
if (range) {
|
|
242
|
+
console.log(`[mbkbucket] Range request for ${keyToDownload}: ${range}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const command = new GetObjectCommand({
|
|
246
|
+
Bucket: bucket,
|
|
247
|
+
Key: keyToDownload,
|
|
248
|
+
...(range && { Range: range }),
|
|
249
|
+
...(ifNoneMatch && { IfNoneMatch: ifNoneMatch }),
|
|
250
|
+
...(ifModifiedSince && { IfModifiedSince: ifModifiedSince }),
|
|
251
|
+
...(responseCacheControl && { ResponseCacheControl: responseCacheControl }),
|
|
252
|
+
...(responseContentType && { ResponseContentType: responseContentType }),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const startTime = Date.now();
|
|
256
|
+
const result = await client.send(command);
|
|
257
|
+
const downloadTime = Date.now() - startTime;
|
|
258
|
+
|
|
259
|
+
// Log performance for large files
|
|
260
|
+
if (result.ContentLength > 10 * 1024 * 1024) {
|
|
261
|
+
console.log(`[mbkbucket] Large file download: ${keyToDownload}, Size: ${result.ContentLength} bytes, Time: ${downloadTime}ms`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
...result,
|
|
266
|
+
key: keyToDownload,
|
|
267
|
+
downloadedAt: new Date().toISOString(),
|
|
268
|
+
downloadTime
|
|
269
|
+
};
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error(`Download failed for key ${key}:`, error);
|
|
272
|
+
|
|
273
|
+
// Some S3-compatible endpoints return 304 Not Modified as a response to conditional GETs.
|
|
274
|
+
// The SDK surface may throw an error with metadata httpStatusCode=304 — treat this as a not-modified condition.
|
|
275
|
+
if (error && error.$metadata && error.$metadata.httpStatusCode === 304) {
|
|
276
|
+
return { notModified: true, key: keyToDownload };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Handle specific S3 errors
|
|
280
|
+
if (error.name === 'NoSuchKey') {
|
|
281
|
+
throw new Error(`File not found: ${key}`);
|
|
282
|
+
} else if (error.name === 'AccessDenied') {
|
|
283
|
+
throw new Error(`Access denied for file: ${key}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
throw new Error(`Download failed: ${error.message}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Delete file with enhanced error handling
|
|
291
|
+
export async function deleteFile(key, bucketName = 'r2') {
|
|
292
|
+
try {
|
|
293
|
+
if (!key) {
|
|
294
|
+
throw new Error('Key is required');
|
|
295
|
+
}
|
|
296
|
+
const keyToDelete = ensureKeyHasAppPrefix(key);
|
|
297
|
+
|
|
298
|
+
const client = getBucketClient(bucketName);
|
|
299
|
+
const config = getBucketConfig(bucketName);
|
|
300
|
+
const bucket = config.BUCKET_NAME;
|
|
301
|
+
|
|
302
|
+
const command = new DeleteObjectCommand({
|
|
303
|
+
Bucket: bucket,
|
|
304
|
+
Key: keyToDelete,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const result = await client.send(command);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
...result,
|
|
311
|
+
key: keyToDelete,
|
|
312
|
+
deletedAt: new Date().toISOString()
|
|
313
|
+
};
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error(`Delete failed:`, error);
|
|
316
|
+
throw new Error(`Delete failed: ${error.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Batch delete multiple files
|
|
321
|
+
export async function deleteFiles(keys, bucketName = 'r2') {
|
|
322
|
+
try {
|
|
323
|
+
if (!keys || !Array.isArray(keys) || keys.length === 0) {
|
|
324
|
+
throw new Error('Keys array is required and must not be empty');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const client = getBucketClient(bucketName);
|
|
328
|
+
const config = getBucketConfig(bucketName);
|
|
329
|
+
const bucket = config.BUCKET_NAME;
|
|
330
|
+
|
|
331
|
+
// S3 allows max 1000 objects per delete request
|
|
332
|
+
const maxBatchSize = 1000;
|
|
333
|
+
const results = [];
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i < keys.length; i += maxBatchSize) {
|
|
336
|
+
const batch = keys.slice(i, i + maxBatchSize).map(k => ({ Key: ensureKeyHasAppPrefix(k) }));
|
|
337
|
+
|
|
338
|
+
const command = new DeleteObjectsCommand({
|
|
339
|
+
Bucket: bucket,
|
|
340
|
+
Delete: {
|
|
341
|
+
Objects: batch,
|
|
342
|
+
Quiet: false
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const result = await client.send(command);
|
|
347
|
+
results.push(result);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
results,
|
|
352
|
+
deletedCount: results.reduce((acc, result) => acc + (result.Deleted?.length || 0), 0),
|
|
353
|
+
errors: results.reduce((acc, result) => acc.concat(result.Errors || []), []),
|
|
354
|
+
deletedAt: new Date().toISOString()
|
|
355
|
+
};
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error('Batch delete failed:', error);
|
|
358
|
+
throw new Error(`Batch delete failed: ${error.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// List files with enhanced pagination and filtering
|
|
363
|
+
export async function listfiles(prefix = '', options = {}) {
|
|
364
|
+
try {
|
|
365
|
+
const {
|
|
366
|
+
maxKeys = 1000,
|
|
367
|
+
continuationToken = null,
|
|
368
|
+
delimiter = null,
|
|
369
|
+
fetchOwner = false,
|
|
370
|
+
startAfter = null,
|
|
371
|
+
bucketName = 'r2'
|
|
372
|
+
} = options;
|
|
373
|
+
|
|
374
|
+
// Enforce non-root prefix: default to APP_NAME and normalize
|
|
375
|
+
const effectivePrefix = ensurePrefix(prefix);
|
|
376
|
+
|
|
377
|
+
const client = getBucketClient(bucketName);
|
|
378
|
+
const config = getBucketConfig(bucketName);
|
|
379
|
+
const bucket = config.BUCKET_NAME;
|
|
380
|
+
|
|
381
|
+
console.log(`[mbkbucket] Listing objects in bucket=${bucket} prefix=${effectivePrefix}`);
|
|
382
|
+
|
|
383
|
+
const command = new ListObjectsV2Command({
|
|
384
|
+
Bucket: bucket,
|
|
385
|
+
Prefix: effectivePrefix,
|
|
386
|
+
MaxKeys: maxKeys,
|
|
387
|
+
...(continuationToken && { ContinuationToken: continuationToken }),
|
|
388
|
+
...(delimiter && { Delimiter: delimiter }),
|
|
389
|
+
...(fetchOwner && { FetchOwner: fetchOwner }),
|
|
390
|
+
...(startAfter && { StartAfter: startAfter }),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const result = await client.send(command);
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
...result,
|
|
397
|
+
requestedAt: new Date().toISOString(),
|
|
398
|
+
totalFiles: result.KeyCount || 0,
|
|
399
|
+
hasMore: result.IsTruncated || false,
|
|
400
|
+
nextToken: result.NextContinuationToken || null
|
|
401
|
+
};
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error('List files failed:', error);
|
|
404
|
+
throw new Error(`List files failed: ${error.message}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Get file metadata without downloading
|
|
409
|
+
export async function getFileMetadata(key, bucketName = 'r2') {
|
|
410
|
+
try {
|
|
411
|
+
if (!key) {
|
|
412
|
+
throw new Error('Key is required');
|
|
413
|
+
}
|
|
414
|
+
const keyToCheck = ensureKeyHasAppPrefix(key);
|
|
415
|
+
|
|
416
|
+
const client = getBucketClient(bucketName);
|
|
417
|
+
const config = getBucketConfig(bucketName);
|
|
418
|
+
const bucket = config.BUCKET_NAME;
|
|
419
|
+
|
|
420
|
+
const command = new HeadObjectCommand({
|
|
421
|
+
Bucket: bucket,
|
|
422
|
+
Key: keyToCheck,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const result = await client.send(command);
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
...result,
|
|
429
|
+
key: keyToCheck,
|
|
430
|
+
exists: true,
|
|
431
|
+
queriedAt: new Date().toISOString()
|
|
432
|
+
};
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error.name === 'NotFound' || error.name === 'NoSuchKey') {
|
|
435
|
+
return {
|
|
436
|
+
key,
|
|
437
|
+
exists: false,
|
|
438
|
+
queriedAt: new Date().toISOString()
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.error(`Get metadata failed for key ${key}:`, error);
|
|
443
|
+
throw new Error(`Get metadata failed: ${error.message}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check if file exists
|
|
448
|
+
export async function fileExists(key, bucketName = 'r2') {
|
|
449
|
+
try {
|
|
450
|
+
const metadata = await getFileMetadata(key, bucketName);
|
|
451
|
+
return metadata.exists;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Get file size without downloading
|
|
458
|
+
export async function getFileSize(key, bucketName = 'r2') {
|
|
459
|
+
try {
|
|
460
|
+
const metadata = await getFileMetadata(key, bucketName);
|
|
461
|
+
return metadata.exists ? metadata.ContentLength : null;
|
|
462
|
+
} catch (error) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Export health check for external use
|
|
468
|
+
export { checkR2Health };
|
|
469
|
+
|
|
470
|
+
// Generate signed URL for temporary access
|
|
471
|
+
export async function generateSignedUrl(key, operation = 'getObject', expiresIn = 3600) {
|
|
472
|
+
try {
|
|
473
|
+
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
474
|
+
const keyToUse = ensureKeyHasAppPrefix(key);
|
|
475
|
+
|
|
476
|
+
let command;
|
|
477
|
+
const cfg = getBucketConfig();
|
|
478
|
+
const bucketName = cfg.BUCKET_NAME;
|
|
479
|
+
switch (operation) {
|
|
480
|
+
case 'getObject':
|
|
481
|
+
command = new GetObjectCommand({ Bucket: bucketName, Key: keyToUse });
|
|
482
|
+
break;
|
|
483
|
+
case 'putObject':
|
|
484
|
+
command = new PutObjectCommand({ Bucket: bucketName, Key: keyToUse });
|
|
485
|
+
break;
|
|
486
|
+
default:
|
|
487
|
+
throw new Error(`Unsupported operation: ${operation}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const signedUrl = await getSignedUrl(bucketClient, command, { expiresIn });
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
url: signedUrl,
|
|
494
|
+
key,
|
|
495
|
+
operation,
|
|
496
|
+
expiresIn,
|
|
497
|
+
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
|
|
498
|
+
generatedAt: new Date().toISOString()
|
|
499
|
+
};
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.error(`Generate signed URL failed for key ${key}:`, error);
|
|
502
|
+
throw new Error(`Generate signed URL failed: ${error.message}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
package/package.json
CHANGED
|
@@ -1,39 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkbucket",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Bucket manager for
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "S3 Bucket manager for mbktech.org apps",
|
|
5
|
+
"author": "Muhammad Bin Khalid",
|
|
6
|
+
"homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
|
|
7
|
+
"license": "GPL-2.0",
|
|
5
8
|
"main": "index.js",
|
|
6
9
|
"type": "module",
|
|
10
|
+
"types": "index.d.ts",
|
|
7
11
|
"scripts": {
|
|
8
|
-
"dev": "nodemon index.js",
|
|
12
|
+
"dev": "cross-env test=dev nodemon index.js",
|
|
9
13
|
"test": "echo \\\"Error: no test specified\\\" && exit 1"
|
|
10
14
|
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@aws-sdk/client-s3": "^3.968.0",
|
|
17
|
+
"compression": "^1.8.0",
|
|
18
|
+
"dotenv": "^16.6.1",
|
|
19
|
+
"express": "^4.22.1",
|
|
20
|
+
"express-handlebars": "^8.0.3",
|
|
21
|
+
"express-rate-limit": "^7.0.1",
|
|
22
|
+
"mbkauthe": "^4.3.0",
|
|
23
|
+
"multer": "^2.0.2",
|
|
24
|
+
"pg": "^8.16.3"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"nodemon": "^2.0.22",
|
|
28
|
+
"cross-env": "^7.0.3"
|
|
29
|
+
},
|
|
11
30
|
"repository": {
|
|
12
31
|
"type": "git",
|
|
13
32
|
"url": "git+https://github.com/MIbnEKhalid/mbkbucket.git"
|
|
14
33
|
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/MIbnEKhalid/mbkbucket/issues"
|
|
36
|
+
},
|
|
15
37
|
"keywords": [
|
|
38
|
+
"mbkbucket",
|
|
16
39
|
"s3-bucket",
|
|
17
40
|
"s3",
|
|
18
41
|
"bucket",
|
|
19
42
|
"r2-bucket",
|
|
20
43
|
"bucket-manager",
|
|
21
|
-
"manager",
|
|
22
44
|
"aws-sdk",
|
|
23
45
|
"cloudflare-r2",
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
"homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"mbkauthe": "latest",
|
|
34
|
-
"module": "^2.0.0"
|
|
35
|
-
},
|
|
36
|
-
"devDependencies": {
|
|
37
|
-
"nodemon": "^3.1.11"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
46
|
+
"idrivee2",
|
|
47
|
+
"storage",
|
|
48
|
+
"mbkauthe",
|
|
49
|
+
"mbktech.org"
|
|
50
|
+
]
|
|
51
|
+
}
|