mbkbucket 3.8.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -14
- package/README.md +561 -377
- package/index.js +32 -30
- package/lib/cli.js +154 -565
- package/lib/public/bucketadmin.css +2042 -1708
- package/lib/public/bucketadmin.js +1622 -1286
- package/lib/public/mbkbucket-helper-demo.html +4 -24
- package/lib/public/mbkbucket-helper.css +28 -34
- package/lib/public/mbkbucket-helper.js +4 -3
- package/lib/src/config/validation.js +86 -73
- package/lib/src/controllers/api.controller.js +459 -410
- package/lib/src/controllers/dashboard.controller.js +69 -49
- package/lib/src/controllers/info.controller.js +34 -3
- package/lib/src/controllers/view.controller.js +233 -231
- package/lib/src/core/storage/errors.js +62 -0
- package/lib/src/core/storage/gdrive-auth.js +193 -0
- package/lib/src/core/storage/gdrive-provider.js +656 -0
- package/lib/src/core/storage/index.js +14 -0
- package/lib/src/core/storage/models.js +80 -0
- package/lib/src/core/storage/path-strategy.js +65 -0
- package/lib/src/core/storage/s3-provider.js +491 -0
- package/lib/src/core/storage/storage-manager.js +203 -0
- package/lib/src/core/storage/storage-provider.js +217 -0
- package/lib/src/middleware/bucket-resolver.js +37 -37
- package/lib/src/routes/api.routes.js +5 -1
- package/lib/src/routes/index.js +60 -60
- package/lib/src/routes/view.routes.js +28 -25
- package/lib/src/services/s3.service.js +42 -594
- package/lib/src/services/storage.service.js +213 -0
- package/lib/src/utils/errors.js +56 -46
- package/package.json +11 -12
- package/views/bucket.hbs +47 -71
- package/views/bucketadmin_move_modal.hbs +35 -0
- package/views/bucketadmin_preview_modal.hbs +108 -40
- package/views/bucketadmincontent.hbs +193 -144
- package/views/mbkbucket_info.hbs +134 -232
- package/.mbkbucket.config.example.json +0 -14
- package/lib/src/cli/auth-helper.js +0 -141
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MBKBucket - Storage Manager & Provider Registry
|
|
3
|
+
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import dotenv from "dotenv";
|
|
8
|
+
import { mbkautheVar } from "mbkauthe";
|
|
9
|
+
import { S3StorageProvider } from "./s3-provider.js";
|
|
10
|
+
import { GoogleDriveStorageProvider } from "./gdrive-provider.js";
|
|
11
|
+
import { StorageConfigError, StorageValidationError } from "./errors.js";
|
|
12
|
+
import { createLogger } from "#logger";
|
|
13
|
+
|
|
14
|
+
dotenv.config();
|
|
15
|
+
const debugStorage = createLogger('storage-manager');
|
|
16
|
+
|
|
17
|
+
function parseJsonEnv(envVar, fallback = {}) {
|
|
18
|
+
if (!envVar) return fallback;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(envVar);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error(`❌ Error parsing JSON for envVar: ${String(envVar).slice(0, 200)} - ${e.message}`);
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class StorageManager {
|
|
28
|
+
constructor() {
|
|
29
|
+
this._providers = new Map();
|
|
30
|
+
this._factories = new Map();
|
|
31
|
+
this._configs = new Map();
|
|
32
|
+
|
|
33
|
+
// Register built-in provider drivers
|
|
34
|
+
this.registerDriver('s3', (name, cfg) => new S3StorageProvider(name, cfg));
|
|
35
|
+
this.registerDriver('r2', (name, cfg) => new S3StorageProvider(name, cfg));
|
|
36
|
+
this.registerDriver('minio', (name, cfg) => new S3StorageProvider(name, cfg));
|
|
37
|
+
this.registerDriver('gdrive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
|
|
38
|
+
this.registerDriver('google-drive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
|
|
39
|
+
this.registerDriver('googledrive', (name, cfg) => new GoogleDriveStorageProvider(name, cfg));
|
|
40
|
+
|
|
41
|
+
this.reloadFromEnv();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register a custom provider driver.
|
|
46
|
+
* @param {string} type
|
|
47
|
+
* @param {(name: string, config: object) => import('./storage-provider.js').StorageProvider} factory
|
|
48
|
+
*/
|
|
49
|
+
registerDriver(type, factory) {
|
|
50
|
+
this._factories.set(String(type).toLowerCase(), factory);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerProviderFactory(type, factory) {
|
|
54
|
+
this.registerDriver(type, factory);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reload all connections from environment variables.
|
|
59
|
+
*/
|
|
60
|
+
reloadFromEnv() {
|
|
61
|
+
this._providers.clear();
|
|
62
|
+
this._configs.clear();
|
|
63
|
+
|
|
64
|
+
const envSource = process.env.StorageConnection || process.env.BucketConnection;
|
|
65
|
+
const rawConfigs = parseJsonEnv(envSource, {});
|
|
66
|
+
|
|
67
|
+
for (const [name, config] of Object.entries(rawConfigs)) {
|
|
68
|
+
if (config && typeof config === 'object' && !Array.isArray(config)) {
|
|
69
|
+
this.registerConnection(name, config);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Register a named storage connection.
|
|
76
|
+
* @param {string} name
|
|
77
|
+
* @param {object} config
|
|
78
|
+
*/
|
|
79
|
+
registerConnection(name, config) {
|
|
80
|
+
const cleanName = String(name).trim();
|
|
81
|
+
if (!cleanName) throw new StorageValidationError("Connection name cannot be empty");
|
|
82
|
+
this._configs.set(cleanName, Object.freeze({ ...config }));
|
|
83
|
+
this._providers.delete(cleanName);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all registered connection names.
|
|
88
|
+
* @returns {string[]}
|
|
89
|
+
*/
|
|
90
|
+
getAvailableConnectionNames() {
|
|
91
|
+
return Array.from(this._configs.keys());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the default connection name.
|
|
96
|
+
* @returns {string|null}
|
|
97
|
+
*/
|
|
98
|
+
getDefaultConnectionName() {
|
|
99
|
+
const configuredDefault = mbkautheVar?.bucket;
|
|
100
|
+
if (configuredDefault && this._configs.has(configuredDefault)) {
|
|
101
|
+
return configuredDefault;
|
|
102
|
+
}
|
|
103
|
+
const available = this.getAvailableConnectionNames();
|
|
104
|
+
return available.length > 0 ? available[0] : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve a candidate connection name or fall back to default.
|
|
109
|
+
* @param {string} [name]
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
resolveConnectionName(name) {
|
|
113
|
+
const raw = typeof name === 'string' ? name.trim() : name;
|
|
114
|
+
const candidate = !raw ? this.getDefaultConnectionName() : String(raw);
|
|
115
|
+
|
|
116
|
+
const available = this.getAvailableConnectionNames();
|
|
117
|
+
if (!candidate) {
|
|
118
|
+
throw new StorageValidationError('No storage connection selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
|
|
119
|
+
}
|
|
120
|
+
if (available.length === 0) {
|
|
121
|
+
throw new StorageConfigError('BucketConnection environment variable is not set or empty. Please configure it with your storage connections.');
|
|
122
|
+
}
|
|
123
|
+
if (!this._configs.has(candidate)) {
|
|
124
|
+
throw new StorageValidationError(`Storage connection '${candidate}' not found in BucketConnection. Available connections: ${available.join(', ')}`);
|
|
125
|
+
}
|
|
126
|
+
return candidate;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get connection configuration object.
|
|
131
|
+
* @param {string} [name]
|
|
132
|
+
* @returns {object}
|
|
133
|
+
*/
|
|
134
|
+
getConnectionConfig(name) {
|
|
135
|
+
const resolved = this.resolveConnectionName(name);
|
|
136
|
+
return this._configs.get(resolved);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Determine the provider type from configuration.
|
|
141
|
+
* @param {object} config
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
inferProviderType(config = {}) {
|
|
145
|
+
if (config?.type) return String(config.type).toLowerCase();
|
|
146
|
+
if (config?.provider) return String(config.provider).toLowerCase();
|
|
147
|
+
if (config?.client_id || config?.refresh_token || config?.service_account_key || config?.folder_id || config?.service_account_email) {
|
|
148
|
+
return 'gdrive';
|
|
149
|
+
}
|
|
150
|
+
return 's3';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get or instantiate the StorageProvider for a connection.
|
|
155
|
+
* @param {string} [name]
|
|
156
|
+
* @returns {import('./storage-provider.js').StorageProvider}
|
|
157
|
+
*/
|
|
158
|
+
getProvider(name) {
|
|
159
|
+
const resolved = this.resolveConnectionName(name);
|
|
160
|
+
if (this._providers.has(resolved)) {
|
|
161
|
+
return this._providers.get(resolved);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const config = this.getConnectionConfig(resolved);
|
|
165
|
+
const providerType = this.inferProviderType(config);
|
|
166
|
+
const factory = this._factories.get(providerType);
|
|
167
|
+
|
|
168
|
+
if (!factory) {
|
|
169
|
+
throw new StorageConfigError(`Unsupported storage provider type '${providerType}' for connection '${resolved}'. Registered drivers: ${Array.from(this._factories.keys()).join(', ')}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
debugStorage('Instantiating provider "%s" (type: %s)', resolved, providerType);
|
|
173
|
+
const provider = factory(resolved, config);
|
|
174
|
+
this._providers.set(resolved, provider);
|
|
175
|
+
return provider;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check health of the default or specified connection.
|
|
180
|
+
* @param {string} [name]
|
|
181
|
+
* @returns {Promise<object>}
|
|
182
|
+
*/
|
|
183
|
+
async checkHealth(name) {
|
|
184
|
+
try {
|
|
185
|
+
const provider = this.getProvider(name);
|
|
186
|
+
return await provider.checkHealth();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
let bucket = 'unknown';
|
|
189
|
+
try {
|
|
190
|
+
const resolved = this.resolveConnectionName(name);
|
|
191
|
+
bucket = this.getConnectionConfig(resolved)?.BUCKET_NAME || resolved;
|
|
192
|
+
} catch {}
|
|
193
|
+
return {
|
|
194
|
+
status: 'unhealthy',
|
|
195
|
+
error: error.message,
|
|
196
|
+
bucket,
|
|
197
|
+
checkedAt: new Date().toISOString()
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const storageManager = new StorageManager();
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MBKBucket - Core Storage Provider Interface
|
|
3
|
+
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { StorageUnsupportedOperationError } from "./errors.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Abstract base class for storage providers (S3, Cloudflare R2, Google Drive, Azure, etc.)
|
|
11
|
+
*/
|
|
12
|
+
export class StorageProvider {
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} name - Connection/Bucket name identifier
|
|
15
|
+
* @param {object} config - Provider-specific configuration
|
|
16
|
+
*/
|
|
17
|
+
constructor(name, config = {}) {
|
|
18
|
+
if (new.target === StorageProvider) {
|
|
19
|
+
throw new TypeError("Cannot construct StorageProvider instances directly");
|
|
20
|
+
}
|
|
21
|
+
this.name = name;
|
|
22
|
+
this.config = Object.freeze({ ...config });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provider type identifier (e.g. 's3', 'gdrive', 'r2')
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
get type() {
|
|
30
|
+
return 'generic';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Explicit provider capabilities.
|
|
35
|
+
* @returns {object}
|
|
36
|
+
*/
|
|
37
|
+
get capabilities() {
|
|
38
|
+
return {
|
|
39
|
+
multipart: false,
|
|
40
|
+
presignedUrls: false,
|
|
41
|
+
nativeFolders: false,
|
|
42
|
+
ranges: true,
|
|
43
|
+
copy: false,
|
|
44
|
+
move: false,
|
|
45
|
+
search: false,
|
|
46
|
+
directDownloadUrl: false
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
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
|
+
async listFiles(prefix = '', options = {}) {
|
|
57
|
+
throw new StorageUnsupportedOperationError(`listFiles() not implemented by ${this.constructor.name}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
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
|
+
async uploadFile(key, fileBuffer, contentType, options = {}) {
|
|
69
|
+
throw new StorageUnsupportedOperationError(`uploadFile() not implemented by ${this.constructor.name}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
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
|
+
async downloadFile(key, options = {}) {
|
|
79
|
+
throw new StorageUnsupportedOperationError(`downloadFile() not implemented by ${this.constructor.name}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
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
|
+
async deleteFile(key, options = {}) {
|
|
89
|
+
throw new StorageUnsupportedOperationError(`deleteFile() not implemented by ${this.constructor.name}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
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
|
+
async deleteFiles(keys, options = {}) {
|
|
99
|
+
let deletedCount = 0;
|
|
100
|
+
const errors = [];
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const key of keys) {
|
|
103
|
+
try {
|
|
104
|
+
const res = await this.deleteFile(key, options);
|
|
105
|
+
results.push(res);
|
|
106
|
+
deletedCount++;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
errors.push({ key, error: err.message });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { results, deletedCount, errors, deletedAt: new Date().toISOString() };
|
|
112
|
+
}
|
|
113
|
+
|
|
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
|
+
async deleteFolder(prefix, options = {}) {
|
|
121
|
+
throw new StorageUnsupportedOperationError(`deleteFolder() not implemented by ${this.constructor.name}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
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
|
+
async createFolder(key, options = {}) {
|
|
131
|
+
throw new StorageUnsupportedOperationError(`createFolder() not implemented by ${this.constructor.name}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
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
|
+
async getFileMetadata(key, options = {}) {
|
|
141
|
+
throw new StorageUnsupportedOperationError(`getFileMetadata() not implemented by ${this.constructor.name}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if a file exists.
|
|
146
|
+
* @param {string} key
|
|
147
|
+
* @param {object} [options={}]
|
|
148
|
+
* @returns {Promise<boolean>}
|
|
149
|
+
*/
|
|
150
|
+
async fileExists(key, options = {}) {
|
|
151
|
+
try {
|
|
152
|
+
const meta = await this.getFileMetadata(key, options);
|
|
153
|
+
return Boolean(meta?.exists);
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get file size in bytes.
|
|
161
|
+
* @param {string} key
|
|
162
|
+
* @param {object} [options={}]
|
|
163
|
+
* @returns {Promise<number|null>}
|
|
164
|
+
*/
|
|
165
|
+
async getFileSize(key, options = {}) {
|
|
166
|
+
try {
|
|
167
|
+
const meta = await this.getFileMetadata(key, options);
|
|
168
|
+
return meta?.exists ? (meta.ContentLength ?? null) : null;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
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
|
+
async copyFile(sourceKey, destKey, options = {}) {
|
|
194
|
+
throw new StorageUnsupportedOperationError(`copyFile() is not supported by provider ${this.type}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
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
|
+
async moveFile(sourceKey, destKey, options = {}) {
|
|
205
|
+
await this.copyFile(sourceKey, destKey, options);
|
|
206
|
+
await this.deleteFile(sourceKey, options);
|
|
207
|
+
return { sourceKey, destKey, movedAt: new Date().toISOString() };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Run health check.
|
|
212
|
+
* @returns {Promise<{ status: 'healthy'|'unhealthy', responseTime?: number, error?: string, [key: string]: any }>}
|
|
213
|
+
*/
|
|
214
|
+
async checkHealth() {
|
|
215
|
+
throw new StorageUnsupportedOperationError(`checkHealth() not implemented by ${this.constructor.name}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { sendApiError, sendViewError } from "../utils/errors.js";
|
|
3
|
-
|
|
4
|
-
const NO_BUCKET_MSG = 'No
|
|
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 =
|
|
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
|
|
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.startsWith('/p_view/')) return next();
|
|
34
|
-
if (req.bucketResolveError) return sendViewError(res, req.bucketResolveError, 'Invalid
|
|
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
|
+
}
|
|
@@ -2,7 +2,7 @@ import express from "express";
|
|
|
2
2
|
import { sessRole } from "mbkauthe";
|
|
3
3
|
import multer from "multer";
|
|
4
4
|
import { requireBucketApi } from "../middleware/bucket-resolver.js";
|
|
5
|
-
import { cleanupUploads, listIncompleteUploads, listFiles, uploadSingleFile, initiateMultipartUpload, uploadChunk, completeUpload, abortUpload, createFolder, deleteItems, downloadFileHandler } from "../controllers/api.controller.js";
|
|
5
|
+
import { cleanupUploads, listIncompleteUploads, listFiles, uploadSingleFile, initiateMultipartUpload, uploadChunk, completeUpload, abortUpload, createFolder, deleteItems, downloadFileHandler, moveItem, copyItem, getHealthStatus, getFileInfo } from "../controllers/api.controller.js";
|
|
6
6
|
|
|
7
7
|
const MAX_CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
8
8
|
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: MAX_CHUNK_SIZE } });
|
|
@@ -36,6 +36,8 @@ export function createApiRoutes({
|
|
|
36
36
|
apiRouter.post('/api/cleanup-uploads', deleteAuth, cleanupUploads);
|
|
37
37
|
apiRouter.get('/api/incomplete-uploads', viewAuth, listIncompleteUploads);
|
|
38
38
|
apiRouter.get('/api/files', viewAuth, listFiles);
|
|
39
|
+
apiRouter.get('/api/health', viewAuth, getHealthStatus);
|
|
40
|
+
apiRouter.get('/api/file-info/*key', viewAuth, getFileInfo);
|
|
39
41
|
apiRouter.post('/upload', uploadAuth, upload.single('file'), uploadSingleFile);
|
|
40
42
|
apiRouter.post('/upload-init', uploadAuth, initiateMultipartUpload);
|
|
41
43
|
apiRouter.post('/upload-chunk', uploadAuth, uploadChunkMiddleware, uploadChunk);
|
|
@@ -43,6 +45,8 @@ export function createApiRoutes({
|
|
|
43
45
|
apiRouter.post('/upload-abort', deleteAuth, abortUpload);
|
|
44
46
|
apiRouter.post('/create-folder', uploadAuth, createFolder);
|
|
45
47
|
apiRouter.post('/delete', deleteAuth, deleteItems);
|
|
48
|
+
apiRouter.post('/move', uploadAuth, moveItem);
|
|
49
|
+
apiRouter.post('/copy', uploadAuth, copyItem);
|
|
46
50
|
apiRouter.get('/download/*key', viewAuth, downloadFileHandler);
|
|
47
51
|
|
|
48
52
|
return apiRouter;
|
package/lib/src/routes/index.js
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MBKBucket
|
|
3
|
-
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
-
* Licensed under the MIT License.
|
|
5
|
-
* Source: https://github.com/MIbnEKhalid/mbkbucket
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import express from "express";
|
|
9
|
-
import { sessRole } from "mbkauthe";
|
|
10
|
-
import path from "path";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
12
|
-
import { bucketResolver } from "../middleware/bucket-resolver.js";
|
|
13
|
-
import { renderBucketDashboard } from "../controllers/dashboard.controller.js";
|
|
14
|
-
import infoRoutes from "./info.routes.js";
|
|
15
|
-
import { createViewRoutes } from "./view.routes.js";
|
|
16
|
-
import { createApiRoutes } from "./api.routes.js";
|
|
17
|
-
|
|
18
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
-
const __dirname = path.dirname(__filename);
|
|
20
|
-
const publicDir = path.resolve(__dirname, "..", "..", "public");
|
|
21
|
-
|
|
22
|
-
const serveAsset = (filename) => (_req, res) => {
|
|
23
|
-
res.set("Cache-Control", "public, max-age=300, must-revalidate");
|
|
24
|
-
res.sendFile(path.join(publicDir, filename), (err) => {
|
|
25
|
-
if (err) res.status(err.status || 404).end();
|
|
26
|
-
});
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export function createBucketRouter({ authorization } = {}) {
|
|
30
|
-
const router = express.Router();
|
|
31
|
-
const defaultAuth = sessRole('superadmin');
|
|
32
|
-
const auths = typeof authorization === 'function'
|
|
33
|
-
? { view: authorization, upload: authorization, delete: authorization }
|
|
34
|
-
: authorization ?? {};
|
|
35
|
-
const viewAuth = auths.view ?? defaultAuth;
|
|
36
|
-
const uploadAuth = auths.upload ?? defaultAuth;
|
|
37
|
-
const deleteAuth = auths.delete ?? defaultAuth;
|
|
38
|
-
|
|
39
|
-
// Static dashboard & helper assets
|
|
40
|
-
router.get('/mbkbucket/bucketadmin.css', serveAsset('bucketadmin.css'));
|
|
41
|
-
router.get('/mbkbucket/bucketadmin.js', serveAsset('bucketadmin.js'));
|
|
42
|
-
router.get('/mbkbucket/mbkbucket-helper.css', serveAsset('mbkbucket-helper.css'));
|
|
43
|
-
router.get('/mbkbucket/mbkbucket-helper.js', serveAsset('mbkbucket-helper.js'));
|
|
44
|
-
router.get('/mbkbucket/helper-demo', serveAsset('mbkbucket-helper-demo.html'));
|
|
45
|
-
|
|
46
|
-
router.get('/mbkbucket', viewAuth, renderBucketDashboard);
|
|
47
|
-
router.use(
|
|
48
|
-
'/mbkbucket',
|
|
49
|
-
bucketResolver,
|
|
50
|
-
infoRoutes,
|
|
51
|
-
createViewRoutes({ authorization: viewAuth }),
|
|
52
|
-
createApiRoutes({ viewAuthorization: viewAuth, uploadAuthorization: uploadAuth, deleteAuthorization: deleteAuth })
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
return router;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const router = createBucketRouter();
|
|
59
|
-
|
|
60
|
-
export default router;
|
|
1
|
+
/**
|
|
2
|
+
* MBKBucket
|
|
3
|
+
* Copyright (c) 2026 Muhammad Bin Khalid, MBKTech.org and contributors
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
* Source: https://github.com/MIbnEKhalid/mbkbucket
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import express from "express";
|
|
9
|
+
import { sessRole } from "mbkauthe";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { bucketResolver } from "../middleware/bucket-resolver.js";
|
|
13
|
+
import { renderBucketDashboard } from "../controllers/dashboard.controller.js";
|
|
14
|
+
import infoRoutes from "./info.routes.js";
|
|
15
|
+
import { createViewRoutes } from "./view.routes.js";
|
|
16
|
+
import { createApiRoutes } from "./api.routes.js";
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
const publicDir = path.resolve(__dirname, "..", "..", "public");
|
|
21
|
+
|
|
22
|
+
const serveAsset = (filename) => (_req, res) => {
|
|
23
|
+
res.set("Cache-Control", "public, max-age=300, must-revalidate");
|
|
24
|
+
res.sendFile(path.join(publicDir, filename), (err) => {
|
|
25
|
+
if (err) res.status(err.status || 404).end();
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function createBucketRouter({ authorization, publiViewEnabled } = {}) {
|
|
30
|
+
const router = express.Router();
|
|
31
|
+
const defaultAuth = sessRole('superadmin');
|
|
32
|
+
const auths = typeof authorization === 'function'
|
|
33
|
+
? { view: authorization, upload: authorization, delete: authorization }
|
|
34
|
+
: authorization ?? {};
|
|
35
|
+
const viewAuth = auths.view ?? defaultAuth;
|
|
36
|
+
const uploadAuth = auths.upload ?? defaultAuth;
|
|
37
|
+
const deleteAuth = auths.delete ?? defaultAuth;
|
|
38
|
+
|
|
39
|
+
// Static dashboard & helper assets
|
|
40
|
+
router.get('/mbkbucket/bucketadmin.css', serveAsset('bucketadmin.css'));
|
|
41
|
+
router.get('/mbkbucket/bucketadmin.js', serveAsset('bucketadmin.js'));
|
|
42
|
+
router.get('/mbkbucket/mbkbucket-helper.css', serveAsset('mbkbucket-helper.css'));
|
|
43
|
+
router.get('/mbkbucket/mbkbucket-helper.js', serveAsset('mbkbucket-helper.js'));
|
|
44
|
+
router.get('/mbkbucket/helper-demo', serveAsset('mbkbucket-helper-demo.html'));
|
|
45
|
+
|
|
46
|
+
router.get('/mbkbucket', viewAuth, renderBucketDashboard);
|
|
47
|
+
router.use(
|
|
48
|
+
'/mbkbucket',
|
|
49
|
+
bucketResolver,
|
|
50
|
+
infoRoutes,
|
|
51
|
+
createViewRoutes({ authorization: viewAuth, publiViewEnabled }),
|
|
52
|
+
createApiRoutes({ viewAuthorization: viewAuth, uploadAuthorization: uploadAuth, deleteAuthorization: deleteAuth })
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return router;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const router = createBucketRouter();
|
|
59
|
+
|
|
60
|
+
export default router;
|