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
package/lib/cli.js
CHANGED
|
@@ -8,643 +8,232 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* mbkbucket CLI —
|
|
11
|
+
* mbkbucket CLI — Standalone server runner for MBKBucket web application.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import dotenv from 'dotenv';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import { createApp } from './src/app.js';
|
|
18
|
+
import { packageJson, checkVersion } from './src/config/index.js';
|
|
19
|
+
import { checkHealth, getAvailableBucketNames } from './src/services/s3.service.js';
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// ANSI Color helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const c = {
|
|
25
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
26
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
27
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
28
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
29
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
30
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
31
|
+
};
|
|
21
32
|
|
|
22
33
|
// ---------------------------------------------------------------------------
|
|
23
34
|
// Parse CLI arguments
|
|
24
35
|
// ---------------------------------------------------------------------------
|
|
25
36
|
const args = process.argv.slice(2);
|
|
26
|
-
const VALUE_FLAGS = new Set(['server', 'app', 'bucket', 'profile-key', 'expires']);
|
|
27
37
|
|
|
28
|
-
function findFlag(name) {
|
|
38
|
+
function findFlag(name, shortChar) {
|
|
29
39
|
const long = `--${name}`;
|
|
30
|
-
const short = `-${name[0]}`;
|
|
40
|
+
const short = shortChar ? `-${shortChar}` : `-${name[0]}`;
|
|
31
41
|
for (let i = 0; i < args.length; i++) {
|
|
32
42
|
if (args[i] === long || args[i] === short) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const hasFlag = name => args.includes(`--${name}`) || args.includes(`-${name[0]}`);
|
|
40
|
-
|
|
41
|
-
function getPositionalArgs() {
|
|
42
|
-
const positional = [];
|
|
43
|
-
for (let i = 0; i < args.length; i++) {
|
|
44
|
-
if (args[i].startsWith('-')) {
|
|
45
|
-
const flagName = args[i].replace(/^-+/, '');
|
|
46
|
-
if (VALUE_FLAGS.has(flagName) && i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
47
|
-
i++;
|
|
43
|
+
if (args[i + 1] !== undefined && !args[i + 1].startsWith('-')) {
|
|
44
|
+
return args[i + 1];
|
|
48
45
|
}
|
|
49
|
-
|
|
50
|
-
positional.push(args[i]);
|
|
46
|
+
return true;
|
|
51
47
|
}
|
|
52
48
|
}
|
|
53
|
-
return
|
|
49
|
+
return undefined;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
52
|
+
const hasFlag = (name, shortChar) => {
|
|
53
|
+
const long = `--${name}`;
|
|
54
|
+
const short = shortChar ? `-${shortChar}` : `-${name[0]}`;
|
|
55
|
+
return args.includes(long) || args.includes(short);
|
|
56
|
+
};
|
|
59
57
|
|
|
60
58
|
// ---------------------------------------------------------------------------
|
|
61
|
-
// Help
|
|
59
|
+
// Help & Version
|
|
62
60
|
// ---------------------------------------------------------------------------
|
|
63
61
|
function showHelp() {
|
|
64
62
|
console.log(`
|
|
65
|
-
mbkbucket
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
--
|
|
95
|
-
|
|
96
|
-
LOGIN OPTIONS:
|
|
97
|
-
--server <url> mbkauthe server URL (required for login)
|
|
98
|
-
--profile-key <k> API token profile key
|
|
99
|
-
|
|
100
|
-
SIGNED-URL OPTIONS:
|
|
101
|
-
--expires <sec> Expiration in seconds (default: 3600)
|
|
102
|
-
|
|
103
|
-
ENVIRONMENT:
|
|
104
|
-
BucketConnection JSON mapping of bucket name → S3 credentials
|
|
105
|
-
APP_NAME Application name for key prefix isolation
|
|
106
|
-
PORT Server port (default: 3004)
|
|
63
|
+
${c.cyan(c.bold('⚡ mbkbucket'))} ${c.dim(`v${packageJson.version}`)}
|
|
64
|
+
Standalone S3/R2 Bucket Management Server
|
|
65
|
+
|
|
66
|
+
${c.bold('USAGE:')}
|
|
67
|
+
mbkbucket [options]
|
|
68
|
+
npx mbkbucket [options]
|
|
69
|
+
|
|
70
|
+
${c.bold('OPTIONS:')}
|
|
71
|
+
-p, --port <number> Port to listen on (default: 3004 or $PORT)
|
|
72
|
+
-H, --host <host> Host address to bind (default: 0.0.0.0 or $HOST)
|
|
73
|
+
-a, --app <name> Override APP_NAME for bucket key prefix isolation
|
|
74
|
+
-b, --bucket <name> Override default bucket name (MBKAUTHE_BUCKET)
|
|
75
|
+
-e, --env <path> Load environment variables from a custom .env file
|
|
76
|
+
-d, --dev Run in development mode (NODE_ENV=dev)
|
|
77
|
+
-o, --open Automatically open the dashboard in default browser
|
|
78
|
+
-v, --version Show version number
|
|
79
|
+
-h, --help Show this help message
|
|
80
|
+
|
|
81
|
+
${c.bold('ENVIRONMENT VARIABLES:')}
|
|
82
|
+
PORT Server port (default: 3004)
|
|
83
|
+
HOST Server host (default: 0.0.0.0)
|
|
84
|
+
NODE_ENV Environment mode ('production' | 'dev')
|
|
85
|
+
BucketConnection JSON mapping of bucket name -> S3 credentials
|
|
86
|
+
mbkautheVar mbkauthe configuration JSON
|
|
87
|
+
mbkbucketVar mbkbucket configuration JSON
|
|
88
|
+
|
|
89
|
+
${c.bold('EXAMPLES:')}
|
|
90
|
+
$ mbkbucket
|
|
91
|
+
$ mbkbucket -p 8080 --open
|
|
92
|
+
$ mbkbucket --env ./configs/.env.prod -a myapp
|
|
107
93
|
`);
|
|
108
94
|
}
|
|
109
95
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (_s3Module) return _s3Module;
|
|
113
|
-
const appFlag = findFlag('app');
|
|
114
|
-
if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
|
|
115
|
-
_s3Module = await import('./src/services/s3.service.js');
|
|
116
|
-
return _s3Module;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function walkDir(dir) {
|
|
120
|
-
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(dirent => {
|
|
121
|
-
const fullPath = path.join(dir, dirent.name);
|
|
122
|
-
return dirent.isDirectory() ? walkDir(fullPath) : [fullPath];
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function promptLine() {
|
|
127
|
-
return new Promise((resolve) => {
|
|
128
|
-
const { stdin, stdout } = process;
|
|
129
|
-
stdin.resume();
|
|
130
|
-
stdout.write('> ');
|
|
131
|
-
stdin.once('data', (data) => {
|
|
132
|
-
stdin.pause();
|
|
133
|
-
resolve(data.toString().trim());
|
|
134
|
-
});
|
|
135
|
-
});
|
|
96
|
+
function showVersion() {
|
|
97
|
+
console.log(`mbkbucket v${packageJson.version}`);
|
|
136
98
|
}
|
|
137
99
|
|
|
138
|
-
|
|
139
|
-
// Command Implementations
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
async function cmdLogin() {
|
|
143
|
-
const storedCfg = readConfig();
|
|
144
|
-
const server_url = findFlag('server') || storedCfg.server_url;
|
|
145
|
-
|
|
146
|
-
if (!server_url || typeof server_url !== 'string') {
|
|
147
|
-
console.error('❌ --server <url> is required for login.\n Example: mbkbucket login --server https://myapp.example.com\n\n💡 Tip: Set it once with:\n mbkbucket config set server_url https://your-server.com\n Then just run: mbkbucket login');
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const profile_key = findFlag('profile-key') || storedCfg.profile_key;
|
|
152
|
-
updateConfig({ server_url });
|
|
153
|
-
|
|
100
|
+
function getNetworkIp() {
|
|
154
101
|
try {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
updateConfig({
|
|
161
|
-
token: result.token,
|
|
162
|
-
token_prefix: result.token_prefix,
|
|
163
|
-
username: result.username,
|
|
164
|
-
server_url,
|
|
165
|
-
profile: result.profile || null,
|
|
166
|
-
logged_in_at: nowIso(),
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
console.log('\n✅ Credentials saved. You are now logged in.\n Run "mbkbucket whoami" to verify.');
|
|
170
|
-
} catch (err) {
|
|
171
|
-
const msg = err.message || '';
|
|
172
|
-
console.error(`❌ Login failed: ${msg}`);
|
|
173
|
-
if (msg.includes('profile_key') || msg.includes('profile_id')) {
|
|
174
|
-
console.error('\n💡 An API token profile is required. Create one in mbkauthe admin panel and pass --profile-key.');
|
|
175
|
-
} else if (msg.includes('fetch') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
176
|
-
console.error(`\n💡 Could not reach server "${server_url}". Verify it is running and CLI_AUTH_ENABLED=true.`);
|
|
177
|
-
}
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function cmdLogout() {
|
|
183
|
-
if (!isLoggedIn()) return console.log('ℹ️ Not logged in.');
|
|
184
|
-
const { username = 'user' } = readConfig();
|
|
185
|
-
clearConfig();
|
|
186
|
-
console.log(`👋 Logged out. Goodbye, ${username}!`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function cmdWhoami() {
|
|
190
|
-
if (!isLoggedIn()) return console.log('ℹ️ Not logged in. Run "mbkbucket login --server <url>" to authenticate.');
|
|
191
|
-
const cfg = readConfig();
|
|
192
|
-
console.log(`✅ Logged in as: ${cfg.username}`);
|
|
193
|
-
console.log(` Server: ${cfg.server_url}`);
|
|
194
|
-
console.log(` Token: ${cfg.token_prefix}...`);
|
|
195
|
-
if (cfg.profile) console.log(` Profile: ${cfg.profile.name} (scope: ${cfg.profile.scope || 'full'})`);
|
|
196
|
-
console.log(` Since: ${formatDate(cfg.logged_in_at)}`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function cmdList() {
|
|
200
|
-
const prefix = cmdArgs[0] || '';
|
|
201
|
-
const bucketName = findFlag('bucket');
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
const s3 = await getS3();
|
|
205
|
-
const result = await s3.listfiles(prefix, { bucketName, delimiter: '/' });
|
|
206
|
-
|
|
207
|
-
if (!result.Contents?.length) {
|
|
208
|
-
return console.log(`📭 No files found${prefix ? ` with prefix "${prefix}"` : ''}.`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
console.log(`\n📁 Files${prefix ? ` matching "${prefix}"` : ''} (${result.KeyCount || result.Contents.length} items):\n` + '─'.repeat(80));
|
|
212
|
-
|
|
213
|
-
if (result.CommonPrefixes?.length) {
|
|
214
|
-
for (const cp of result.CommonPrefixes) {
|
|
215
|
-
const folderName = cp.Prefix.replace(/\/$/, '').split('/').pop() || cp.Prefix;
|
|
216
|
-
console.log(` 📂 ${folderName}/`);
|
|
102
|
+
const nets = os.networkInterfaces();
|
|
103
|
+
for (const name of Object.keys(nets)) {
|
|
104
|
+
for (const net of nets[name] ?? []) {
|
|
105
|
+
if (net.family === 'IPv4' && !net.internal) return net.address;
|
|
217
106
|
}
|
|
218
|
-
if (result.Contents.length > 0) console.log('');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
for (const obj of result.Contents) {
|
|
222
|
-
const name = obj.Key || '';
|
|
223
|
-
const displayName = name.length > 60 ? `...${name.slice(-57)}` : name;
|
|
224
|
-
console.log(` ${displayName.padEnd(62)} ${formatBytes(obj.Size).padStart(10)} ${formatDate(obj.LastModified)}`);
|
|
225
107
|
}
|
|
108
|
+
} catch {}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
226
111
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
112
|
+
function openBrowser(url) {
|
|
113
|
+
const platform = process.platform;
|
|
114
|
+
let cmd = '';
|
|
115
|
+
if (platform === 'darwin') cmd = `open "${url}"`;
|
|
116
|
+
else if (platform === 'win32') cmd = `start "" "${url}"`;
|
|
117
|
+
else cmd = `xdg-open "${url}"`;
|
|
118
|
+
import('node:child_process').then(({ exec }) => {
|
|
119
|
+
exec(cmd, () => {});
|
|
120
|
+
}).catch(() => {});
|
|
234
121
|
}
|
|
235
122
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Main Runner
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
async function main() {
|
|
127
|
+
if (hasFlag('help', 'h')) {
|
|
128
|
+
showHelp();
|
|
129
|
+
process.exit(0);
|
|
241
130
|
}
|
|
242
131
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
process.exit(1);
|
|
132
|
+
if (hasFlag('version', 'v')) {
|
|
133
|
+
showVersion();
|
|
134
|
+
process.exit(0);
|
|
247
135
|
}
|
|
248
136
|
|
|
249
|
-
|
|
250
|
-
|
|
137
|
+
// Load custom or default .env
|
|
138
|
+
const envPath = findFlag('env', 'e');
|
|
139
|
+
if (typeof envPath === 'string') {
|
|
140
|
+
dotenv.config({ path: path.resolve(process.cwd(), envPath) });
|
|
251
141
|
} else {
|
|
252
|
-
|
|
142
|
+
dotenv.config();
|
|
253
143
|
}
|
|
254
|
-
}
|
|
255
144
|
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const key = cmdArgs[1] || fileName;
|
|
261
|
-
const contentType = getMimeType(fileName);
|
|
262
|
-
|
|
263
|
-
console.log(`⬆️ Uploading: ${fileName} (${formatBytes(stat.size)}) → ${key}`);
|
|
264
|
-
try {
|
|
265
|
-
const s3 = await getS3();
|
|
266
|
-
const result = await s3.uploadFile(key, fs.readFileSync(filePath), contentType, { bucketName });
|
|
267
|
-
console.log(`✅ Uploaded: ${result.key} (${formatBytes(result.fileSize)})`);
|
|
268
|
-
} catch (err) {
|
|
269
|
-
console.error(`❌ Upload failed: ${err.message}`);
|
|
270
|
-
process.exit(1);
|
|
145
|
+
// Handle environment overrides from flags
|
|
146
|
+
const appFlag = findFlag('app', 'a');
|
|
147
|
+
if (typeof appFlag === 'string') {
|
|
148
|
+
process.env.APP_NAME = appFlag;
|
|
271
149
|
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function uploadFolder(folderPath) {
|
|
275
|
-
const bucketName = findFlag('bucket');
|
|
276
|
-
const prefix = cmdArgs[1] || path.basename(folderPath);
|
|
277
|
-
const files = walkDir(folderPath);
|
|
278
|
-
|
|
279
|
-
if (!files.length) return console.log('📭 No files found in folder.');
|
|
280
|
-
|
|
281
|
-
console.log(`📁 Uploading folder: ${folderPath} (${files.length} files) → ${prefix}/\n`);
|
|
282
|
-
let uploaded = 0;
|
|
283
|
-
let failed = 0;
|
|
284
150
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
const s3 = await getS3();
|
|
290
|
-
await s3.uploadFile(key, fs.readFileSync(file), getMimeType(file), { bucketName });
|
|
291
|
-
console.log(` ✅ ${rel}`);
|
|
292
|
-
uploaded++;
|
|
293
|
-
} catch (err) {
|
|
294
|
-
console.error(` ❌ ${rel}: ${err.message}`);
|
|
295
|
-
failed++;
|
|
296
|
-
}
|
|
151
|
+
const bucketFlag = findFlag('bucket', 'b');
|
|
152
|
+
if (typeof bucketFlag === 'string') {
|
|
153
|
+
process.env.MBKAUTHE_BUCKET = bucketFlag;
|
|
297
154
|
}
|
|
298
155
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async function cmdDownload() {
|
|
304
|
-
const key = cmdArgs[0];
|
|
305
|
-
if (!key) {
|
|
306
|
-
console.error('❌ Usage: mbkbucket download <key> [destination]');
|
|
307
|
-
process.exit(1);
|
|
156
|
+
if (hasFlag('dev', 'd')) {
|
|
157
|
+
process.env.NODE_ENV = 'dev';
|
|
308
158
|
}
|
|
309
159
|
|
|
310
|
-
const
|
|
311
|
-
const
|
|
160
|
+
const port = parseInt(findFlag('port', 'p') || process.env.PORT || '3004', 10);
|
|
161
|
+
const host = findFlag('host', 'H') || process.env.HOST || '0.0.0.0';
|
|
162
|
+
const shouldOpen = hasFlag('open', 'o');
|
|
312
163
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (key.endsWith('/')) return downloadFolder(s3, key, dest, bucketName);
|
|
164
|
+
// Check version in background
|
|
165
|
+
await checkVersion();
|
|
316
166
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const body = await streamToBuffer(result.Body);
|
|
322
|
-
const destDir = path.dirname(path.resolve(dest));
|
|
323
|
-
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
324
|
-
fs.writeFileSync(path.resolve(dest), body);
|
|
325
|
-
|
|
326
|
-
console.log(`✅ Downloaded: ${dest} (${formatBytes(body.length)})`);
|
|
167
|
+
// Test S3 connectivity
|
|
168
|
+
let healthResult = null;
|
|
169
|
+
try {
|
|
170
|
+
healthResult = await checkHealth();
|
|
327
171
|
} catch (err) {
|
|
328
|
-
|
|
329
|
-
process.exit(1);
|
|
172
|
+
healthResult = { status: 'unhealthy', error: err.message };
|
|
330
173
|
}
|
|
331
|
-
}
|
|
332
174
|
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
let token = null;
|
|
336
|
-
const resolvedDest = path.resolve(destDir);
|
|
175
|
+
// Create express application
|
|
176
|
+
const app = createApp();
|
|
337
177
|
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
});
|
|
343
|
-
token = result.IsTruncated ? result.NextContinuationToken : null;
|
|
344
|
-
} while (token);
|
|
178
|
+
const server = app.listen(port, host, () => {
|
|
179
|
+
const isAnyHost = host === '0.0.0.0' || host === '::' || host === '';
|
|
180
|
+
const displayHost = isAnyHost ? 'localhost' : host;
|
|
181
|
+
const localUrl = `http://${displayHost}:${port}/mbkbucket`;
|
|
345
182
|
|
|
346
|
-
|
|
183
|
+
console.log();
|
|
184
|
+
console.log(` ${c.cyan(c.bold('⚡ MBKBucket'))} ${c.dim(`v${packageJson.version}`)}`);
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(` ${c.green('➜')} ${c.bold('Local:')} ${c.cyan(localUrl)}`);
|
|
347
187
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
for (const fileKey of allFiles) {
|
|
353
|
-
const relativePath = fileKey.startsWith(prefix) ? fileKey.slice(prefix.length) : fileKey;
|
|
354
|
-
const filePath = path.join(resolvedDest, relativePath);
|
|
355
|
-
try {
|
|
356
|
-
const result = await s3.downloadFile(fileKey, { bucketName });
|
|
357
|
-
if (!result.notModified) {
|
|
358
|
-
const body = await streamToBuffer(result.Body);
|
|
359
|
-
const fileDir = path.dirname(filePath);
|
|
360
|
-
if (!fs.existsSync(fileDir)) fs.mkdirSync(fileDir, { recursive: true });
|
|
361
|
-
fs.writeFileSync(filePath, body);
|
|
188
|
+
if (isAnyHost) {
|
|
189
|
+
const netIp = getNetworkIp();
|
|
190
|
+
if (netIp) {
|
|
191
|
+
console.log(` ${c.green('➜')} ${c.bold('Network:')} ${c.cyan(`http://${netIp}:${port}/mbkbucket`)}`);
|
|
362
192
|
}
|
|
363
|
-
console.log(` ✅ ${relativePath}`);
|
|
364
|
-
downloaded++;
|
|
365
|
-
} catch (err) {
|
|
366
|
-
console.error(` ❌ ${relativePath}: ${err.message}`);
|
|
367
|
-
failed++;
|
|
368
193
|
}
|
|
369
|
-
}
|
|
370
194
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
195
|
+
const appName = process.env.APP_NAME || 'root';
|
|
196
|
+
console.log(` ${c.green('➜')} ${c.bold('App Name:')} ${appName}`);
|
|
374
197
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
process.exit(1);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
const s3 = await getS3();
|
|
384
|
-
console.log(`🗑️ Deleting: ${key}`);
|
|
385
|
-
const result = await s3.deleteFile(key, findFlag('bucket'));
|
|
386
|
-
console.log(`✅ Deleted: ${result.key}`);
|
|
387
|
-
} catch (err) {
|
|
388
|
-
console.error(`❌ Delete failed: ${err.message}`);
|
|
389
|
-
process.exit(1);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function cmdDeleteFolder() {
|
|
394
|
-
const prefix = cmdArgs[0];
|
|
395
|
-
if (!prefix) {
|
|
396
|
-
console.error('❌ Usage: mbkbucket delete-folder <prefix>');
|
|
397
|
-
process.exit(1);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const cleaned = trimSlashes(prefix);
|
|
401
|
-
console.log(`⚠️ This will delete ALL files under "${cleaned}/".\n Are you sure? Type "yes" to confirm:`);
|
|
402
|
-
|
|
403
|
-
const confirmed = await promptLine();
|
|
404
|
-
if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
|
|
405
|
-
|
|
406
|
-
try {
|
|
407
|
-
const s3 = await getS3();
|
|
408
|
-
console.log(`🗑️ Deleting folder: ${cleaned}/`);
|
|
409
|
-
const result = await s3.deleteFolder(cleaned, findFlag('bucket'));
|
|
410
|
-
console.log(`✅ Deleted ${result.deletedCount} file(s) under "${cleaned}/".`);
|
|
411
|
-
} catch (err) {
|
|
412
|
-
console.error(`❌ Delete folder failed: ${err.message}`);
|
|
413
|
-
process.exit(1);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function cmdInfo() {
|
|
418
|
-
const key = cmdArgs[0];
|
|
419
|
-
if (!key) {
|
|
420
|
-
console.error('❌ Usage: mbkbucket info <key>');
|
|
421
|
-
process.exit(1);
|
|
422
|
-
}
|
|
198
|
+
let bucketNames = [];
|
|
199
|
+
try {
|
|
200
|
+
bucketNames = getAvailableBucketNames();
|
|
201
|
+
} catch {}
|
|
423
202
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const meta = await s3.getFileMetadata(key, findFlag('bucket'));
|
|
427
|
-
if (!meta.exists) {
|
|
428
|
-
console.log(`❌ File not found: ${key}`);
|
|
429
|
-
process.exit(1);
|
|
203
|
+
if (bucketNames.length > 0) {
|
|
204
|
+
console.log(` ${c.green('➜')} ${c.bold('Buckets:')} ${bucketNames.join(', ')}`);
|
|
430
205
|
}
|
|
431
206
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
console.log(` ETag: ${meta.ETag || '—'}`);
|
|
437
|
-
console.log(` Cache-Control: ${meta.CacheControl || '—'}`);
|
|
438
|
-
if (meta.Metadata && Object.keys(meta.Metadata).length > 0) {
|
|
439
|
-
console.log(' Metadata:');
|
|
440
|
-
for (const [k, v] of Object.entries(meta.Metadata)) console.log(` ${k}: ${v}`);
|
|
207
|
+
if (healthResult?.status === 'healthy') {
|
|
208
|
+
console.log(` ${c.green('➜')} ${c.bold('Storage:')} ${c.green('✓ Connected')} (${healthResult.bucket} in ${healthResult.responseTime}ms)`);
|
|
209
|
+
} else if (healthResult?.error) {
|
|
210
|
+
console.log(` ${c.yellow('➜')} ${c.bold('Storage:')} ${c.yellow('⚠ Connection warning')} (${healthResult.error})`);
|
|
441
211
|
}
|
|
442
|
-
console.log('');
|
|
443
|
-
} catch (err) {
|
|
444
|
-
console.error(`❌ Info failed: ${err.message}`);
|
|
445
|
-
process.exit(1);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
async function cmdSignedUrl() {
|
|
450
|
-
const key = cmdArgs[0];
|
|
451
|
-
if (!key) {
|
|
452
|
-
console.error('❌ Usage: mbkbucket signed-url <key> [--expires <seconds>]');
|
|
453
|
-
process.exit(1);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const expires = parseInt(findFlag('expires'), 10) || 3600;
|
|
457
|
-
try {
|
|
458
|
-
const s3 = await getS3();
|
|
459
|
-
console.log(`🔗 Generating signed URL (expires in ${expires}s)...`);
|
|
460
|
-
const result = await s3.generateSignedUrl(key, 'getObject', expires, findFlag('bucket'));
|
|
461
|
-
console.log(`\n${result.url || result}`);
|
|
462
|
-
} catch (err) {
|
|
463
|
-
console.error(`❌ Signed URL failed: ${err.message}`);
|
|
464
|
-
process.exit(1);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ---------------------------------------------------------------------------
|
|
469
|
-
// Config Commands
|
|
470
|
-
// ---------------------------------------------------------------------------
|
|
471
|
-
const DEMO_CONFIG = {
|
|
472
|
-
token: 'mbk_demo_token_replace_with_real_one_from_login',
|
|
473
|
-
token_prefix: 'mbk_demo',
|
|
474
|
-
username: 'demo-user',
|
|
475
|
-
server_url: 'https://your-app.example.com',
|
|
476
|
-
profile: { name: 'Default CLI Profile', scope: 'read-write', allowed_apps: ['mbkbucket'] },
|
|
477
|
-
logged_in_at: nowIso(),
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
const VALID_CONFIG_KEYS = ['token', 'token_prefix', 'username', 'server_url', 'profile', 'profile_key', 'logged_in_at'];
|
|
481
|
-
|
|
482
|
-
async function cmdConfig() {
|
|
483
|
-
const [sub, key, value] = cmdArgs;
|
|
484
|
-
switch (sub) {
|
|
485
|
-
case 'get': return cmdConfigGet(key);
|
|
486
|
-
case 'set': return cmdConfigSet(key, value);
|
|
487
|
-
case 'unset':
|
|
488
|
-
case 'del':
|
|
489
|
-
case 'delete': return cmdConfigUnset(key);
|
|
490
|
-
case 'path': return cmdConfigPath();
|
|
491
|
-
case 'reset': return cmdConfigReset();
|
|
492
|
-
case 'edit': return cmdConfigEdit();
|
|
493
|
-
default: return cmdConfigShow();
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function cmdConfigShow() {
|
|
498
|
-
const cfg = readConfig();
|
|
499
|
-
const entries = Object.keys(cfg).filter(k => !k.startsWith('_'));
|
|
500
|
-
|
|
501
|
-
if (!entries.length) {
|
|
502
|
-
console.log(`📭 No config values set.\n File: ${CONFIG_FILE}\n Run "mbkbucket config reset" to populate demo values.`);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
console.log(`\n📋 Config (${CONFIG_FILE}):\n` + '─'.repeat(60));
|
|
507
|
-
for (const k of entries.sort()) {
|
|
508
|
-
const v = cfg[k];
|
|
509
|
-
const display = typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);
|
|
510
|
-
const masked = (k === 'token' && display.length > 12)
|
|
511
|
-
? `${display.slice(0, 8)}...${display.slice(-4)}`
|
|
512
|
-
: display.length > 55 ? `${display.slice(0, 52)}...` : display;
|
|
513
|
-
console.log(` ${k.padEnd(14)} ${masked}`);
|
|
514
|
-
}
|
|
515
|
-
console.log('─'.repeat(60) + '\n Run "mbkbucket config set <key> <value>" to change a value.\n Run "mbkbucket config edit" to open in your editor.\n');
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function cmdConfigGet(key) {
|
|
519
|
-
if (!key || !VALID_CONFIG_KEYS.includes(key)) {
|
|
520
|
-
console.error(`❌ Usage: mbkbucket config get <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
521
|
-
process.exit(1);
|
|
522
|
-
}
|
|
523
|
-
const cfg = readConfig();
|
|
524
|
-
if (!(key in cfg)) {
|
|
525
|
-
console.error(`❌ Key not found: ${key}\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
526
|
-
process.exit(1);
|
|
527
|
-
}
|
|
528
|
-
const v = cfg[key];
|
|
529
|
-
console.log(typeof v === 'object' && v !== null ? JSON.stringify(v, null, 2) : String(v));
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function cmdConfigSet(key, value) {
|
|
533
|
-
if (!key || value === undefined || !VALID_CONFIG_KEYS.includes(key)) {
|
|
534
|
-
console.error(`❌ Usage: mbkbucket config set <key> <value>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
535
|
-
process.exit(1);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
let parsed = value;
|
|
539
|
-
if (/^[{\[].*[}\]]$/.test(value)) {
|
|
540
|
-
try { parsed = JSON.parse(value); } catch {}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
updateConfig({ [key]: parsed });
|
|
544
|
-
console.log(`✅ Set ${key} = ${typeof parsed === 'object' ? JSON.stringify(parsed) : parsed}`);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function cmdConfigUnset(key) {
|
|
548
|
-
if (!key) {
|
|
549
|
-
console.error(`❌ Usage: mbkbucket config unset <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
550
|
-
process.exit(1);
|
|
551
|
-
}
|
|
552
|
-
const cfg = readConfig();
|
|
553
|
-
if (!(key in cfg)) {
|
|
554
|
-
console.error(`❌ Key not found: ${key}`);
|
|
555
|
-
process.exit(1);
|
|
556
|
-
}
|
|
557
|
-
delete cfg[key];
|
|
558
|
-
writeConfig(cfg);
|
|
559
|
-
console.log(`🗑️ Removed: ${key}`);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function cmdConfigPath() {
|
|
563
|
-
console.log(`📁 Config directory: ${CONFIG_DIR}\n📄 Config file: ${CONFIG_FILE}`);
|
|
564
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
565
|
-
const stat = fs.statSync(CONFIG_FILE);
|
|
566
|
-
console.log(` Size: ${formatBytes(stat.size)}\n Modified: ${formatDate(stat.mtime.toISOString())}`);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
async function cmdConfigReset() {
|
|
571
|
-
console.log('⚠️ This will overwrite your current config with demo values.\n Are you sure? Type "yes" to confirm:');
|
|
572
|
-
const confirmed = await promptLine();
|
|
573
|
-
if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
|
|
574
|
-
writeConfig({ ...DEMO_CONFIG });
|
|
575
|
-
console.log('✅ Config reset to demo values.\n Run "mbkbucket config" to view.');
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async function cmdConfigEdit() {
|
|
579
|
-
const editor = process.env.EDITOR || process.env.VISUAL || 'notepad';
|
|
580
|
-
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
581
|
-
if (!fs.existsSync(CONFIG_FILE)) writeConfig({ ...DEMO_CONFIG });
|
|
582
212
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
213
|
+
console.log();
|
|
214
|
+
console.log(` ${c.dim('Press Ctrl+C to stop the server')}`);
|
|
215
|
+
console.log();
|
|
586
216
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
else console.error(`⚠️ Editor exited with code ${code}.`);
|
|
591
|
-
resolve();
|
|
592
|
-
});
|
|
593
|
-
child.on('error', (err) => {
|
|
594
|
-
console.error(`❌ Could not open editor (${editor}): ${err.message}`);
|
|
595
|
-
resolve();
|
|
596
|
-
});
|
|
217
|
+
if (shouldOpen) {
|
|
218
|
+
openBrowser(localUrl);
|
|
219
|
+
}
|
|
597
220
|
});
|
|
598
|
-
}
|
|
599
221
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
ls: cmdList,
|
|
609
|
-
upload: cmdUpload,
|
|
610
|
-
up: cmdUpload,
|
|
611
|
-
download: cmdDownload,
|
|
612
|
-
dl: cmdDownload,
|
|
613
|
-
delete: cmdDelete,
|
|
614
|
-
rm: cmdDelete,
|
|
615
|
-
'delete-folder': cmdDeleteFolder,
|
|
616
|
-
rmdir: cmdDeleteFolder,
|
|
617
|
-
info: cmdInfo,
|
|
618
|
-
stat: cmdInfo,
|
|
619
|
-
'signed-url': cmdSignedUrl,
|
|
620
|
-
sign: cmdSignedUrl,
|
|
621
|
-
config: cmdConfig,
|
|
622
|
-
cfg: cmdConfig,
|
|
623
|
-
help: showHelp,
|
|
624
|
-
'--help': showHelp,
|
|
625
|
-
'-h': showHelp,
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
async function main() {
|
|
629
|
-
if (hasFlag('help')) return showHelp();
|
|
630
|
-
|
|
631
|
-
const appFlag = findFlag('app');
|
|
632
|
-
if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
|
|
633
|
-
|
|
634
|
-
const bucketFlag = findFlag('bucket');
|
|
635
|
-
if (bucketFlag && typeof bucketFlag === 'string') process.env.MBKAUTHE_BUCKET = bucketFlag;
|
|
636
|
-
|
|
637
|
-
const handler = COMMANDS[command];
|
|
638
|
-
if (!handler) {
|
|
639
|
-
console.error(`❌ Unknown command: ${command}\n Run "mbkbucket --help" for usage information.`);
|
|
640
|
-
process.exit(1);
|
|
641
|
-
}
|
|
222
|
+
const shutdown = (signal) => {
|
|
223
|
+
console.log(`\n ${c.dim(`Received ${signal}, shutting down...`)}`);
|
|
224
|
+
server.close(() => {
|
|
225
|
+
console.log(` ${c.green('✓')} ${c.dim('MBKBucket server stopped.')}\n`);
|
|
226
|
+
process.exit(0);
|
|
227
|
+
});
|
|
228
|
+
setTimeout(() => process.exit(0), 2000).unref();
|
|
229
|
+
};
|
|
642
230
|
|
|
643
|
-
|
|
231
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
232
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
644
233
|
}
|
|
645
234
|
|
|
646
235
|
main().catch((err) => {
|
|
647
|
-
console.error(
|
|
236
|
+
console.error(`\n${c.red(c.bold('Error starting mbkbucket:'))} ${err.message}`);
|
|
648
237
|
if (process.env.DEBUG) console.error(err);
|
|
649
238
|
process.exit(1);
|
|
650
239
|
});
|