mbkbucket 3.3.1 → 3.4.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/README.md +17 -24
- package/index.js +2 -12
- package/lib/cli.js +191 -525
- package/lib/public/bucketadmin.js +1 -1
- package/lib/src/app.js +37 -50
- package/lib/src/cli/auth-helper.js +39 -112
- package/lib/src/config/index.js +68 -96
- package/lib/src/config/validation.js +14 -30
- package/lib/src/controllers/api.controller.js +85 -176
- package/lib/src/controllers/dashboard.controller.js +12 -23
- package/lib/src/controllers/info.controller.js +17 -20
- package/lib/src/controllers/view.controller.js +23 -70
- package/lib/src/middleware/async-handler.js +1 -16
- package/lib/src/middleware/bucket-resolver.js +7 -13
- package/lib/src/middleware/error-handler.js +40 -20
- package/lib/src/middleware/rate-limiter.js +2 -10
- package/lib/src/middleware/security.js +1 -17
- package/lib/src/routes/api.routes.js +21 -47
- package/lib/src/routes/index.js +2 -10
- package/lib/src/routes/info.routes.js +2 -3
- package/lib/src/routes/view.routes.js +3 -8
- package/lib/src/services/s3.service.js +157 -349
- package/lib/src/utils/errors.js +37 -59
- package/lib/src/utils/helpers.js +124 -70
- package/package.json +13 -5
- /package/{lib/views → views}/bucket.handlebars +0 -0
- /package/{lib/views → views}/bucketadmin_alerts.handlebars +0 -0
- /package/{lib/views → views}/bucketadmin_delete_modal.handlebars +0 -0
- /package/{lib/views → views}/bucketadmin_filelist_skeleton.handlebars +0 -0
- /package/{lib/views → views}/bucketadmin_newfolder_modal.handlebars +0 -0
- /package/{lib/views → views}/bucketadmin_preview_modal.handlebars +0 -0
- /package/{lib/views → views}/bucketadmin_upload_modal.handlebars +0 -0
- /package/{lib/views → views}/bucketadmincontent.handlebars +0 -0
- /package/{lib/views → views}/bucketportal.handlebars +0 -0
- /package/{lib/views → views}/mbkbucket_info.handlebars +0 -0
package/lib/cli.js
CHANGED
|
@@ -2,34 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* mbkbucket CLI — Manage S3/R2 buckets from the command line.
|
|
5
|
-
*
|
|
6
|
-
* mbkbucket login --server <url> Authenticate via device flow
|
|
7
|
-
* mbkbucket logout Clear stored credentials
|
|
8
|
-
* mbkbucket whoami Show login status
|
|
9
|
-
* mbkbucket list [prefix] List files and folders
|
|
10
|
-
* mbkbucket upload <file> [key] Upload a file
|
|
11
|
-
* mbkbucket download <key> [dest] Download a file
|
|
12
|
-
* mbkbucket delete <key> Delete a file
|
|
13
|
-
* mbkbucket delete-folder <prefix> Recursively delete a folder
|
|
14
|
-
* mbkbucket info <key> Show file metadata
|
|
15
|
-
* mbkbucket signed-url <key> Generate a pre-signed URL
|
|
16
|
-
*
|
|
17
|
-
* Before running, set these env vars (or use a .env file in cwd):
|
|
18
|
-
* BucketConnection={"bucketname":{...}}
|
|
19
|
-
* APP_NAME=your-app-name
|
|
20
5
|
*/
|
|
21
6
|
|
|
22
7
|
import dotenv from 'dotenv';
|
|
23
8
|
import path from 'node:path';
|
|
24
9
|
import fs from 'node:fs';
|
|
10
|
+
import { formatBytes, formatDate, streamToBuffer, getMimeType, trimSlashes, nowIso } from './src/utils/helpers.js';
|
|
11
|
+
import { readConfig, clearConfig, updateConfig, writeConfig, isLoggedIn, deviceFlowLogin, CONFIG_FILE, CONFIG_DIR } from '#helpers';
|
|
25
12
|
|
|
26
|
-
// Load .env from cwd first so S3 service can pick up BucketConnection etc.
|
|
27
13
|
dotenv.config();
|
|
28
14
|
|
|
29
15
|
// ---------------------------------------------------------------------------
|
|
30
16
|
// Parse CLI arguments
|
|
31
17
|
// ---------------------------------------------------------------------------
|
|
32
18
|
const args = process.argv.slice(2);
|
|
19
|
+
const VALUE_FLAGS = new Set(['server', 'app', 'bucket', 'profile-key', 'expires']);
|
|
33
20
|
|
|
34
21
|
function findFlag(name) {
|
|
35
22
|
const long = `--${name}`;
|
|
@@ -42,21 +29,15 @@ function findFlag(name) {
|
|
|
42
29
|
return undefined;
|
|
43
30
|
}
|
|
44
31
|
|
|
45
|
-
|
|
46
|
-
return args.includes(`--${name}`) || args.includes(`-${name[0]}`);
|
|
47
|
-
}
|
|
32
|
+
const hasFlag = name => args.includes(`--${name}`) || args.includes(`-${name[0]}`);
|
|
48
33
|
|
|
49
34
|
function getPositionalArgs() {
|
|
50
35
|
const positional = [];
|
|
51
36
|
for (let i = 0; i < args.length; i++) {
|
|
52
|
-
if (args[i].startsWith('
|
|
53
|
-
|
|
54
|
-
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
55
|
-
|
|
56
|
-
// Only skip value for flags that take one (not boolean flags like --help)
|
|
57
|
-
if (['server', 'app', 'bucket', 'profile-key', 'expires'].includes(long)) {
|
|
58
|
-
i++;
|
|
59
|
-
}
|
|
37
|
+
if (args[i].startsWith('-')) {
|
|
38
|
+
const flagName = args[i].replace(/^-+/, '');
|
|
39
|
+
if (VALUE_FLAGS.has(flagName) && i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
40
|
+
i++;
|
|
60
41
|
}
|
|
61
42
|
} else {
|
|
62
43
|
positional.push(args[i]);
|
|
@@ -69,21 +50,7 @@ const command = getPositionalArgs()[0] || 'help';
|
|
|
69
50
|
const cmdArgs = getPositionalArgs().slice(1);
|
|
70
51
|
|
|
71
52
|
// ---------------------------------------------------------------------------
|
|
72
|
-
//
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
import {
|
|
75
|
-
readConfig,
|
|
76
|
-
clearConfig,
|
|
77
|
-
updateConfig,
|
|
78
|
-
writeConfig,
|
|
79
|
-
isLoggedIn,
|
|
80
|
-
deviceFlowLogin,
|
|
81
|
-
CONFIG_FILE,
|
|
82
|
-
CONFIG_DIR,
|
|
83
|
-
} from './src/cli/auth-helper.js';
|
|
84
|
-
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Help text
|
|
53
|
+
// Help text & S3 helper
|
|
87
54
|
// ---------------------------------------------------------------------------
|
|
88
55
|
function showHelp() {
|
|
89
56
|
console.log(`
|
|
@@ -132,140 +99,93 @@ function showHelp() {
|
|
|
132
99
|
`);
|
|
133
100
|
}
|
|
134
101
|
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
// S3 helper — conditionally imports S3 service after env is set up
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
102
|
let _s3Module = null;
|
|
139
103
|
async function getS3() {
|
|
140
104
|
if (_s3Module) return _s3Module;
|
|
141
|
-
// Set APP_NAME from CLI flag before importing
|
|
142
105
|
const appFlag = findFlag('app');
|
|
143
|
-
if (appFlag && typeof appFlag === 'string')
|
|
144
|
-
process.env.APP_NAME = appFlag;
|
|
145
|
-
}
|
|
106
|
+
if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
|
|
146
107
|
_s3Module = await import('./src/services/s3.service.js');
|
|
147
108
|
return _s3Module;
|
|
148
109
|
}
|
|
149
110
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
156
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
157
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
|
|
111
|
+
function walkDir(dir) {
|
|
112
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(dirent => {
|
|
113
|
+
const fullPath = path.join(dir, dirent.name);
|
|
114
|
+
return dirent.isDirectory() ? walkDir(fullPath) : [fullPath];
|
|
115
|
+
});
|
|
158
116
|
}
|
|
159
117
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
118
|
+
function promptLine() {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const { stdin, stdout } = process;
|
|
121
|
+
stdin.resume();
|
|
122
|
+
stdout.write('> ');
|
|
123
|
+
stdin.once('data', (data) => {
|
|
124
|
+
stdin.pause();
|
|
125
|
+
resolve(data.toString().trim());
|
|
126
|
+
});
|
|
127
|
+
});
|
|
166
128
|
}
|
|
167
129
|
|
|
168
130
|
// ---------------------------------------------------------------------------
|
|
169
|
-
// Command
|
|
131
|
+
// Command Implementations
|
|
170
132
|
// ---------------------------------------------------------------------------
|
|
171
133
|
|
|
172
134
|
async function cmdLogin() {
|
|
173
|
-
// Use --server flag, or fall back to stored config
|
|
174
|
-
let serverUrl = findFlag('server');
|
|
175
135
|
const storedCfg = readConfig();
|
|
136
|
+
let server_url = findFlag('server') || storedCfg.server_url;
|
|
176
137
|
|
|
177
|
-
if (!
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.log(`ℹ️ Using stored server: ${serverUrl}`);
|
|
181
|
-
} else {
|
|
182
|
-
console.error('❌ --server <url> is required for login.');
|
|
183
|
-
console.error(' Example: mbkbucket login --server https://myapp.example.com');
|
|
184
|
-
console.error('');
|
|
185
|
-
console.error('💡 Tip: Set it once with:');
|
|
186
|
-
console.error(' mbkbucket config set serverUrl https://your-server.com');
|
|
187
|
-
console.error(' Then just run: mbkbucket login');
|
|
188
|
-
process.exit(1);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Use --profile-key flag, or fall back to stored config
|
|
193
|
-
let profileKey = findFlag('profile-key');
|
|
194
|
-
if (!profileKey && storedCfg.profileKey) {
|
|
195
|
-
profileKey = storedCfg.profileKey;
|
|
138
|
+
if (!server_url || typeof server_url !== 'string') {
|
|
139
|
+
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');
|
|
140
|
+
process.exit(1);
|
|
196
141
|
}
|
|
197
142
|
|
|
198
|
-
|
|
199
|
-
updateConfig({
|
|
143
|
+
const profile_key = findFlag('profile-key') || storedCfg.profile_key;
|
|
144
|
+
updateConfig({ server_url });
|
|
200
145
|
|
|
201
146
|
try {
|
|
202
147
|
const result = await deviceFlowLogin({
|
|
203
|
-
|
|
204
|
-
|
|
148
|
+
server_url,
|
|
149
|
+
profile_key: typeof profile_key === 'string' ? profile_key : undefined,
|
|
205
150
|
});
|
|
206
151
|
|
|
207
152
|
updateConfig({
|
|
208
153
|
token: result.token,
|
|
209
|
-
|
|
154
|
+
token_prefix: result.token_prefix,
|
|
210
155
|
username: result.username,
|
|
211
|
-
|
|
156
|
+
server_url,
|
|
212
157
|
profile: result.profile || null,
|
|
213
|
-
|
|
158
|
+
logged_in_at: nowIso(),
|
|
214
159
|
});
|
|
215
160
|
|
|
216
|
-
console.log('');
|
|
217
|
-
console.log('✅ Credentials saved. You are now logged in.');
|
|
218
|
-
console.log(` Run "mbkbucket whoami" to verify.`);
|
|
161
|
+
console.log('\n✅ Credentials saved. You are now logged in.\n Run "mbkbucket whoami" to verify.');
|
|
219
162
|
} catch (err) {
|
|
220
163
|
const msg = err.message || '';
|
|
221
164
|
console.error(`❌ Login failed: ${msg}`);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (msg.includes('profileKey') || msg.includes('profileId')) {
|
|
225
|
-
console.error('');
|
|
226
|
-
console.error('💡 An API token profile is required. You need to:');
|
|
227
|
-
console.error(' 1. Create an API token profile in the mbkauthe admin panel');
|
|
228
|
-
console.error(' 2. Run: mbkbucket login --profile-key <your-profile-key>');
|
|
229
|
-
console.error('');
|
|
230
|
-
console.error(' Or save it for future use:');
|
|
231
|
-
console.error(' mbkbucket config set profileKey <your-profile-key>');
|
|
165
|
+
if (msg.includes('profile_key') || msg.includes('profile_id')) {
|
|
166
|
+
console.error('\n💡 An API token profile is required. Create one in mbkauthe admin panel and pass --profile-key.');
|
|
232
167
|
} else if (msg.includes('fetch') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
233
|
-
console.error(
|
|
234
|
-
console.error('💡 Could not reach the server. Check:');
|
|
235
|
-
console.error(' 1. Is the server running?');
|
|
236
|
-
console.error(` 2. Is "${serverUrl}" the correct URL?`);
|
|
237
|
-
console.error(' 3. Is CLI_AUTH_ENABLED=true on the server?');
|
|
168
|
+
console.error(`\n💡 Could not reach server "${server_url}". Verify it is running and CLI_AUTH_ENABLED=true.`);
|
|
238
169
|
}
|
|
239
|
-
|
|
240
|
-
console.error('');
|
|
241
|
-
console.error(` Server URL saved. Run "mbkbucket login" to retry.`);
|
|
242
170
|
process.exit(1);
|
|
243
171
|
}
|
|
244
172
|
}
|
|
245
173
|
|
|
246
174
|
function cmdLogout() {
|
|
247
|
-
if (!isLoggedIn())
|
|
248
|
-
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const cfg = readConfig();
|
|
175
|
+
if (!isLoggedIn()) return console.log('ℹ️ Not logged in.');
|
|
176
|
+
const { username = 'user' } = readConfig();
|
|
252
177
|
clearConfig();
|
|
253
|
-
console.log(`👋 Logged out. Goodbye, ${
|
|
178
|
+
console.log(`👋 Logged out. Goodbye, ${username}!`);
|
|
254
179
|
}
|
|
255
180
|
|
|
256
181
|
function cmdWhoami() {
|
|
257
|
-
if (!isLoggedIn())
|
|
258
|
-
console.log('ℹ️ Not logged in. Run "mbkbucket login --server <url>" to authenticate.');
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
182
|
+
if (!isLoggedIn()) return console.log('ℹ️ Not logged in. Run "mbkbucket login --server <url>" to authenticate.');
|
|
261
183
|
const cfg = readConfig();
|
|
262
184
|
console.log(`✅ Logged in as: ${cfg.username}`);
|
|
263
|
-
console.log(` Server: ${cfg.
|
|
264
|
-
console.log(` Token: ${cfg.
|
|
265
|
-
if (cfg.profile) {
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
console.log(` Since: ${formatDate(cfg.loggedInAt)}`);
|
|
185
|
+
console.log(` Server: ${cfg.server_url}`);
|
|
186
|
+
console.log(` Token: ${cfg.token_prefix}...`);
|
|
187
|
+
if (cfg.profile) console.log(` Profile: ${cfg.profile.name} (scope: ${cfg.profile.scope || 'full'})`);
|
|
188
|
+
console.log(` Since: ${formatDate(cfg.logged_in_at)}`);
|
|
269
189
|
}
|
|
270
190
|
|
|
271
191
|
async function cmdList() {
|
|
@@ -276,17 +196,13 @@ async function cmdList() {
|
|
|
276
196
|
const s3 = await getS3();
|
|
277
197
|
const result = await s3.listfiles(prefix, { bucketName, delimiter: '/' });
|
|
278
198
|
|
|
279
|
-
if (!result.Contents
|
|
280
|
-
console.log(`📭 No files found${prefix ? ` with prefix "${prefix}"` : ''}.`);
|
|
281
|
-
return;
|
|
199
|
+
if (!result.Contents?.length) {
|
|
200
|
+
return console.log(`📭 No files found${prefix ? ` with prefix "${prefix}"` : ''}.`);
|
|
282
201
|
}
|
|
283
202
|
|
|
284
|
-
console.log('');
|
|
285
|
-
console.log(`📁 Files${prefix ? ` matching "${prefix}"` : ''} (${result.KeyCount || result.Contents.length} items):`);
|
|
286
|
-
console.log('─'.repeat(80));
|
|
203
|
+
console.log(`\n📁 Files${prefix ? ` matching "${prefix}"` : ''} (${result.KeyCount || result.Contents.length} items):\n` + '─'.repeat(80));
|
|
287
204
|
|
|
288
|
-
|
|
289
|
-
if (result.CommonPrefixes && result.CommonPrefixes.length > 0) {
|
|
205
|
+
if (result.CommonPrefixes?.length) {
|
|
290
206
|
for (const cp of result.CommonPrefixes) {
|
|
291
207
|
const folderName = cp.Prefix.replace(/\/$/, '').split('/').pop() || cp.Prefix;
|
|
292
208
|
console.log(` 📂 ${folderName}/`);
|
|
@@ -296,16 +212,12 @@ async function cmdList() {
|
|
|
296
212
|
|
|
297
213
|
for (const obj of result.Contents) {
|
|
298
214
|
const name = obj.Key || '';
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
const displayName = name.length > 60 ? '...' + name.slice(-57) : name;
|
|
302
|
-
console.log(` ${displayName.padEnd(62)} ${size.padStart(10)} ${modified}`);
|
|
215
|
+
const displayName = name.length > 60 ? `...${name.slice(-57)}` : name;
|
|
216
|
+
console.log(` ${displayName.padEnd(62)} ${formatBytes(obj.Size).padStart(10)} ${formatDate(obj.LastModified)}`);
|
|
303
217
|
}
|
|
304
218
|
|
|
305
219
|
console.log('─'.repeat(80));
|
|
306
|
-
if (result.hasMore)
|
|
307
|
-
console.log(` ... more files available (use --prefix to narrow down)`);
|
|
308
|
-
}
|
|
220
|
+
if (result.hasMore) console.log(' ... more files available (use --prefix to narrow down)');
|
|
309
221
|
console.log('');
|
|
310
222
|
} catch (err) {
|
|
311
223
|
console.error(`❌ List failed: ${err.message}`);
|
|
@@ -316,54 +228,34 @@ async function cmdList() {
|
|
|
316
228
|
async function cmdUpload() {
|
|
317
229
|
const filePath = cmdArgs[0];
|
|
318
230
|
if (!filePath) {
|
|
319
|
-
console.error('❌ Usage: mbkbucket upload <file> [key]');
|
|
320
|
-
console.error(' Example: mbkbucket upload ./photo.jpg');
|
|
321
|
-
console.error(' Example: mbkbucket upload ./photo.jpg images/photo.jpg');
|
|
231
|
+
console.error('❌ Usage: mbkbucket upload <file> [key]\n Example: mbkbucket upload ./photo.jpg images/photo.jpg');
|
|
322
232
|
process.exit(1);
|
|
323
233
|
}
|
|
324
234
|
|
|
325
|
-
const
|
|
326
|
-
|
|
235
|
+
const resolved = path.resolve(filePath);
|
|
236
|
+
if (!fs.existsSync(resolved)) {
|
|
237
|
+
console.error(`❌ File not found: ${filePath}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
327
240
|
|
|
328
|
-
if (
|
|
329
|
-
await uploadFolder(
|
|
241
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
242
|
+
await uploadFolder(resolved);
|
|
330
243
|
} else {
|
|
331
|
-
await uploadSingleFile(
|
|
244
|
+
await uploadSingleFile(resolved);
|
|
332
245
|
}
|
|
333
246
|
}
|
|
334
247
|
|
|
335
248
|
async function uploadSingleFile(filePath) {
|
|
336
249
|
const bucketName = findFlag('bucket');
|
|
337
|
-
const customKey = cmdArgs[1] || null;
|
|
338
|
-
|
|
339
|
-
if (!fs.existsSync(filePath)) {
|
|
340
|
-
console.error(`❌ File not found: ${filePath}`);
|
|
341
|
-
process.exit(1);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
250
|
const stat = fs.statSync(filePath);
|
|
345
251
|
const fileName = path.basename(filePath);
|
|
346
|
-
const key =
|
|
347
|
-
|
|
348
|
-
// Determine MIME type from extension
|
|
349
|
-
const ext = path.extname(fileName).slice(1).toLowerCase();
|
|
350
|
-
const mimeMap = {
|
|
351
|
-
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
|
|
352
|
-
'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml',
|
|
353
|
-
'mp4': 'video/mp4', 'webm': 'video/webm', 'mp3': 'audio/mpeg',
|
|
354
|
-
'wav': 'audio/wav', 'pdf': 'application/pdf', 'json': 'application/json',
|
|
355
|
-
'js': 'application/javascript', 'css': 'text/css', 'html': 'text/html',
|
|
356
|
-
'txt': 'text/plain', 'md': 'text/markdown', 'csv': 'text/csv',
|
|
357
|
-
'zip': 'application/zip', 'gz': 'application/gzip',
|
|
358
|
-
};
|
|
359
|
-
const contentType = mimeMap[ext] || 'application/octet-stream';
|
|
252
|
+
const key = cmdArgs[1] || fileName;
|
|
253
|
+
const contentType = getMimeType(fileName);
|
|
360
254
|
|
|
361
255
|
console.log(`⬆️ Uploading: ${fileName} (${formatBytes(stat.size)}) → ${key}`);
|
|
362
|
-
|
|
363
256
|
try {
|
|
364
257
|
const s3 = await getS3();
|
|
365
|
-
const
|
|
366
|
-
const result = await s3.uploadFile(key, buffer, contentType, { bucketName });
|
|
258
|
+
const result = await s3.uploadFile(key, fs.readFileSync(filePath), contentType, { bucketName });
|
|
367
259
|
console.log(`✅ Uploaded: ${result.key} (${formatBytes(result.fileSize)})`);
|
|
368
260
|
} catch (err) {
|
|
369
261
|
console.error(`❌ Upload failed: ${err.message}`);
|
|
@@ -374,63 +266,32 @@ async function uploadSingleFile(filePath) {
|
|
|
374
266
|
async function uploadFolder(folderPath) {
|
|
375
267
|
const bucketName = findFlag('bucket');
|
|
376
268
|
const prefix = cmdArgs[1] || path.basename(folderPath);
|
|
377
|
-
|
|
378
|
-
// Recursively collect all files
|
|
379
269
|
const files = walkDir(folderPath);
|
|
380
270
|
|
|
381
|
-
if (files.length
|
|
382
|
-
console.log('📭 No files found in folder.');
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
console.log(`📁 Uploading folder: ${folderPath} (${files.length} files) → ${prefix}/`);
|
|
387
|
-
console.log('');
|
|
271
|
+
if (!files.length) return console.log('📭 No files found in folder.');
|
|
388
272
|
|
|
273
|
+
console.log(`📁 Uploading folder: ${folderPath} (${files.length} files) → ${prefix}/\n`);
|
|
389
274
|
let uploaded = 0;
|
|
390
275
|
let failed = 0;
|
|
391
276
|
|
|
392
277
|
for (const file of files) {
|
|
393
|
-
const
|
|
394
|
-
const key = `${prefix}/${
|
|
395
|
-
const stat = fs.statSync(file);
|
|
396
|
-
const ext = path.extname(file).slice(1).toLowerCase();
|
|
397
|
-
const mimeMap = {
|
|
398
|
-
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
|
|
399
|
-
'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml',
|
|
400
|
-
};
|
|
401
|
-
const contentType = mimeMap[ext] || 'application/octet-stream';
|
|
402
|
-
|
|
278
|
+
const rel = path.relative(folderPath, file).replace(/\\/g, '/');
|
|
279
|
+
const key = `${prefix}/${rel}`;
|
|
403
280
|
try {
|
|
404
281
|
const s3 = await getS3();
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
console.log(` ✅ ${relativePath}`);
|
|
282
|
+
await s3.uploadFile(key, fs.readFileSync(file), getMimeType(file), { bucketName });
|
|
283
|
+
console.log(` ✅ ${rel}`);
|
|
408
284
|
uploaded++;
|
|
409
285
|
} catch (err) {
|
|
410
|
-
console.error(` ❌ ${
|
|
286
|
+
console.error(` ❌ ${rel}: ${err.message}`);
|
|
411
287
|
failed++;
|
|
412
288
|
}
|
|
413
289
|
}
|
|
414
290
|
|
|
415
|
-
console.log(
|
|
416
|
-
console.log(`Done: ${uploaded} uploaded, ${failed} failed.`);
|
|
291
|
+
console.log(`\nDone: ${uploaded} uploaded, ${failed} failed.`);
|
|
417
292
|
if (failed > 0) process.exit(1);
|
|
418
293
|
}
|
|
419
294
|
|
|
420
|
-
function walkDir(dir) {
|
|
421
|
-
const results = [];
|
|
422
|
-
const list = fs.readdirSync(dir);
|
|
423
|
-
for (const item of list) {
|
|
424
|
-
const fullPath = path.join(dir, item);
|
|
425
|
-
if (fs.statSync(fullPath).isDirectory()) {
|
|
426
|
-
results.push(...walkDir(fullPath));
|
|
427
|
-
} else {
|
|
428
|
-
results.push(fullPath);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return results;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
295
|
async function cmdDownload() {
|
|
435
296
|
const key = cmdArgs[0];
|
|
436
297
|
if (!key) {
|
|
@@ -443,29 +304,15 @@ async function cmdDownload() {
|
|
|
443
304
|
|
|
444
305
|
try {
|
|
445
306
|
const s3 = await getS3();
|
|
307
|
+
if (key.endsWith('/')) return downloadFolder(s3, key, dest, bucketName);
|
|
446
308
|
|
|
447
|
-
// Check if it's a folder (key ends with / or has children)
|
|
448
|
-
const isFolderLike = key.endsWith('/');
|
|
449
|
-
if (isFolderLike) {
|
|
450
|
-
await downloadFolder(s3, key, dest, bucketName);
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Single file download
|
|
455
309
|
console.log(`⬇️ Downloading: ${key} → ${dest}`);
|
|
456
310
|
const result = await s3.downloadFile(key, { bucketName });
|
|
311
|
+
if (result.notModified) return console.log('ℹ️ File not modified.');
|
|
457
312
|
|
|
458
|
-
if (result.notModified) {
|
|
459
|
-
console.log('ℹ️ File not modified.');
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Read the body stream
|
|
464
313
|
const body = await streamToBuffer(result.Body);
|
|
465
314
|
const destDir = path.dirname(path.resolve(dest));
|
|
466
|
-
if (!fs.existsSync(destDir)) {
|
|
467
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
468
|
-
}
|
|
315
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
469
316
|
fs.writeFileSync(path.resolve(dest), body);
|
|
470
317
|
|
|
471
318
|
console.log(`✅ Downloaded: ${dest} (${formatBytes(body.length)})`);
|
|
@@ -476,38 +323,27 @@ async function cmdDownload() {
|
|
|
476
323
|
}
|
|
477
324
|
|
|
478
325
|
async function downloadFolder(s3, prefix, destDir, bucketName) {
|
|
479
|
-
// List all files under the prefix
|
|
480
326
|
const allFiles = [];
|
|
481
327
|
let token = null;
|
|
482
328
|
const resolvedDest = path.resolve(destDir);
|
|
483
329
|
|
|
484
330
|
do {
|
|
485
331
|
const result = await s3.listfiles(prefix, { bucketName, continuationToken: token });
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
allFiles.push(obj.Key);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
332
|
+
(result.Contents || []).forEach(obj => {
|
|
333
|
+
if (obj.Key && obj.Key !== prefix) allFiles.push(obj.Key);
|
|
334
|
+
});
|
|
493
335
|
token = result.IsTruncated ? result.NextContinuationToken : null;
|
|
494
336
|
} while (token);
|
|
495
337
|
|
|
496
|
-
if (allFiles.length
|
|
497
|
-
console.log(`📭 No files found with prefix "${prefix}".`);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
console.log(`📁 Downloading ${allFiles.length} files to ${resolvedDest}/`);
|
|
502
|
-
console.log('');
|
|
338
|
+
if (!allFiles.length) return console.log(`📭 No files found with prefix "${prefix}".`);
|
|
503
339
|
|
|
340
|
+
console.log(`📁 Downloading ${allFiles.length} files to ${resolvedDest}/\n`);
|
|
504
341
|
let downloaded = 0;
|
|
505
342
|
let failed = 0;
|
|
506
343
|
|
|
507
344
|
for (const fileKey of allFiles) {
|
|
508
345
|
const relativePath = fileKey.startsWith(prefix) ? fileKey.slice(prefix.length) : fileKey;
|
|
509
346
|
const filePath = path.join(resolvedDest, relativePath);
|
|
510
|
-
|
|
511
347
|
try {
|
|
512
348
|
const result = await s3.downloadFile(fileKey, { bucketName });
|
|
513
349
|
if (!result.notModified) {
|
|
@@ -524,32 +360,10 @@ async function downloadFolder(s3, prefix, destDir, bucketName) {
|
|
|
524
360
|
}
|
|
525
361
|
}
|
|
526
362
|
|
|
527
|
-
console.log(
|
|
528
|
-
console.log(`Done: ${downloaded} downloaded, ${failed} failed.`);
|
|
363
|
+
console.log(`\nDone: ${downloaded} downloaded, ${failed} failed.`);
|
|
529
364
|
if (failed > 0) process.exit(1);
|
|
530
365
|
}
|
|
531
366
|
|
|
532
|
-
async function streamToBuffer(stream) {
|
|
533
|
-
if (Buffer.isBuffer(stream)) return stream;
|
|
534
|
-
if (stream?.transformToWeb) {
|
|
535
|
-
// AWS SDK v3 sometimes returns Node.js Readable
|
|
536
|
-
const chunks = [];
|
|
537
|
-
for await (const chunk of stream) {
|
|
538
|
-
chunks.push(chunk);
|
|
539
|
-
}
|
|
540
|
-
return Buffer.concat(chunks);
|
|
541
|
-
}
|
|
542
|
-
// Web ReadableStream
|
|
543
|
-
const reader = stream.getReader();
|
|
544
|
-
const chunks = [];
|
|
545
|
-
while (true) {
|
|
546
|
-
const { done, value } = await reader.read();
|
|
547
|
-
if (done) break;
|
|
548
|
-
chunks.push(value);
|
|
549
|
-
}
|
|
550
|
-
return Buffer.concat(chunks);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
367
|
async function cmdDelete() {
|
|
554
368
|
const key = cmdArgs[0];
|
|
555
369
|
if (!key) {
|
|
@@ -557,12 +371,10 @@ async function cmdDelete() {
|
|
|
557
371
|
process.exit(1);
|
|
558
372
|
}
|
|
559
373
|
|
|
560
|
-
const bucketName = findFlag('bucket');
|
|
561
|
-
|
|
562
374
|
try {
|
|
563
375
|
const s3 = await getS3();
|
|
564
376
|
console.log(`🗑️ Deleting: ${key}`);
|
|
565
|
-
const result = await s3.deleteFile(key,
|
|
377
|
+
const result = await s3.deleteFile(key, findFlag('bucket'));
|
|
566
378
|
console.log(`✅ Deleted: ${result.key}`);
|
|
567
379
|
} catch (err) {
|
|
568
380
|
console.error(`❌ Delete failed: ${err.message}`);
|
|
@@ -577,24 +389,17 @@ async function cmdDeleteFolder() {
|
|
|
577
389
|
process.exit(1);
|
|
578
390
|
}
|
|
579
391
|
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
console.log(`⚠️ This will delete ALL files under "${cleanedPrefix}/".`);
|
|
584
|
-
console.log(` Are you sure? Type "yes" to confirm:`);
|
|
392
|
+
const cleaned = trimSlashes(prefix);
|
|
393
|
+
console.log(`⚠️ This will delete ALL files under "${cleaned}/".\n Are you sure? Type "yes" to confirm:`);
|
|
585
394
|
|
|
586
|
-
// Simple confirmation
|
|
587
395
|
const confirmed = await promptLine();
|
|
588
|
-
if (confirmed.trim().toLowerCase() !== 'yes')
|
|
589
|
-
console.log('Cancelled.');
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
396
|
+
if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
|
|
592
397
|
|
|
593
398
|
try {
|
|
594
399
|
const s3 = await getS3();
|
|
595
|
-
console.log(`🗑️ Deleting folder: ${
|
|
596
|
-
const result = await s3.deleteFolder(
|
|
597
|
-
console.log(`✅ Deleted ${result.deletedCount} file(s) under "${
|
|
400
|
+
console.log(`🗑️ Deleting folder: ${cleaned}/`);
|
|
401
|
+
const result = await s3.deleteFolder(cleaned, findFlag('bucket'));
|
|
402
|
+
console.log(`✅ Deleted ${result.deletedCount} file(s) under "${cleaned}/".`);
|
|
598
403
|
} catch (err) {
|
|
599
404
|
console.error(`❌ Delete folder failed: ${err.message}`);
|
|
600
405
|
process.exit(1);
|
|
@@ -608,20 +413,15 @@ async function cmdInfo() {
|
|
|
608
413
|
process.exit(1);
|
|
609
414
|
}
|
|
610
415
|
|
|
611
|
-
const bucketName = findFlag('bucket');
|
|
612
|
-
|
|
613
416
|
try {
|
|
614
417
|
const s3 = await getS3();
|
|
615
|
-
const meta = await s3.getFileMetadata(key,
|
|
616
|
-
|
|
418
|
+
const meta = await s3.getFileMetadata(key, findFlag('bucket'));
|
|
617
419
|
if (!meta.exists) {
|
|
618
420
|
console.log(`❌ File not found: ${key}`);
|
|
619
421
|
process.exit(1);
|
|
620
422
|
}
|
|
621
423
|
|
|
622
|
-
console.log('');
|
|
623
|
-
console.log(`📄 ${meta.key}`);
|
|
624
|
-
console.log('─'.repeat(60));
|
|
424
|
+
console.log(`\n📄 ${meta.key}\n` + '─'.repeat(60));
|
|
625
425
|
console.log(` Size: ${formatBytes(meta.ContentLength)}`);
|
|
626
426
|
console.log(` Type: ${meta.ContentType || 'unknown'}`);
|
|
627
427
|
console.log(` Last Modified: ${formatDate(meta.LastModified)}`);
|
|
@@ -629,9 +429,7 @@ async function cmdInfo() {
|
|
|
629
429
|
console.log(` Cache-Control: ${meta.CacheControl || '—'}`);
|
|
630
430
|
if (meta.Metadata && Object.keys(meta.Metadata).length > 0) {
|
|
631
431
|
console.log(' Metadata:');
|
|
632
|
-
for (const [k, v] of Object.entries(meta.Metadata)) {
|
|
633
|
-
console.log(` ${k}: ${v}`);
|
|
634
|
-
}
|
|
432
|
+
for (const [k, v] of Object.entries(meta.Metadata)) console.log(` ${k}: ${v}`);
|
|
635
433
|
}
|
|
636
434
|
console.log('');
|
|
637
435
|
} catch (err) {
|
|
@@ -640,49 +438,51 @@ async function cmdInfo() {
|
|
|
640
438
|
}
|
|
641
439
|
}
|
|
642
440
|
|
|
441
|
+
async function cmdSignedUrl() {
|
|
442
|
+
const key = cmdArgs[0];
|
|
443
|
+
if (!key) {
|
|
444
|
+
console.error('❌ Usage: mbkbucket signed-url <key> [--expires <seconds>]');
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const expires = parseInt(findFlag('expires'), 10) || 3600;
|
|
449
|
+
try {
|
|
450
|
+
const s3 = await getS3();
|
|
451
|
+
console.log(`🔗 Generating signed URL (expires in ${expires}s)...`);
|
|
452
|
+
const result = await s3.generateSignedUrl(key, 'getObject', expires, findFlag('bucket'));
|
|
453
|
+
console.log(`\n${result.url || result}`);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
console.error(`❌ Signed URL failed: ${err.message}`);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
643
460
|
// ---------------------------------------------------------------------------
|
|
644
|
-
// Config
|
|
461
|
+
// Config Commands
|
|
645
462
|
// ---------------------------------------------------------------------------
|
|
646
|
-
|
|
647
463
|
const DEMO_CONFIG = {
|
|
648
464
|
token: 'mbk_demo_token_replace_with_real_one_from_login',
|
|
649
|
-
|
|
465
|
+
token_prefix: 'mbk_demo',
|
|
650
466
|
username: 'demo-user',
|
|
651
|
-
|
|
652
|
-
profile: {
|
|
653
|
-
|
|
654
|
-
scope: 'read-write',
|
|
655
|
-
allowedApps: ['mbkbucket'],
|
|
656
|
-
},
|
|
657
|
-
loggedInAt: new Date().toISOString(),
|
|
467
|
+
server_url: 'https://your-app.example.com',
|
|
468
|
+
profile: { name: 'Default CLI Profile', scope: 'read-write', allowed_apps: ['mbkbucket'] },
|
|
469
|
+
logged_in_at: nowIso(),
|
|
658
470
|
};
|
|
659
471
|
|
|
660
|
-
const VALID_CONFIG_KEYS = ['token', '
|
|
472
|
+
const VALID_CONFIG_KEYS = ['token', 'token_prefix', 'username', 'server_url', 'profile', 'profile_key', 'logged_in_at'];
|
|
661
473
|
|
|
662
474
|
async function cmdConfig() {
|
|
663
|
-
const sub = cmdArgs
|
|
664
|
-
const key = cmdArgs[1];
|
|
665
|
-
const value = cmdArgs[2];
|
|
666
|
-
|
|
475
|
+
const [sub, key, value] = cmdArgs;
|
|
667
476
|
switch (sub) {
|
|
668
|
-
case 'get':
|
|
669
|
-
|
|
670
|
-
case 'set':
|
|
671
|
-
return cmdConfigSet(key, value);
|
|
477
|
+
case 'get': return cmdConfigGet(key);
|
|
478
|
+
case 'set': return cmdConfigSet(key, value);
|
|
672
479
|
case 'unset':
|
|
673
480
|
case 'del':
|
|
674
|
-
case 'delete':
|
|
675
|
-
|
|
676
|
-
case '
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
return cmdConfigReset();
|
|
680
|
-
case 'edit':
|
|
681
|
-
await cmdConfigEdit();
|
|
682
|
-
break;
|
|
683
|
-
default:
|
|
684
|
-
cmdConfigShow();
|
|
685
|
-
break;
|
|
481
|
+
case 'delete': return cmdConfigUnset(key);
|
|
482
|
+
case 'path': return cmdConfigPath();
|
|
483
|
+
case 'reset': return cmdConfigReset();
|
|
484
|
+
case 'edit': return cmdConfigEdit();
|
|
485
|
+
default: return cmdConfigShow();
|
|
686
486
|
}
|
|
687
487
|
}
|
|
688
488
|
|
|
@@ -690,83 +490,46 @@ function cmdConfigShow() {
|
|
|
690
490
|
const cfg = readConfig();
|
|
691
491
|
const entries = Object.keys(cfg).filter(k => !k.startsWith('_'));
|
|
692
492
|
|
|
693
|
-
if (entries.length
|
|
694
|
-
console.log(
|
|
695
|
-
console.log(` File: ${CONFIG_FILE}`);
|
|
696
|
-
console.log(' Run "mbkbucket config reset" to populate demo values.');
|
|
493
|
+
if (!entries.length) {
|
|
494
|
+
console.log(`📭 No config values set.\n File: ${CONFIG_FILE}\n Run "mbkbucket config reset" to populate demo values.`);
|
|
697
495
|
return;
|
|
698
496
|
}
|
|
699
497
|
|
|
700
|
-
console.log('');
|
|
701
|
-
console.log(`📋 Config (${CONFIG_FILE}):`);
|
|
702
|
-
console.log('─'.repeat(60));
|
|
703
|
-
|
|
498
|
+
console.log(`\n📋 Config (${CONFIG_FILE}):\n` + '─'.repeat(60));
|
|
704
499
|
for (const k of entries.sort()) {
|
|
705
500
|
const v = cfg[k];
|
|
706
|
-
const display = typeof v === 'object' && v !== null
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (k === 'token' && display.length > 12) {
|
|
712
|
-
console.log(` ${k.padEnd(14)} ${display.slice(0, 8)}...${display.slice(-4)}`);
|
|
713
|
-
} else {
|
|
714
|
-
const truncated = display.length > 55 ? display.slice(0, 52) + '...' : display;
|
|
715
|
-
console.log(` ${k.padEnd(14)} ${truncated}`);
|
|
716
|
-
}
|
|
501
|
+
const display = typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);
|
|
502
|
+
const masked = (k === 'token' && display.length > 12)
|
|
503
|
+
? `${display.slice(0, 8)}...${display.slice(-4)}`
|
|
504
|
+
: display.length > 55 ? `${display.slice(0, 52)}...` : display;
|
|
505
|
+
console.log(` ${k.padEnd(14)} ${masked}`);
|
|
717
506
|
}
|
|
718
|
-
|
|
719
|
-
console.log('─'.repeat(60));
|
|
720
|
-
console.log(' Run "mbkbucket config set <key> <value>" to change a value.');
|
|
721
|
-
console.log(' Run "mbkbucket config edit" to open in your editor.');
|
|
722
|
-
console.log('');
|
|
507
|
+
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');
|
|
723
508
|
}
|
|
724
509
|
|
|
725
510
|
function cmdConfigGet(key) {
|
|
726
|
-
if (!key) {
|
|
727
|
-
console.error(
|
|
728
|
-
console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
|
|
511
|
+
if (!key || !VALID_CONFIG_KEYS.includes(key)) {
|
|
512
|
+
console.error(`❌ Usage: mbkbucket config get <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
729
513
|
process.exit(1);
|
|
730
514
|
}
|
|
731
|
-
|
|
732
515
|
const cfg = readConfig();
|
|
733
516
|
if (!(key in cfg)) {
|
|
734
|
-
console.error(`❌ Key not found: ${key}`);
|
|
735
|
-
console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
|
|
517
|
+
console.error(`❌ Key not found: ${key}\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
736
518
|
process.exit(1);
|
|
737
519
|
}
|
|
738
|
-
|
|
739
520
|
const v = cfg[key];
|
|
740
|
-
|
|
741
|
-
console.log(JSON.stringify(v, null, 2));
|
|
742
|
-
} else {
|
|
743
|
-
console.log(String(v));
|
|
744
|
-
}
|
|
521
|
+
console.log(typeof v === 'object' && v !== null ? JSON.stringify(v, null, 2) : String(v));
|
|
745
522
|
}
|
|
746
523
|
|
|
747
524
|
function cmdConfigSet(key, value) {
|
|
748
|
-
if (!key || value === undefined) {
|
|
749
|
-
console.error(
|
|
750
|
-
console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
|
|
751
|
-
console.error(' Example: mbkbucket config set serverUrl https://myapp.example.com');
|
|
752
|
-
process.exit(1);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
if (!VALID_CONFIG_KEYS.includes(key)) {
|
|
756
|
-
console.error(`❌ Invalid key: ${key}`);
|
|
757
|
-
console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
|
|
525
|
+
if (!key || value === undefined || !VALID_CONFIG_KEYS.includes(key)) {
|
|
526
|
+
console.error(`❌ Usage: mbkbucket config set <key> <value>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
758
527
|
process.exit(1);
|
|
759
528
|
}
|
|
760
529
|
|
|
761
|
-
// Parse JSON if it looks like an object/array
|
|
762
530
|
let parsed = value;
|
|
763
|
-
if ((value
|
|
764
|
-
|
|
765
|
-
try {
|
|
766
|
-
parsed = JSON.parse(value);
|
|
767
|
-
} catch {
|
|
768
|
-
// Keep as string if parse fails
|
|
769
|
-
}
|
|
531
|
+
if (/^[{\[].*[}\]]$/.test(value)) {
|
|
532
|
+
try { parsed = JSON.parse(value); } catch {}
|
|
770
533
|
}
|
|
771
534
|
|
|
772
535
|
updateConfig({ [key]: parsed });
|
|
@@ -775,198 +538,101 @@ function cmdConfigSet(key, value) {
|
|
|
775
538
|
|
|
776
539
|
function cmdConfigUnset(key) {
|
|
777
540
|
if (!key) {
|
|
778
|
-
console.error(
|
|
779
|
-
console.error(' Valid keys: ' + VALID_CONFIG_KEYS.join(', '));
|
|
541
|
+
console.error(`❌ Usage: mbkbucket config unset <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
780
542
|
process.exit(1);
|
|
781
543
|
}
|
|
782
|
-
|
|
783
544
|
const cfg = readConfig();
|
|
784
545
|
if (!(key in cfg)) {
|
|
785
546
|
console.error(`❌ Key not found: ${key}`);
|
|
786
547
|
process.exit(1);
|
|
787
548
|
}
|
|
788
|
-
|
|
789
549
|
delete cfg[key];
|
|
790
550
|
writeConfig(cfg);
|
|
791
551
|
console.log(`🗑️ Removed: ${key}`);
|
|
792
552
|
}
|
|
793
553
|
|
|
794
554
|
function cmdConfigPath() {
|
|
795
|
-
console.log(`📁 Config directory: ${CONFIG_DIR}`);
|
|
796
|
-
console.log(`📄 Config file: ${CONFIG_FILE}`);
|
|
555
|
+
console.log(`📁 Config directory: ${CONFIG_DIR}\n📄 Config file: ${CONFIG_FILE}`);
|
|
797
556
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
798
557
|
const stat = fs.statSync(CONFIG_FILE);
|
|
799
|
-
console.log(` Size: ${formatBytes(stat.size)}`);
|
|
800
|
-
console.log(` Modified: ${formatDate(stat.mtime.toISOString())}`);
|
|
801
|
-
}
|
|
802
|
-
if (!isLoggedIn()) {
|
|
803
|
-
console.log('');
|
|
804
|
-
console.log('💡 Tip: Run "mbkbucket login --server <url>" to authenticate.');
|
|
558
|
+
console.log(` Size: ${formatBytes(stat.size)}\n Modified: ${formatDate(stat.mtime.toISOString())}`);
|
|
805
559
|
}
|
|
806
560
|
}
|
|
807
561
|
|
|
808
562
|
async function cmdConfigReset() {
|
|
809
|
-
console.log('⚠️ This will overwrite your current config with demo values
|
|
810
|
-
console.log(' Are you sure? Type "yes" to confirm:');
|
|
811
|
-
|
|
563
|
+
console.log('⚠️ This will overwrite your current config with demo values.\n Are you sure? Type "yes" to confirm:');
|
|
812
564
|
const confirmed = await promptLine();
|
|
813
|
-
if (confirmed.trim().toLowerCase() !== 'yes')
|
|
814
|
-
console.log('Cancelled.');
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
|
|
565
|
+
if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
|
|
818
566
|
writeConfig({ ...DEMO_CONFIG });
|
|
819
|
-
console.log('✅ Config reset to demo values.');
|
|
820
|
-
console.log(' Run "mbkbucket config" to view.');
|
|
567
|
+
console.log('✅ Config reset to demo values.\n Run "mbkbucket config" to view.');
|
|
821
568
|
}
|
|
822
569
|
|
|
823
570
|
async function cmdConfigEdit() {
|
|
824
571
|
const editor = process.env.EDITOR || process.env.VISUAL || 'notepad';
|
|
825
|
-
|
|
826
|
-
if (!fs.existsSync(
|
|
827
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
828
|
-
}
|
|
829
|
-
if (!fs.existsSync(CONFIG_FILE)) {
|
|
830
|
-
writeConfig({ ...DEMO_CONFIG });
|
|
831
|
-
}
|
|
572
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
573
|
+
if (!fs.existsSync(CONFIG_FILE)) writeConfig({ ...DEMO_CONFIG });
|
|
832
574
|
|
|
833
575
|
console.log(`✏️ Opening ${CONFIG_FILE} with ${editor}...`);
|
|
834
|
-
|
|
835
576
|
const { spawn } = await import('node:child_process');
|
|
836
|
-
const child = spawn(editor, [CONFIG_FILE], {
|
|
837
|
-
stdio: 'inherit',
|
|
838
|
-
shell: true,
|
|
839
|
-
});
|
|
577
|
+
const child = spawn(editor, [CONFIG_FILE], { stdio: 'inherit', shell: true });
|
|
840
578
|
|
|
841
579
|
return new Promise((resolve) => {
|
|
842
580
|
child.on('exit', (code) => {
|
|
843
|
-
if (code === 0)
|
|
844
|
-
|
|
845
|
-
} else {
|
|
846
|
-
console.error(`⚠️ Editor exited with code ${code}.`);
|
|
847
|
-
}
|
|
581
|
+
if (code === 0) console.log('✅ Editor closed.');
|
|
582
|
+
else console.error(`⚠️ Editor exited with code ${code}.`);
|
|
848
583
|
resolve();
|
|
849
584
|
});
|
|
850
|
-
|
|
851
585
|
child.on('error', (err) => {
|
|
852
586
|
console.error(`❌ Could not open editor (${editor}): ${err.message}`);
|
|
853
|
-
console.error(' Set the EDITOR environment variable to your preferred editor.');
|
|
854
|
-
console.error(` Config file location: ${CONFIG_FILE}`);
|
|
855
587
|
resolve();
|
|
856
588
|
});
|
|
857
589
|
});
|
|
858
590
|
}
|
|
859
591
|
|
|
860
|
-
async function cmdSignedUrl() {
|
|
861
|
-
const key = cmdArgs[0];
|
|
862
|
-
if (!key) {
|
|
863
|
-
console.error('❌ Usage: mbkbucket signed-url <key> [--expires <seconds>]');
|
|
864
|
-
process.exit(1);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
const bucketName = findFlag('bucket');
|
|
868
|
-
const expires = parseInt(findFlag('expires'), 10) || 3600;
|
|
869
|
-
|
|
870
|
-
try {
|
|
871
|
-
const s3 = await getS3();
|
|
872
|
-
console.log(`🔗 Generating signed URL (expires in ${expires}s)...`);
|
|
873
|
-
const result = await s3.generateSignedUrl(key, 'getObject', expires, bucketName);
|
|
874
|
-
console.log('');
|
|
875
|
-
console.log(result.url || result);
|
|
876
|
-
} catch (err) {
|
|
877
|
-
console.error(`❌ Signed URL failed: ${err.message}`);
|
|
878
|
-
process.exit(1);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
592
|
// ---------------------------------------------------------------------------
|
|
883
|
-
//
|
|
593
|
+
// Main Router
|
|
884
594
|
// ---------------------------------------------------------------------------
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
595
|
+
const COMMANDS = {
|
|
596
|
+
login: cmdLogin,
|
|
597
|
+
logout: cmdLogout,
|
|
598
|
+
whoami: cmdWhoami,
|
|
599
|
+
list: cmdList,
|
|
600
|
+
ls: cmdList,
|
|
601
|
+
upload: cmdUpload,
|
|
602
|
+
up: cmdUpload,
|
|
603
|
+
download: cmdDownload,
|
|
604
|
+
dl: cmdDownload,
|
|
605
|
+
delete: cmdDelete,
|
|
606
|
+
rm: cmdDelete,
|
|
607
|
+
'delete-folder': cmdDeleteFolder,
|
|
608
|
+
rmdir: cmdDeleteFolder,
|
|
609
|
+
info: cmdInfo,
|
|
610
|
+
stat: cmdInfo,
|
|
611
|
+
'signed-url': cmdSignedUrl,
|
|
612
|
+
sign: cmdSignedUrl,
|
|
613
|
+
config: cmdConfig,
|
|
614
|
+
cfg: cmdConfig,
|
|
615
|
+
help: showHelp,
|
|
616
|
+
'--help': showHelp,
|
|
617
|
+
'-h': showHelp,
|
|
618
|
+
};
|
|
896
619
|
|
|
897
|
-
// ---------------------------------------------------------------------------
|
|
898
|
-
// Command router
|
|
899
|
-
// ---------------------------------------------------------------------------
|
|
900
620
|
async function main() {
|
|
901
|
-
|
|
902
|
-
if (hasFlag('help')) {
|
|
903
|
-
showHelp();
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
621
|
+
if (hasFlag('help')) return showHelp();
|
|
906
622
|
|
|
907
|
-
// Set APP_NAME from flag before any S3 import
|
|
908
623
|
const appFlag = findFlag('app');
|
|
909
|
-
if (appFlag && typeof appFlag === 'string')
|
|
910
|
-
process.env.APP_NAME = appFlag;
|
|
911
|
-
}
|
|
624
|
+
if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
|
|
912
625
|
|
|
913
626
|
const bucketFlag = findFlag('bucket');
|
|
914
|
-
if (bucketFlag && typeof bucketFlag === 'string')
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
await cmdLogin();
|
|
921
|
-
break;
|
|
922
|
-
case 'logout':
|
|
923
|
-
cmdLogout();
|
|
924
|
-
break;
|
|
925
|
-
case 'whoami':
|
|
926
|
-
cmdWhoami();
|
|
927
|
-
break;
|
|
928
|
-
case 'list':
|
|
929
|
-
case 'ls':
|
|
930
|
-
await cmdList();
|
|
931
|
-
break;
|
|
932
|
-
case 'upload':
|
|
933
|
-
case 'up':
|
|
934
|
-
await cmdUpload();
|
|
935
|
-
break;
|
|
936
|
-
case 'download':
|
|
937
|
-
case 'dl':
|
|
938
|
-
await cmdDownload();
|
|
939
|
-
break;
|
|
940
|
-
case 'delete':
|
|
941
|
-
case 'rm':
|
|
942
|
-
await cmdDelete();
|
|
943
|
-
break;
|
|
944
|
-
case 'delete-folder':
|
|
945
|
-
case 'rmdir':
|
|
946
|
-
await cmdDeleteFolder();
|
|
947
|
-
break;
|
|
948
|
-
case 'info':
|
|
949
|
-
case 'stat':
|
|
950
|
-
await cmdInfo();
|
|
951
|
-
break;
|
|
952
|
-
case 'signed-url':
|
|
953
|
-
case 'sign':
|
|
954
|
-
await cmdSignedUrl();
|
|
955
|
-
break;
|
|
956
|
-
case 'config':
|
|
957
|
-
case 'cfg':
|
|
958
|
-
await cmdConfig();
|
|
959
|
-
break;
|
|
960
|
-
case 'help':
|
|
961
|
-
case '--help':
|
|
962
|
-
case '-h':
|
|
963
|
-
showHelp();
|
|
964
|
-
break;
|
|
965
|
-
default:
|
|
966
|
-
console.error(`❌ Unknown command: ${command}`);
|
|
967
|
-
console.error(' Run "mbkbucket --help" for usage information.');
|
|
968
|
-
process.exit(1);
|
|
627
|
+
if (bucketFlag && typeof bucketFlag === 'string') process.env.MBKAUTHE_BUCKET = bucketFlag;
|
|
628
|
+
|
|
629
|
+
const handler = COMMANDS[command];
|
|
630
|
+
if (!handler) {
|
|
631
|
+
console.error(`❌ Unknown command: ${command}\n Run "mbkbucket --help" for usage information.`);
|
|
632
|
+
process.exit(1);
|
|
969
633
|
}
|
|
634
|
+
|
|
635
|
+
await handler();
|
|
970
636
|
}
|
|
971
637
|
|
|
972
638
|
main().catch((err) => {
|