mbkbucket 3.3.0 → 3.3.2
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/index.js +8 -18
- package/lib/cli.js +642 -0
- package/{public → lib/public}/bucketadmin.js +1 -1
- package/{src → lib/src}/app.js +37 -50
- package/{src → lib/src}/cli/auth-helper.js +24 -103
- package/lib/src/config/index.js +108 -0
- package/{src → lib/src}/config/validation.js +14 -30
- package/{src → lib/src}/controllers/api.controller.js +85 -176
- package/{src → lib/src}/controllers/dashboard.controller.js +12 -23
- package/{src → lib/src}/controllers/info.controller.js +17 -20
- package/{src → lib/src}/controllers/view.controller.js +23 -70
- package/{src → lib/src}/middleware/async-handler.js +1 -16
- package/{src → lib/src}/middleware/bucket-resolver.js +7 -13
- package/{src → lib/src}/middleware/error-handler.js +6 -14
- package/{src → lib/src}/middleware/rate-limiter.js +2 -10
- package/{src → lib/src}/middleware/security.js +1 -17
- package/lib/src/routes/api.routes.js +40 -0
- package/{src → lib/src}/routes/index.js +1 -9
- package/{src → lib/src}/routes/info.routes.js +2 -3
- package/{src → lib/src}/routes/view.routes.js +3 -8
- package/{src → lib/src}/services/s3.service.js +157 -349
- package/lib/src/utils/errors.js +45 -0
- package/lib/src/utils/helpers.js +251 -0
- package/package.json +18 -3
- package/.github/PACKAGE.md +0 -19
- package/.github/workflows/publish.yml +0 -88
- package/cli.js +0 -977
- package/index.d.ts +0 -444
- package/lib/index.js +0 -17
- package/src/config/index.js +0 -133
- package/src/routes/api.routes.js +0 -66
- package/src/utils/errors.js +0 -76
- package/src/utils/helpers.js +0 -197
- package/test/config-validation.test.js +0 -116
- /package/{public → lib/public}/bucketadmin.css +0 -0
- /package/{public → lib/public}/mbkbucket-helper-demo.html +0 -0
- /package/{public → lib/public}/mbkbucket-helper.css +0 -0
- /package/{public → lib/public}/mbkbucket-helper.js +0 -0
- /package/{src → lib/src}/utils/logger.js +0 -0
package/index.js
CHANGED
|
@@ -1,34 +1,24 @@
|
|
|
1
1
|
import dotenv from "dotenv";
|
|
2
|
-
import { createApp } from "./src/app.js";
|
|
3
|
-
import { checkVersion } from "./src/config/index.js";
|
|
4
|
-
import { runHealthCheck } from "./src/services/s3.service.js";
|
|
5
|
-
import { createLogger } from "
|
|
2
|
+
import { createApp } from "./lib/src/app.js";
|
|
3
|
+
import { checkVersion } from "./lib/src/config/index.js";
|
|
4
|
+
import { runHealthCheck } from "./lib/src/services/s3.service.js";
|
|
5
|
+
import { createLogger } from "#logger";
|
|
6
6
|
|
|
7
7
|
dotenv.config();
|
|
8
8
|
|
|
9
9
|
const debugServer = createLogger('server');
|
|
10
10
|
const port = process.env.PORT || 3004;
|
|
11
|
-
const isDevMode = process.env.NODE_ENV === "dev";
|
|
12
|
-
|
|
13
|
-
// Create and start the Express application
|
|
14
11
|
const server = createApp();
|
|
15
12
|
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
// Check for package updates and S3 connectivity on startup
|
|
13
|
+
if (process.env.NODE_ENV === "dev") {
|
|
19
14
|
await checkVersion();
|
|
20
15
|
await runHealthCheck();
|
|
21
|
-
|
|
22
16
|
server.listen(port, () => {
|
|
23
17
|
debugServer("Server running on http://localhost:%s", port);
|
|
24
18
|
});
|
|
25
|
-
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
export * from "./src/
|
|
30
|
-
export
|
|
31
|
-
export { default as bucket } from "./src/routes/index.js";
|
|
32
|
-
|
|
33
|
-
// Default export: the Express application instance
|
|
21
|
+
export * from "./lib/src/services/s3.service.js";
|
|
22
|
+
export * from "./lib/src/config/index.js";
|
|
23
|
+
export { default as bucket } from "./lib/src/routes/index.js";
|
|
34
24
|
export default server;
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* mbkbucket CLI — Manage S3/R2 buckets from the command line.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import dotenv from 'dotenv';
|
|
8
|
+
import path from 'node:path';
|
|
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';
|
|
12
|
+
|
|
13
|
+
dotenv.config();
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Parse CLI arguments
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const VALUE_FLAGS = new Set(['server', 'app', 'bucket', 'profile-key', 'expires']);
|
|
20
|
+
|
|
21
|
+
function findFlag(name) {
|
|
22
|
+
const long = `--${name}`;
|
|
23
|
+
const short = `-${name[0]}`;
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
if (args[i] === long || args[i] === short) {
|
|
26
|
+
return args[i + 1] !== undefined && !args[i + 1].startsWith('-') ? args[i + 1] : true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hasFlag = name => args.includes(`--${name}`) || args.includes(`-${name[0]}`);
|
|
33
|
+
|
|
34
|
+
function getPositionalArgs() {
|
|
35
|
+
const positional = [];
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
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++;
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
positional.push(args[i]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return positional;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const command = getPositionalArgs()[0] || 'help';
|
|
50
|
+
const cmdArgs = getPositionalArgs().slice(1);
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Help text & S3 helper
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
function showHelp() {
|
|
56
|
+
console.log(`
|
|
57
|
+
mbkbucket — S3/R2 bucket CLI
|
|
58
|
+
|
|
59
|
+
USAGE:
|
|
60
|
+
mbkbucket <command> [options] [arguments]
|
|
61
|
+
|
|
62
|
+
COMMANDS:
|
|
63
|
+
login Authenticate with mbkauthe device flow
|
|
64
|
+
logout Clear stored credentials
|
|
65
|
+
whoami Show current login status
|
|
66
|
+
|
|
67
|
+
config Show all config values
|
|
68
|
+
config get <key> Get a specific config value
|
|
69
|
+
config set <k> <v> Set a config value
|
|
70
|
+
config unset <key> Remove a config value
|
|
71
|
+
config path Show config file path
|
|
72
|
+
config reset Reset to demo values
|
|
73
|
+
config edit Open config file in editor
|
|
74
|
+
|
|
75
|
+
list [prefix] List files and folders
|
|
76
|
+
upload <file> [key] Upload a file or folder
|
|
77
|
+
download <key> [dest] Download a file or folder
|
|
78
|
+
delete <key> Delete a file
|
|
79
|
+
delete-folder <prefix> Recursively delete a folder
|
|
80
|
+
info <key> Show file metadata
|
|
81
|
+
signed-url <key> Generate a pre-signed download URL
|
|
82
|
+
|
|
83
|
+
GLOBAL OPTIONS:
|
|
84
|
+
--app, -a <name> Override APP_NAME for key prefixing
|
|
85
|
+
--bucket, -b <name> Override default bucket name
|
|
86
|
+
--help, -h Show this help
|
|
87
|
+
|
|
88
|
+
LOGIN OPTIONS:
|
|
89
|
+
--server <url> mbkauthe server URL (required for login)
|
|
90
|
+
--profile-key <k> API token profile key
|
|
91
|
+
|
|
92
|
+
SIGNED-URL OPTIONS:
|
|
93
|
+
--expires <sec> Expiration in seconds (default: 3600)
|
|
94
|
+
|
|
95
|
+
ENVIRONMENT:
|
|
96
|
+
BucketConnection JSON mapping of bucket name → S3 credentials
|
|
97
|
+
APP_NAME Application name for key prefix isolation
|
|
98
|
+
PORT Server port (default: 3004)
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let _s3Module = null;
|
|
103
|
+
async function getS3() {
|
|
104
|
+
if (_s3Module) return _s3Module;
|
|
105
|
+
const appFlag = findFlag('app');
|
|
106
|
+
if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
|
|
107
|
+
_s3Module = await import('./src/services/s3.service.js');
|
|
108
|
+
return _s3Module;
|
|
109
|
+
}
|
|
110
|
+
|
|
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
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
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
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Command Implementations
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
async function cmdLogin() {
|
|
135
|
+
const storedCfg = readConfig();
|
|
136
|
+
let serverUrl = findFlag('server') || storedCfg.serverUrl;
|
|
137
|
+
|
|
138
|
+
if (!serverUrl || typeof serverUrl !== '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 serverUrl https://your-server.com\n Then just run: mbkbucket login');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const profileKey = findFlag('profile-key') || storedCfg.profileKey;
|
|
144
|
+
updateConfig({ serverUrl });
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const result = await deviceFlowLogin({
|
|
148
|
+
serverUrl,
|
|
149
|
+
profileKey: typeof profileKey === 'string' ? profileKey : undefined,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
updateConfig({
|
|
153
|
+
token: result.token,
|
|
154
|
+
tokenPrefix: result.tokenPrefix,
|
|
155
|
+
username: result.username,
|
|
156
|
+
serverUrl,
|
|
157
|
+
profile: result.profile || null,
|
|
158
|
+
loggedInAt: nowIso(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log('\n✅ Credentials saved. You are now logged in.\n Run "mbkbucket whoami" to verify.');
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const msg = err.message || '';
|
|
164
|
+
console.error(`❌ Login failed: ${msg}`);
|
|
165
|
+
if (msg.includes('profileKey') || msg.includes('profileId')) {
|
|
166
|
+
console.error('\n💡 An API token profile is required. Create one in mbkauthe admin panel and pass --profile-key.');
|
|
167
|
+
} else if (msg.includes('fetch') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
168
|
+
console.error(`\n💡 Could not reach server "${serverUrl}". Verify it is running and CLI_AUTH_ENABLED=true.`);
|
|
169
|
+
}
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function cmdLogout() {
|
|
175
|
+
if (!isLoggedIn()) return console.log('ℹ️ Not logged in.');
|
|
176
|
+
const { username = 'user' } = readConfig();
|
|
177
|
+
clearConfig();
|
|
178
|
+
console.log(`👋 Logged out. Goodbye, ${username}!`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cmdWhoami() {
|
|
182
|
+
if (!isLoggedIn()) return console.log('ℹ️ Not logged in. Run "mbkbucket login --server <url>" to authenticate.');
|
|
183
|
+
const cfg = readConfig();
|
|
184
|
+
console.log(`✅ Logged in as: ${cfg.username}`);
|
|
185
|
+
console.log(` Server: ${cfg.serverUrl}`);
|
|
186
|
+
console.log(` Token: ${cfg.tokenPrefix}...`);
|
|
187
|
+
if (cfg.profile) console.log(` Profile: ${cfg.profile.name} (scope: ${cfg.profile.scope || 'full'})`);
|
|
188
|
+
console.log(` Since: ${formatDate(cfg.loggedInAt)}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function cmdList() {
|
|
192
|
+
const prefix = cmdArgs[0] || '';
|
|
193
|
+
const bucketName = findFlag('bucket');
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const s3 = await getS3();
|
|
197
|
+
const result = await s3.listfiles(prefix, { bucketName, delimiter: '/' });
|
|
198
|
+
|
|
199
|
+
if (!result.Contents?.length) {
|
|
200
|
+
return console.log(`📭 No files found${prefix ? ` with prefix "${prefix}"` : ''}.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(`\n📁 Files${prefix ? ` matching "${prefix}"` : ''} (${result.KeyCount || result.Contents.length} items):\n` + '─'.repeat(80));
|
|
204
|
+
|
|
205
|
+
if (result.CommonPrefixes?.length) {
|
|
206
|
+
for (const cp of result.CommonPrefixes) {
|
|
207
|
+
const folderName = cp.Prefix.replace(/\/$/, '').split('/').pop() || cp.Prefix;
|
|
208
|
+
console.log(` 📂 ${folderName}/`);
|
|
209
|
+
}
|
|
210
|
+
if (result.Contents.length > 0) console.log('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const obj of result.Contents) {
|
|
214
|
+
const name = obj.Key || '';
|
|
215
|
+
const displayName = name.length > 60 ? `...${name.slice(-57)}` : name;
|
|
216
|
+
console.log(` ${displayName.padEnd(62)} ${formatBytes(obj.Size).padStart(10)} ${formatDate(obj.LastModified)}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log('─'.repeat(80));
|
|
220
|
+
if (result.hasMore) console.log(' ... more files available (use --prefix to narrow down)');
|
|
221
|
+
console.log('');
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error(`❌ List failed: ${err.message}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function cmdUpload() {
|
|
229
|
+
const filePath = cmdArgs[0];
|
|
230
|
+
if (!filePath) {
|
|
231
|
+
console.error('❌ Usage: mbkbucket upload <file> [key]\n Example: mbkbucket upload ./photo.jpg images/photo.jpg');
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const resolved = path.resolve(filePath);
|
|
236
|
+
if (!fs.existsSync(resolved)) {
|
|
237
|
+
console.error(`❌ File not found: ${filePath}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
242
|
+
await uploadFolder(resolved);
|
|
243
|
+
} else {
|
|
244
|
+
await uploadSingleFile(resolved);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function uploadSingleFile(filePath) {
|
|
249
|
+
const bucketName = findFlag('bucket');
|
|
250
|
+
const stat = fs.statSync(filePath);
|
|
251
|
+
const fileName = path.basename(filePath);
|
|
252
|
+
const key = cmdArgs[1] || fileName;
|
|
253
|
+
const contentType = getMimeType(fileName);
|
|
254
|
+
|
|
255
|
+
console.log(`⬆️ Uploading: ${fileName} (${formatBytes(stat.size)}) → ${key}`);
|
|
256
|
+
try {
|
|
257
|
+
const s3 = await getS3();
|
|
258
|
+
const result = await s3.uploadFile(key, fs.readFileSync(filePath), contentType, { bucketName });
|
|
259
|
+
console.log(`✅ Uploaded: ${result.key} (${formatBytes(result.fileSize)})`);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(`❌ Upload failed: ${err.message}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function uploadFolder(folderPath) {
|
|
267
|
+
const bucketName = findFlag('bucket');
|
|
268
|
+
const prefix = cmdArgs[1] || path.basename(folderPath);
|
|
269
|
+
const files = walkDir(folderPath);
|
|
270
|
+
|
|
271
|
+
if (!files.length) return console.log('📭 No files found in folder.');
|
|
272
|
+
|
|
273
|
+
console.log(`📁 Uploading folder: ${folderPath} (${files.length} files) → ${prefix}/\n`);
|
|
274
|
+
let uploaded = 0;
|
|
275
|
+
let failed = 0;
|
|
276
|
+
|
|
277
|
+
for (const file of files) {
|
|
278
|
+
const rel = path.relative(folderPath, file).replace(/\\/g, '/');
|
|
279
|
+
const key = `${prefix}/${rel}`;
|
|
280
|
+
try {
|
|
281
|
+
const s3 = await getS3();
|
|
282
|
+
await s3.uploadFile(key, fs.readFileSync(file), getMimeType(file), { bucketName });
|
|
283
|
+
console.log(` ✅ ${rel}`);
|
|
284
|
+
uploaded++;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error(` ❌ ${rel}: ${err.message}`);
|
|
287
|
+
failed++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log(`\nDone: ${uploaded} uploaded, ${failed} failed.`);
|
|
292
|
+
if (failed > 0) process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function cmdDownload() {
|
|
296
|
+
const key = cmdArgs[0];
|
|
297
|
+
if (!key) {
|
|
298
|
+
console.error('❌ Usage: mbkbucket download <key> [destination]');
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const dest = cmdArgs[1] || path.basename(key);
|
|
303
|
+
const bucketName = findFlag('bucket');
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const s3 = await getS3();
|
|
307
|
+
if (key.endsWith('/')) return downloadFolder(s3, key, dest, bucketName);
|
|
308
|
+
|
|
309
|
+
console.log(`⬇️ Downloading: ${key} → ${dest}`);
|
|
310
|
+
const result = await s3.downloadFile(key, { bucketName });
|
|
311
|
+
if (result.notModified) return console.log('ℹ️ File not modified.');
|
|
312
|
+
|
|
313
|
+
const body = await streamToBuffer(result.Body);
|
|
314
|
+
const destDir = path.dirname(path.resolve(dest));
|
|
315
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
316
|
+
fs.writeFileSync(path.resolve(dest), body);
|
|
317
|
+
|
|
318
|
+
console.log(`✅ Downloaded: ${dest} (${formatBytes(body.length)})`);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.error(`❌ Download failed: ${err.message}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function downloadFolder(s3, prefix, destDir, bucketName) {
|
|
326
|
+
const allFiles = [];
|
|
327
|
+
let token = null;
|
|
328
|
+
const resolvedDest = path.resolve(destDir);
|
|
329
|
+
|
|
330
|
+
do {
|
|
331
|
+
const result = await s3.listfiles(prefix, { bucketName, continuationToken: token });
|
|
332
|
+
(result.Contents || []).forEach(obj => {
|
|
333
|
+
if (obj.Key && obj.Key !== prefix) allFiles.push(obj.Key);
|
|
334
|
+
});
|
|
335
|
+
token = result.IsTruncated ? result.NextContinuationToken : null;
|
|
336
|
+
} while (token);
|
|
337
|
+
|
|
338
|
+
if (!allFiles.length) return console.log(`📭 No files found with prefix "${prefix}".`);
|
|
339
|
+
|
|
340
|
+
console.log(`📁 Downloading ${allFiles.length} files to ${resolvedDest}/\n`);
|
|
341
|
+
let downloaded = 0;
|
|
342
|
+
let failed = 0;
|
|
343
|
+
|
|
344
|
+
for (const fileKey of allFiles) {
|
|
345
|
+
const relativePath = fileKey.startsWith(prefix) ? fileKey.slice(prefix.length) : fileKey;
|
|
346
|
+
const filePath = path.join(resolvedDest, relativePath);
|
|
347
|
+
try {
|
|
348
|
+
const result = await s3.downloadFile(fileKey, { bucketName });
|
|
349
|
+
if (!result.notModified) {
|
|
350
|
+
const body = await streamToBuffer(result.Body);
|
|
351
|
+
const fileDir = path.dirname(filePath);
|
|
352
|
+
if (!fs.existsSync(fileDir)) fs.mkdirSync(fileDir, { recursive: true });
|
|
353
|
+
fs.writeFileSync(filePath, body);
|
|
354
|
+
}
|
|
355
|
+
console.log(` ✅ ${relativePath}`);
|
|
356
|
+
downloaded++;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.error(` ❌ ${relativePath}: ${err.message}`);
|
|
359
|
+
failed++;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.log(`\nDone: ${downloaded} downloaded, ${failed} failed.`);
|
|
364
|
+
if (failed > 0) process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function cmdDelete() {
|
|
368
|
+
const key = cmdArgs[0];
|
|
369
|
+
if (!key) {
|
|
370
|
+
console.error('❌ Usage: mbkbucket delete <key>');
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const s3 = await getS3();
|
|
376
|
+
console.log(`🗑️ Deleting: ${key}`);
|
|
377
|
+
const result = await s3.deleteFile(key, findFlag('bucket'));
|
|
378
|
+
console.log(`✅ Deleted: ${result.key}`);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
console.error(`❌ Delete failed: ${err.message}`);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function cmdDeleteFolder() {
|
|
386
|
+
const prefix = cmdArgs[0];
|
|
387
|
+
if (!prefix) {
|
|
388
|
+
console.error('❌ Usage: mbkbucket delete-folder <prefix>');
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const cleaned = trimSlashes(prefix);
|
|
393
|
+
console.log(`⚠️ This will delete ALL files under "${cleaned}/".\n Are you sure? Type "yes" to confirm:`);
|
|
394
|
+
|
|
395
|
+
const confirmed = await promptLine();
|
|
396
|
+
if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const s3 = await getS3();
|
|
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}/".`);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.error(`❌ Delete folder failed: ${err.message}`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function cmdInfo() {
|
|
410
|
+
const key = cmdArgs[0];
|
|
411
|
+
if (!key) {
|
|
412
|
+
console.error('❌ Usage: mbkbucket info <key>');
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
const s3 = await getS3();
|
|
418
|
+
const meta = await s3.getFileMetadata(key, findFlag('bucket'));
|
|
419
|
+
if (!meta.exists) {
|
|
420
|
+
console.log(`❌ File not found: ${key}`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log(`\n📄 ${meta.key}\n` + '─'.repeat(60));
|
|
425
|
+
console.log(` Size: ${formatBytes(meta.ContentLength)}`);
|
|
426
|
+
console.log(` Type: ${meta.ContentType || 'unknown'}`);
|
|
427
|
+
console.log(` Last Modified: ${formatDate(meta.LastModified)}`);
|
|
428
|
+
console.log(` ETag: ${meta.ETag || '—'}`);
|
|
429
|
+
console.log(` Cache-Control: ${meta.CacheControl || '—'}`);
|
|
430
|
+
if (meta.Metadata && Object.keys(meta.Metadata).length > 0) {
|
|
431
|
+
console.log(' Metadata:');
|
|
432
|
+
for (const [k, v] of Object.entries(meta.Metadata)) console.log(` ${k}: ${v}`);
|
|
433
|
+
}
|
|
434
|
+
console.log('');
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error(`❌ Info failed: ${err.message}`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
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
|
+
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Config Commands
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
const DEMO_CONFIG = {
|
|
464
|
+
token: 'mbk_demo_token_replace_with_real_one_from_login',
|
|
465
|
+
tokenPrefix: 'mbk_demo',
|
|
466
|
+
username: 'demo-user',
|
|
467
|
+
serverUrl: 'https://your-app.example.com',
|
|
468
|
+
profile: { name: 'Default CLI Profile', scope: 'read-write', allowedApps: ['mbkbucket'] },
|
|
469
|
+
loggedInAt: nowIso(),
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const VALID_CONFIG_KEYS = ['token', 'tokenPrefix', 'username', 'serverUrl', 'profile', 'profileKey', 'loggedInAt'];
|
|
473
|
+
|
|
474
|
+
async function cmdConfig() {
|
|
475
|
+
const [sub, key, value] = cmdArgs;
|
|
476
|
+
switch (sub) {
|
|
477
|
+
case 'get': return cmdConfigGet(key);
|
|
478
|
+
case 'set': return cmdConfigSet(key, value);
|
|
479
|
+
case 'unset':
|
|
480
|
+
case 'del':
|
|
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();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function cmdConfigShow() {
|
|
490
|
+
const cfg = readConfig();
|
|
491
|
+
const entries = Object.keys(cfg).filter(k => !k.startsWith('_'));
|
|
492
|
+
|
|
493
|
+
if (!entries.length) {
|
|
494
|
+
console.log(`📭 No config values set.\n File: ${CONFIG_FILE}\n Run "mbkbucket config reset" to populate demo values.`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
console.log(`\n📋 Config (${CONFIG_FILE}):\n` + '─'.repeat(60));
|
|
499
|
+
for (const k of entries.sort()) {
|
|
500
|
+
const v = cfg[k];
|
|
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}`);
|
|
506
|
+
}
|
|
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');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function cmdConfigGet(key) {
|
|
511
|
+
if (!key || !VALID_CONFIG_KEYS.includes(key)) {
|
|
512
|
+
console.error(`❌ Usage: mbkbucket config get <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
const cfg = readConfig();
|
|
516
|
+
if (!(key in cfg)) {
|
|
517
|
+
console.error(`❌ Key not found: ${key}\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
const v = cfg[key];
|
|
521
|
+
console.log(typeof v === 'object' && v !== null ? JSON.stringify(v, null, 2) : String(v));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function cmdConfigSet(key, value) {
|
|
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(', ')}`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let parsed = value;
|
|
531
|
+
if (/^[{\[].*[}\]]$/.test(value)) {
|
|
532
|
+
try { parsed = JSON.parse(value); } catch {}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
updateConfig({ [key]: parsed });
|
|
536
|
+
console.log(`✅ Set ${key} = ${typeof parsed === 'object' ? JSON.stringify(parsed) : parsed}`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function cmdConfigUnset(key) {
|
|
540
|
+
if (!key) {
|
|
541
|
+
console.error(`❌ Usage: mbkbucket config unset <key>\n Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
const cfg = readConfig();
|
|
545
|
+
if (!(key in cfg)) {
|
|
546
|
+
console.error(`❌ Key not found: ${key}`);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
delete cfg[key];
|
|
550
|
+
writeConfig(cfg);
|
|
551
|
+
console.log(`🗑️ Removed: ${key}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function cmdConfigPath() {
|
|
555
|
+
console.log(`📁 Config directory: ${CONFIG_DIR}\n📄 Config file: ${CONFIG_FILE}`);
|
|
556
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
557
|
+
const stat = fs.statSync(CONFIG_FILE);
|
|
558
|
+
console.log(` Size: ${formatBytes(stat.size)}\n Modified: ${formatDate(stat.mtime.toISOString())}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function cmdConfigReset() {
|
|
563
|
+
console.log('⚠️ This will overwrite your current config with demo values.\n Are you sure? Type "yes" to confirm:');
|
|
564
|
+
const confirmed = await promptLine();
|
|
565
|
+
if (confirmed.trim().toLowerCase() !== 'yes') return console.log('Cancelled.');
|
|
566
|
+
writeConfig({ ...DEMO_CONFIG });
|
|
567
|
+
console.log('✅ Config reset to demo values.\n Run "mbkbucket config" to view.');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function cmdConfigEdit() {
|
|
571
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'notepad';
|
|
572
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
573
|
+
if (!fs.existsSync(CONFIG_FILE)) writeConfig({ ...DEMO_CONFIG });
|
|
574
|
+
|
|
575
|
+
console.log(`✏️ Opening ${CONFIG_FILE} with ${editor}...`);
|
|
576
|
+
const { spawn } = await import('node:child_process');
|
|
577
|
+
const child = spawn(editor, [CONFIG_FILE], { stdio: 'inherit', shell: true });
|
|
578
|
+
|
|
579
|
+
return new Promise((resolve) => {
|
|
580
|
+
child.on('exit', (code) => {
|
|
581
|
+
if (code === 0) console.log('✅ Editor closed.');
|
|
582
|
+
else console.error(`⚠️ Editor exited with code ${code}.`);
|
|
583
|
+
resolve();
|
|
584
|
+
});
|
|
585
|
+
child.on('error', (err) => {
|
|
586
|
+
console.error(`❌ Could not open editor (${editor}): ${err.message}`);
|
|
587
|
+
resolve();
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
// Main Router
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
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
|
+
};
|
|
619
|
+
|
|
620
|
+
async function main() {
|
|
621
|
+
if (hasFlag('help')) return showHelp();
|
|
622
|
+
|
|
623
|
+
const appFlag = findFlag('app');
|
|
624
|
+
if (appFlag && typeof appFlag === 'string') process.env.APP_NAME = appFlag;
|
|
625
|
+
|
|
626
|
+
const bucketFlag = findFlag('bucket');
|
|
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);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
await handler();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
main().catch((err) => {
|
|
639
|
+
console.error(`❌ Unexpected error: ${err.message}`);
|
|
640
|
+
if (process.env.DEBUG) console.error(err);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
});
|