mbkbucket 4.0.0 → 4.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 +16 -15
- package/README.md +613 -561
- package/index.js +36 -33
- package/lib/cli.js +239 -239
- package/lib/public/bucketadmin.css +2042 -2042
- package/lib/public/bucketadmin.js +1844 -1622
- package/lib/public/mbkbucket-helper-demo.html +0 -58
- package/lib/public/mbkbucket-helper.js +1 -9
- package/lib/src/config/validation.js +91 -86
- package/lib/src/controllers/api.controller.js +486 -459
- package/lib/src/controllers/dashboard.controller.js +69 -69
- package/lib/src/controllers/info.controller.js +13 -13
- package/lib/src/controllers/view.controller.js +234 -233
- package/lib/src/core/metrics.js +134 -0
- package/lib/src/core/storage/capabilities.js +58 -0
- package/lib/src/core/storage/errors.js +0 -5
- package/lib/src/core/storage/gdrive-auth.js +0 -36
- package/lib/src/core/storage/gdrive-provider.js +101 -43
- package/lib/src/core/storage/index.js +4 -1
- package/lib/src/core/storage/local-provider.js +507 -0
- package/lib/src/core/storage/models.js +90 -50
- package/lib/src/core/storage/path-strategy.js +14 -1
- package/lib/src/core/storage/s3-provider.js +17 -37
- package/lib/src/core/storage/storage-manager.js +70 -50
- package/lib/src/core/storage/storage-provider.js +49 -120
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/routes/api.routes.js +4 -1
- package/lib/src/routes/index.js +60 -60
- package/lib/src/routes/view.routes.js +28 -28
- package/lib/src/services/s3.service.js +42 -42
- package/lib/src/services/storage.service.js +311 -213
- package/lib/src/utils/cleanup-scheduler.js +91 -0
- package/lib/src/utils/errors.js +56 -56
- package/lib/src/utils/helpers.js +6 -6
- package/package.json +3 -5
- package/views/bucket.hbs +0 -5
- package/views/bucketadmin_move_modal.hbs +35 -35
- package/views/bucketadmin_preview_modal.hbs +108 -108
- package/views/bucketadmincontent.hbs +193 -193
- package/views/mbkbucket_info.hbs +134 -134
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { S3Client, ListObjectsV2Command, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, HeadObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, ListMultipartUploadsCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
|
|
8
8
|
import { StorageProvider } from './storage-provider.js';
|
|
9
|
+
import { StorageCapabilities } from './capabilities.js';
|
|
9
10
|
import { StorageFile, StorageFolder, StorageListResult } from './models.js';
|
|
10
11
|
import { StorageNotFoundError, StorageConflictError, StorageAccessDeniedError, StorageValidationError, StorageError } from './errors.js';
|
|
11
12
|
import { createLogger } from "#logger";
|
|
@@ -14,10 +15,6 @@ import { nowIso, trimSlashes } from "#helpers";
|
|
|
14
15
|
const debugS3 = createLogger('s3-provider');
|
|
15
16
|
|
|
16
17
|
export class S3StorageProvider extends StorageProvider {
|
|
17
|
-
/**
|
|
18
|
-
* @param {string} name - Connection name
|
|
19
|
-
* @param {object} config - S3 configuration (BUCKET_NAME, ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, region)
|
|
20
|
-
*/
|
|
21
18
|
constructor(name, config = {}) {
|
|
22
19
|
super(name, config);
|
|
23
20
|
this.bucket = config.BUCKET_NAME;
|
|
@@ -49,16 +46,19 @@ export class S3StorageProvider extends StorageProvider {
|
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
get capabilities() {
|
|
52
|
-
return {
|
|
49
|
+
return new StorageCapabilities({
|
|
53
50
|
multipart: true,
|
|
54
|
-
|
|
51
|
+
resumable: true,
|
|
55
52
|
nativeFolders: false,
|
|
56
53
|
ranges: true,
|
|
57
54
|
copy: true,
|
|
58
55
|
move: true,
|
|
59
56
|
search: false,
|
|
60
|
-
directDownloadUrl: false
|
|
61
|
-
|
|
57
|
+
directDownloadUrl: false,
|
|
58
|
+
versioning: false,
|
|
59
|
+
publicUrls: false,
|
|
60
|
+
metadata: true
|
|
61
|
+
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async checkHealth() {
|
|
@@ -279,7 +279,8 @@ export class S3StorageProvider extends StorageProvider {
|
|
|
279
279
|
size: item.Size,
|
|
280
280
|
lastModified: item.LastModified,
|
|
281
281
|
etag: item.ETag,
|
|
282
|
-
storageClass: item.StorageClass || 'STANDARD'
|
|
282
|
+
storageClass: item.StorageClass || 'STANDARD',
|
|
283
|
+
provider: 's3'
|
|
283
284
|
}));
|
|
284
285
|
|
|
285
286
|
const commonPrefixes = (result.CommonPrefixes || []).map(p => new StorageFolder(p.Prefix));
|
|
@@ -340,6 +341,7 @@ export class S3StorageProvider extends StorageProvider {
|
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
async copyFile(sourceKey, destKey, options = {}) {
|
|
344
|
+
this.assertCapability('copy', 'copyFile');
|
|
343
345
|
try {
|
|
344
346
|
const copySource = encodeURIComponent(`${this.bucket}/${sourceKey}`);
|
|
345
347
|
await this.client.send(new CopyObjectCommand({
|
|
@@ -357,39 +359,12 @@ export class S3StorageProvider extends StorageProvider {
|
|
|
357
359
|
}
|
|
358
360
|
}
|
|
359
361
|
|
|
360
|
-
async generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, _options = {}) {
|
|
361
|
-
try {
|
|
362
|
-
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
|
|
363
|
-
const command = operation === 'getObject'
|
|
364
|
-
? new GetObjectCommand({ Bucket: this.bucket, Key: key })
|
|
365
|
-
: operation === 'putObject'
|
|
366
|
-
? new PutObjectCommand({ Bucket: this.bucket, Key: key })
|
|
367
|
-
: null;
|
|
368
|
-
|
|
369
|
-
if (!command) throw new StorageValidationError(`Unsupported operation: ${operation}`);
|
|
370
|
-
|
|
371
|
-
const signedUrl = await getSignedUrl(this.client, command, { expiresIn });
|
|
372
|
-
const now = Date.now();
|
|
373
|
-
return {
|
|
374
|
-
url: signedUrl,
|
|
375
|
-
key,
|
|
376
|
-
operation,
|
|
377
|
-
expiresIn,
|
|
378
|
-
expiresAt: new Date(now + expiresIn * 1000).toISOString(),
|
|
379
|
-
generatedAt: new Date(now).toISOString()
|
|
380
|
-
};
|
|
381
|
-
} catch (error) {
|
|
382
|
-
debugS3('Generate signed URL failed for key %s: %s', key, error.message);
|
|
383
|
-
if (error instanceof StorageError) throw error;
|
|
384
|
-
throw new StorageError(`Generate signed URL failed: ${error.message}`);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
362
|
// ---------------------------------------------------------------------------
|
|
389
363
|
// S3 Multipart Upload Operations
|
|
390
364
|
// ---------------------------------------------------------------------------
|
|
391
365
|
|
|
392
366
|
async createMultipartUpload(key, contentType = 'application/octet-stream', metadata = {}) {
|
|
367
|
+
this.assertCapability('multipart', 'createMultipartUpload');
|
|
393
368
|
const result = await this.client.send(new CreateMultipartUploadCommand({
|
|
394
369
|
Bucket: this.bucket,
|
|
395
370
|
Key: key,
|
|
@@ -404,6 +379,7 @@ export class S3StorageProvider extends StorageProvider {
|
|
|
404
379
|
}
|
|
405
380
|
|
|
406
381
|
async uploadPart(key, uploadId, partNumber, buffer) {
|
|
382
|
+
this.assertCapability('multipart', 'uploadPart');
|
|
407
383
|
const result = await this.client.send(new UploadPartCommand({
|
|
408
384
|
Bucket: this.bucket,
|
|
409
385
|
Key: key,
|
|
@@ -416,6 +392,7 @@ export class S3StorageProvider extends StorageProvider {
|
|
|
416
392
|
}
|
|
417
393
|
|
|
418
394
|
async completeMultipartUpload(key, uploadId, parts) {
|
|
395
|
+
this.assertCapability('multipart', 'completeMultipartUpload');
|
|
419
396
|
const sorted = [...parts].sort((a, b) => a.partNumber - b.partNumber);
|
|
420
397
|
await this.client.send(new CompleteMultipartUploadCommand({
|
|
421
398
|
Bucket: this.bucket,
|
|
@@ -429,6 +406,7 @@ export class S3StorageProvider extends StorageProvider {
|
|
|
429
406
|
}
|
|
430
407
|
|
|
431
408
|
async abortMultipartUpload(key, uploadId) {
|
|
409
|
+
this.assertCapability('multipart', 'abortMultipartUpload');
|
|
432
410
|
await this.client.send(new AbortMultipartUploadCommand({
|
|
433
411
|
Bucket: this.bucket,
|
|
434
412
|
Key: key,
|
|
@@ -438,6 +416,7 @@ export class S3StorageProvider extends StorageProvider {
|
|
|
438
416
|
}
|
|
439
417
|
|
|
440
418
|
async listIncompleteMultipartUploads(prefix = '') {
|
|
419
|
+
this.assertCapability('multipart', 'listIncompleteMultipartUploads');
|
|
441
420
|
try {
|
|
442
421
|
const result = await this.client.send(new ListMultipartUploadsCommand({
|
|
443
422
|
Bucket: this.bucket,
|
|
@@ -452,6 +431,7 @@ export class S3StorageProvider extends StorageProvider {
|
|
|
452
431
|
}
|
|
453
432
|
|
|
454
433
|
async cleanupIncompleteMultipartUploads(olderThanDays = 7, prefix = '') {
|
|
434
|
+
this.assertCapability('multipart', 'cleanupIncompleteMultipartUploads');
|
|
455
435
|
try {
|
|
456
436
|
const uploads = await this.listIncompleteMultipartUploads(prefix);
|
|
457
437
|
if (!uploads.length) {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MBKBucket - Storage Manager & Provider Registry
|
|
2
|
+
* MBKBucket - Storage Manager & Multi-Provider Connection Registry
|
|
3
3
|
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
4
|
* Licensed under the MIT License.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { EventEmitter } from "events";
|
|
7
8
|
import dotenv from "dotenv";
|
|
8
9
|
import { mbkautheVar } from "mbkauthe";
|
|
9
10
|
import { S3StorageProvider } from "./s3-provider.js";
|
|
10
11
|
import { GoogleDriveStorageProvider } from "./gdrive-provider.js";
|
|
12
|
+
import { LocalStorageProvider } from "./local-provider.js";
|
|
11
13
|
import { StorageConfigError, StorageValidationError } from "./errors.js";
|
|
12
14
|
import { createLogger } from "#logger";
|
|
13
15
|
|
|
@@ -24,29 +26,36 @@ function parseJsonEnv(envVar, fallback = {}) {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export class StorageManager {
|
|
29
|
+
export class StorageManager extends EventEmitter {
|
|
28
30
|
constructor() {
|
|
31
|
+
super();
|
|
29
32
|
this._providers = new Map();
|
|
30
33
|
this._factories = new Map();
|
|
31
34
|
this._configs = new Map();
|
|
35
|
+
this._defaultConnectionName = null;
|
|
32
36
|
|
|
33
|
-
// Register built-in provider drivers
|
|
34
37
|
this.registerDriver('s3', (name, cfg) => new S3StorageProvider(name, cfg));
|
|
35
38
|
this.registerDriver('r2', (name, cfg) => new S3StorageProvider(name, cfg));
|
|
36
39
|
this.registerDriver('minio', (name, cfg) => new S3StorageProvider(name, cfg));
|
|
40
|
+
this.registerDriver('idrive', (name, cfg) => new S3StorageProvider(name, cfg));
|
|
37
41
|
this.registerDriver('gdrive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
|
|
38
42
|
this.registerDriver('google-drive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
|
|
39
43
|
this.registerDriver('googledrive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
|
|
44
|
+
this.registerDriver('local', (name, cfg) => new LocalStorageProvider(name, cfg));
|
|
45
|
+
this.registerDriver('fs', (name, cfg) => new LocalStorageProvider(name, cfg));
|
|
46
|
+
this.registerDriver('filesystem', (name, cfg) => new LocalStorageProvider(name, cfg));
|
|
40
47
|
|
|
41
48
|
this.reloadFromEnv();
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
* Register a custom provider driver.
|
|
46
|
-
* @param {string} type
|
|
47
|
-
* @param {(name: string, config: object) => import('./storage-provider.js').StorageProvider} factory
|
|
48
|
-
*/
|
|
51
|
+
|
|
49
52
|
registerDriver(type, factory) {
|
|
53
|
+
if (!type || typeof type !== 'string') {
|
|
54
|
+
throw new StorageValidationError("Driver type must be a non-empty string");
|
|
55
|
+
}
|
|
56
|
+
if (typeof factory !== 'function') {
|
|
57
|
+
throw new StorageValidationError("Driver factory must be a function");
|
|
58
|
+
}
|
|
50
59
|
this._factories.set(String(type).toLowerCase(), factory);
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -54,9 +63,14 @@ export class StorageManager {
|
|
|
54
63
|
this.registerDriver(type, factory);
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
hasDriver(type) {
|
|
67
|
+
return this._factories.has(String(type).toLowerCase());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getDriverTypes() {
|
|
71
|
+
return Array.from(this._factories.keys());
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
reloadFromEnv() {
|
|
61
75
|
this._providers.clear();
|
|
62
76
|
this._configs.clear();
|
|
@@ -71,31 +85,39 @@ export class StorageManager {
|
|
|
71
85
|
}
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
/**
|
|
75
|
-
* Register a named storage connection.
|
|
76
|
-
* @param {string} name
|
|
77
|
-
* @param {object} config
|
|
78
|
-
*/
|
|
79
88
|
registerConnection(name, config) {
|
|
80
|
-
const cleanName = String(name).trim();
|
|
89
|
+
const cleanName = String(name || '').trim();
|
|
81
90
|
if (!cleanName) throw new StorageValidationError("Connection name cannot be empty");
|
|
91
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
92
|
+
throw new StorageValidationError(`Connection configuration for '${cleanName}' must be an object`);
|
|
93
|
+
}
|
|
94
|
+
|
|
82
95
|
this._configs.set(cleanName, Object.freeze({ ...config }));
|
|
83
96
|
this._providers.delete(cleanName);
|
|
84
97
|
}
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
unregisterConnection(name) {
|
|
100
|
+
const cleanName = String(name || '').trim();
|
|
101
|
+
this._configs.delete(cleanName);
|
|
102
|
+
this._providers.delete(cleanName);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
hasConnection(name) {
|
|
106
|
+
return this._configs.has(String(name || '').trim());
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
getAvailableConnectionNames() {
|
|
91
110
|
return Array.from(this._configs.keys());
|
|
92
111
|
}
|
|
93
112
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
setDefaultConnectionName(name) {
|
|
114
|
+
this._defaultConnectionName = name ? String(name).trim() : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
98
117
|
getDefaultConnectionName() {
|
|
118
|
+
if (this._defaultConnectionName && this._configs.has(this._defaultConnectionName)) {
|
|
119
|
+
return this._defaultConnectionName;
|
|
120
|
+
}
|
|
99
121
|
const configuredDefault = mbkautheVar?.bucket;
|
|
100
122
|
if (configuredDefault && this._configs.has(configuredDefault)) {
|
|
101
123
|
return configuredDefault;
|
|
@@ -104,11 +126,6 @@ export class StorageManager {
|
|
|
104
126
|
return available.length > 0 ? available[0] : null;
|
|
105
127
|
}
|
|
106
128
|
|
|
107
|
-
/**
|
|
108
|
-
* Resolve a candidate connection name or fall back to default.
|
|
109
|
-
* @param {string} [name]
|
|
110
|
-
* @returns {string}
|
|
111
|
-
*/
|
|
112
129
|
resolveConnectionName(name) {
|
|
113
130
|
const raw = typeof name === 'string' ? name.trim() : name;
|
|
114
131
|
const candidate = !raw ? this.getDefaultConnectionName() : String(raw);
|
|
@@ -126,35 +143,41 @@ export class StorageManager {
|
|
|
126
143
|
return candidate;
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
/**
|
|
130
|
-
* Get connection configuration object.
|
|
131
|
-
* @param {string} [name]
|
|
132
|
-
* @returns {object}
|
|
133
|
-
*/
|
|
134
146
|
getConnectionConfig(name) {
|
|
135
147
|
const resolved = this.resolveConnectionName(name);
|
|
136
148
|
return this._configs.get(resolved);
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
/**
|
|
140
|
-
* Determine the provider type from configuration.
|
|
141
|
-
* @param {object} config
|
|
142
|
-
* @returns {string}
|
|
143
|
-
*/
|
|
144
151
|
inferProviderType(config = {}) {
|
|
145
152
|
if (config?.type) return String(config.type).toLowerCase();
|
|
146
153
|
if (config?.provider) return String(config.provider).toLowerCase();
|
|
154
|
+
if (config?.basePath || config?.rootPath || config?.directory) {
|
|
155
|
+
return 'local';
|
|
156
|
+
}
|
|
147
157
|
if (config?.client_id || config?.refresh_token || config?.service_account_key || config?.folder_id || config?.service_account_email) {
|
|
148
158
|
return 'gdrive';
|
|
149
159
|
}
|
|
150
160
|
return 's3';
|
|
151
161
|
}
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
clearCache() {
|
|
164
|
+
this._providers.clear();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async dispose() {
|
|
168
|
+
for (const [name, provider] of this._providers.entries()) {
|
|
169
|
+
try {
|
|
170
|
+
if (typeof provider.dispose === 'function') {
|
|
171
|
+
await provider.dispose();
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
debugStorage('Error disposing provider "%s": %s', name, err.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
this._providers.clear();
|
|
178
|
+
this.removeAllListeners();
|
|
179
|
+
}
|
|
180
|
+
|
|
158
181
|
getProvider(name) {
|
|
159
182
|
const resolved = this.resolveConnectionName(name);
|
|
160
183
|
if (this._providers.has(resolved)) {
|
|
@@ -172,14 +195,10 @@ export class StorageManager {
|
|
|
172
195
|
debugStorage('Instantiating provider "%s" (type: %s)', resolved, providerType);
|
|
173
196
|
const provider = factory(resolved, config);
|
|
174
197
|
this._providers.set(resolved, provider);
|
|
198
|
+
this.emit('provider:instantiated', { name: resolved, type: providerType });
|
|
175
199
|
return provider;
|
|
176
200
|
}
|
|
177
201
|
|
|
178
|
-
/**
|
|
179
|
-
* Check health of the default or specified connection.
|
|
180
|
-
* @param {string} [name]
|
|
181
|
-
* @returns {Promise<object>}
|
|
182
|
-
*/
|
|
183
202
|
async checkHealth(name) {
|
|
184
203
|
try {
|
|
185
204
|
const provider = this.getProvider(name);
|
|
@@ -201,3 +220,4 @@ export class StorageManager {
|
|
|
201
220
|
}
|
|
202
221
|
|
|
203
222
|
export const storageManager = new StorageManager();
|
|
223
|
+
|
|
@@ -5,148 +5,98 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { StorageUnsupportedOperationError } from "./errors.js";
|
|
8
|
+
import { StorageCapabilities } from "./capabilities.js";
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
* Abstract base class for storage providers (S3, Cloudflare R2, Google Drive, Azure, etc.)
|
|
11
|
-
*/
|
|
12
10
|
export class StorageProvider {
|
|
13
|
-
/**
|
|
14
|
-
* @param {string} name - Connection/Bucket name identifier
|
|
15
|
-
* @param {object} config - Provider-specific configuration
|
|
16
|
-
*/
|
|
17
11
|
constructor(name, config = {}) {
|
|
18
12
|
if (new.target === StorageProvider) {
|
|
19
13
|
throw new TypeError("Cannot construct StorageProvider instances directly");
|
|
20
14
|
}
|
|
21
|
-
this.name = name;
|
|
15
|
+
this.name = String(name);
|
|
22
16
|
this.config = Object.freeze({ ...config });
|
|
17
|
+
this.rootPrefix = config.rootPrefix || config.basePath || '';
|
|
23
18
|
}
|
|
24
19
|
|
|
25
|
-
/**
|
|
26
|
-
* Provider type identifier (e.g. 's3', 'gdrive', 'r2')
|
|
27
|
-
* @returns {string}
|
|
28
|
-
*/
|
|
29
20
|
get type() {
|
|
30
21
|
return 'generic';
|
|
31
22
|
}
|
|
32
23
|
|
|
33
|
-
/**
|
|
34
|
-
* Explicit provider capabilities.
|
|
35
|
-
* @returns {object}
|
|
36
|
-
*/
|
|
37
24
|
get capabilities() {
|
|
38
|
-
return {
|
|
25
|
+
return new StorageCapabilities({
|
|
39
26
|
multipart: false,
|
|
40
|
-
presignedUrls: false,
|
|
41
27
|
nativeFolders: false,
|
|
42
28
|
ranges: true,
|
|
43
29
|
copy: false,
|
|
44
30
|
move: false,
|
|
45
31
|
search: false,
|
|
46
|
-
directDownloadUrl: false
|
|
47
|
-
|
|
32
|
+
directDownloadUrl: false,
|
|
33
|
+
versioning: false,
|
|
34
|
+
publicUrls: false,
|
|
35
|
+
metadata: true
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
supports(capabilityName) {
|
|
40
|
+
return this.capabilities.has(capabilityName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
assertCapability(capabilityName, operationName) {
|
|
44
|
+
this.capabilities.assert(capabilityName, operationName, `provider '${this.type}'`);
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
/**
|
|
51
|
-
* List files and folders at the given prefix/path.
|
|
52
|
-
* @param {string} [prefix='']
|
|
53
|
-
* @param {object} [options={}]
|
|
54
|
-
* @returns {Promise<import('./models.js').StorageListResult>}
|
|
55
|
-
*/
|
|
56
47
|
async listFiles(prefix = '', options = {}) {
|
|
57
48
|
throw new StorageUnsupportedOperationError(`listFiles() not implemented by ${this.constructor.name}`);
|
|
58
49
|
}
|
|
59
50
|
|
|
60
|
-
/**
|
|
61
|
-
* Upload a file.
|
|
62
|
-
* @param {string} key
|
|
63
|
-
* @param {Buffer|Uint8Array|import('node:stream').Readable} fileBuffer
|
|
64
|
-
* @param {string} contentType
|
|
65
|
-
* @param {object} [options={}]
|
|
66
|
-
* @returns {Promise<{ key: string, fileSize: number, contentType: string, uploadedAt: string, [key: string]: any }>}
|
|
67
|
-
*/
|
|
68
51
|
async uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
69
52
|
throw new StorageUnsupportedOperationError(`uploadFile() not implemented by ${this.constructor.name}`);
|
|
70
53
|
}
|
|
71
54
|
|
|
72
|
-
/**
|
|
73
|
-
* Download a file.
|
|
74
|
-
* @param {string} key
|
|
75
|
-
* @param {object} [options={}]
|
|
76
|
-
* @returns {Promise<{ Body: import('node:stream').Readable, ContentType?: string, ContentLength?: number, ETag?: string, LastModified?: Date, notModified?: boolean, key: string }>}
|
|
77
|
-
*/
|
|
78
55
|
async downloadFile(key, options = {}) {
|
|
79
56
|
throw new StorageUnsupportedOperationError(`downloadFile() not implemented by ${this.constructor.name}`);
|
|
80
57
|
}
|
|
81
58
|
|
|
82
|
-
/**
|
|
83
|
-
* Delete a single file.
|
|
84
|
-
* @param {string} key
|
|
85
|
-
* @param {object} [options={}]
|
|
86
|
-
* @returns {Promise<{ key: string, deletedAt: string, [key: string]: any }>}
|
|
87
|
-
*/
|
|
88
59
|
async deleteFile(key, options = {}) {
|
|
89
60
|
throw new StorageUnsupportedOperationError(`deleteFile() not implemented by ${this.constructor.name}`);
|
|
90
61
|
}
|
|
91
62
|
|
|
92
|
-
/**
|
|
93
|
-
* Delete multiple files.
|
|
94
|
-
* @param {string[]} keys
|
|
95
|
-
* @param {object} [options={}]
|
|
96
|
-
* @returns {Promise<{ results: any[], deletedCount: number, errors: any[], deletedAt: string }>}
|
|
97
|
-
*/
|
|
98
63
|
async deleteFiles(keys, options = {}) {
|
|
64
|
+
const concurrency = Math.max(1, Math.min(Number(options.concurrency) || 8, 32));
|
|
99
65
|
let deletedCount = 0;
|
|
100
66
|
const errors = [];
|
|
101
67
|
const results = [];
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < keys.length; i += concurrency) {
|
|
70
|
+
const batch = keys.slice(i, i + concurrency);
|
|
71
|
+
const batchResults = await Promise.allSettled(
|
|
72
|
+
batch.map(key => this.deleteFile(key, options))
|
|
73
|
+
);
|
|
74
|
+
batchResults.forEach((outcome, idx) => {
|
|
75
|
+
const key = batch[idx];
|
|
76
|
+
if (outcome.status === 'fulfilled') {
|
|
77
|
+
results.push(outcome.value);
|
|
78
|
+
deletedCount++;
|
|
79
|
+
} else {
|
|
80
|
+
errors.push({ key, error: outcome.reason?.message || 'Delete failed' });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
110
83
|
}
|
|
84
|
+
|
|
111
85
|
return { results, deletedCount, errors, deletedAt: new Date().toISOString() };
|
|
112
86
|
}
|
|
113
87
|
|
|
114
|
-
/**
|
|
115
|
-
* Delete a folder and its contents.
|
|
116
|
-
* @param {string} prefix
|
|
117
|
-
* @param {object} [options={}]
|
|
118
|
-
* @returns {Promise<{ deletedCount: number, prefix: string, deletedAt: string }>}
|
|
119
|
-
*/
|
|
120
88
|
async deleteFolder(prefix, options = {}) {
|
|
121
89
|
throw new StorageUnsupportedOperationError(`deleteFolder() not implemented by ${this.constructor.name}`);
|
|
122
90
|
}
|
|
123
91
|
|
|
124
|
-
/**
|
|
125
|
-
* Create a folder.
|
|
126
|
-
* @param {string} key
|
|
127
|
-
* @param {object} [options={}]
|
|
128
|
-
* @returns {Promise<{ key: string, success: boolean, [key: string]: any }>}
|
|
129
|
-
*/
|
|
130
92
|
async createFolder(key, options = {}) {
|
|
131
93
|
throw new StorageUnsupportedOperationError(`createFolder() not implemented by ${this.constructor.name}`);
|
|
132
94
|
}
|
|
133
95
|
|
|
134
|
-
/**
|
|
135
|
-
* Get file metadata.
|
|
136
|
-
* @param {string} key
|
|
137
|
-
* @param {object} [options={}]
|
|
138
|
-
* @returns {Promise<{ key: string, exists: boolean, ContentLength?: number, ContentType?: string, LastModified?: Date, ETag?: string, Metadata?: object, queriedAt: string }>}
|
|
139
|
-
*/
|
|
140
96
|
async getFileMetadata(key, options = {}) {
|
|
141
97
|
throw new StorageUnsupportedOperationError(`getFileMetadata() not implemented by ${this.constructor.name}`);
|
|
142
98
|
}
|
|
143
99
|
|
|
144
|
-
/**
|
|
145
|
-
* Check if a file exists.
|
|
146
|
-
* @param {string} key
|
|
147
|
-
* @param {object} [options={}]
|
|
148
|
-
* @returns {Promise<boolean>}
|
|
149
|
-
*/
|
|
150
100
|
async fileExists(key, options = {}) {
|
|
151
101
|
try {
|
|
152
102
|
const meta = await this.getFileMetadata(key, options);
|
|
@@ -156,12 +106,6 @@ export class StorageProvider {
|
|
|
156
106
|
}
|
|
157
107
|
}
|
|
158
108
|
|
|
159
|
-
/**
|
|
160
|
-
* Get file size in bytes.
|
|
161
|
-
* @param {string} key
|
|
162
|
-
* @param {object} [options={}]
|
|
163
|
-
* @returns {Promise<number|null>}
|
|
164
|
-
*/
|
|
165
109
|
async getFileSize(key, options = {}) {
|
|
166
110
|
try {
|
|
167
111
|
const meta = await this.getFileMetadata(key, options);
|
|
@@ -171,47 +115,32 @@ export class StorageProvider {
|
|
|
171
115
|
}
|
|
172
116
|
}
|
|
173
117
|
|
|
174
|
-
/**
|
|
175
|
-
* Generate a signed or temporary access URL if supported.
|
|
176
|
-
* @param {string} key
|
|
177
|
-
* @param {'getObject'|'putObject'} [operation='getObject']
|
|
178
|
-
* @param {number} [expiresIn=3600]
|
|
179
|
-
* @param {object} [options={}]
|
|
180
|
-
* @returns {Promise<{ url: string, key: string, operation: string, expiresIn: number, expiresAt: string, generatedAt: string }>}
|
|
181
|
-
*/
|
|
182
|
-
async generateSignedUrl(key, operation = 'getObject', expiresIn = 3600, options = {}) {
|
|
183
|
-
throw new StorageUnsupportedOperationError(`generateSignedUrl() is not supported by provider ${this.type}`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Copy a file.
|
|
188
|
-
* @param {string} sourceKey
|
|
189
|
-
* @param {string} destKey
|
|
190
|
-
* @param {object} [options={}]
|
|
191
|
-
* @returns {Promise<{ sourceKey: string, destKey: string, copiedAt: string }>}
|
|
192
|
-
*/
|
|
193
118
|
async copyFile(sourceKey, destKey, options = {}) {
|
|
119
|
+
this.assertCapability('copy', 'copyFile');
|
|
194
120
|
throw new StorageUnsupportedOperationError(`copyFile() is not supported by provider ${this.type}`);
|
|
195
121
|
}
|
|
196
122
|
|
|
197
|
-
/**
|
|
198
|
-
* Move a file.
|
|
199
|
-
* @param {string} sourceKey
|
|
200
|
-
* @param {string} destKey
|
|
201
|
-
* @param {object} [options={}]
|
|
202
|
-
* @returns {Promise<{ sourceKey: string, destKey: string, movedAt: string }>}
|
|
203
|
-
*/
|
|
204
123
|
async moveFile(sourceKey, destKey, options = {}) {
|
|
124
|
+
this.assertCapability('move', 'moveFile');
|
|
205
125
|
await this.copyFile(sourceKey, destKey, options);
|
|
206
126
|
await this.deleteFile(sourceKey, options);
|
|
207
127
|
return { sourceKey, destKey, movedAt: new Date().toISOString() };
|
|
208
128
|
}
|
|
209
129
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
130
|
+
async initialize() {
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async dispose() {
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async ping() {
|
|
139
|
+
return this.checkHealth();
|
|
140
|
+
}
|
|
141
|
+
|
|
214
142
|
async checkHealth() {
|
|
215
143
|
throw new StorageUnsupportedOperationError(`checkHealth() not implemented by ${this.constructor.name}`);
|
|
216
144
|
}
|
|
217
145
|
}
|
|
146
|
+
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { resolveStorageName } from "../services/storage.service.js";
|
|
2
|
-
import { sendApiError, sendViewError } from "../utils/errors.js";
|
|
3
|
-
|
|
4
|
-
const NO_BUCKET_MSG = 'No storage connection selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Resolves `?bucket=<name>` query param and attaches `req.activeBucket` and `req.bucketResolveError`.
|
|
8
|
-
*/
|
|
9
|
-
export function bucketResolver(req, _res, next) {
|
|
10
|
-
try {
|
|
11
|
-
req.activeBucket = resolveStorageName(req.query.bucket);
|
|
12
|
-
req.bucketResolveError = null;
|
|
13
|
-
} catch (err) {
|
|
14
|
-
req.activeBucket = null;
|
|
15
|
-
req.bucketResolveError = err;
|
|
16
|
-
}
|
|
17
|
-
next();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Guard middleware for API routes — returns 400/503 JSON if bucket is invalid.
|
|
22
|
-
*/
|
|
23
|
-
export function requireBucketApi(req, res, next) {
|
|
24
|
-
if (req.bucketResolveError) return sendApiError(res, req.bucketResolveError, 'Invalid storage connection');
|
|
25
|
-
if (!req.activeBucket) return sendApiError(res, new Error(NO_BUCKET_MSG));
|
|
26
|
-
next();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Guard middleware for view routes — returns 400/503 JSON if bucket is invalid (skips /p_view/...).
|
|
31
|
-
*/
|
|
32
|
-
export function requireBucketView(req, res, next) {
|
|
33
|
-
if (req.path.includes('/p_view/') || req.url.startsWith('/p_view/') || req.originalUrl?.includes('/p_view/')) return next();
|
|
34
|
-
if (req.bucketResolveError) return sendViewError(res, req.bucketResolveError, 'Invalid storage connection');
|
|
35
|
-
if (!req.activeBucket) return sendViewError(res, new Error(NO_BUCKET_MSG));
|
|
36
|
-
next();
|
|
37
|
-
}
|
|
1
|
+
import { resolveStorageName } from "../services/storage.service.js";
|
|
2
|
+
import { sendApiError, sendViewError } from "../utils/errors.js";
|
|
3
|
+
|
|
4
|
+
const NO_BUCKET_MSG = 'No storage connection selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves `?bucket=<name>` query param and attaches `req.activeBucket` and `req.bucketResolveError`.
|
|
8
|
+
*/
|
|
9
|
+
export function bucketResolver(req, _res, next) {
|
|
10
|
+
try {
|
|
11
|
+
req.activeBucket = resolveStorageName(req.query.bucket);
|
|
12
|
+
req.bucketResolveError = null;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
req.activeBucket = null;
|
|
15
|
+
req.bucketResolveError = err;
|
|
16
|
+
}
|
|
17
|
+
next();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Guard middleware for API routes — returns 400/503 JSON if bucket is invalid.
|
|
22
|
+
*/
|
|
23
|
+
export function requireBucketApi(req, res, next) {
|
|
24
|
+
if (req.bucketResolveError) return sendApiError(res, req.bucketResolveError, 'Invalid storage connection');
|
|
25
|
+
if (!req.activeBucket) return sendApiError(res, new Error(NO_BUCKET_MSG));
|
|
26
|
+
next();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Guard middleware for view routes — returns 400/503 JSON if bucket is invalid (skips /p_view/...).
|
|
31
|
+
*/
|
|
32
|
+
export function requireBucketView(req, res, next) {
|
|
33
|
+
if (req.path.includes('/p_view/') || req.url.startsWith('/p_view/') || req.originalUrl?.includes('/p_view/')) return next();
|
|
34
|
+
if (req.bucketResolveError) return sendViewError(res, req.bucketResolveError, 'Invalid storage connection');
|
|
35
|
+
if (!req.activeBucket) return sendViewError(res, new Error(NO_BUCKET_MSG));
|
|
36
|
+
next();
|
|
37
|
+
}
|