@nightowne/tas-cli 2.1.0 → 2.3.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 +307 -92
- package/package.json +4 -2
- package/src/cli.js +179 -23
- package/src/crypto/encryption.js +25 -3
- package/src/db/index.js +19 -13
- package/src/fuse/mount.js +158 -124
- package/src/index.js +44 -17
- package/src/share/server.js +55 -16
- package/src/sync/sync.js +159 -23
- package/src/telegram/client.js +64 -16
- package/src/utils/branding.js +10 -3
- package/src/utils/chunker.js +4 -2
- package/src/utils/cli-helpers.js +44 -6
- package/src/utils/progress.js +3 -3
package/src/sync/sync.js
CHANGED
|
@@ -13,6 +13,10 @@ import { processFile } from '../index.js';
|
|
|
13
13
|
// Debounce time in ms to batch rapid file changes
|
|
14
14
|
const DEBOUNCE_MS = 1000;
|
|
15
15
|
|
|
16
|
+
// Exponential backoff limits for upload errors
|
|
17
|
+
const INITIAL_BACKOFF_MS = 2000;
|
|
18
|
+
const MAX_BACKOFF_MS = 60000;
|
|
19
|
+
|
|
16
20
|
// Ignore patterns
|
|
17
21
|
const IGNORE_PATTERNS = [
|
|
18
22
|
/^\./, // Hidden files
|
|
@@ -31,6 +35,7 @@ export class SyncEngine extends EventEmitter {
|
|
|
31
35
|
this.config = options.config;
|
|
32
36
|
this.limitRate = options.limitRate || null;
|
|
33
37
|
this.watchers = new Map(); // path -> FSWatcher
|
|
38
|
+
this.rootWatchers = new Map(); // rootPath -> Set of watched subpaths (for Linux recursive watch fallback)
|
|
34
39
|
this.pendingChanges = new Map(); // path -> timeout
|
|
35
40
|
this.db = null;
|
|
36
41
|
this.running = false;
|
|
@@ -105,6 +110,8 @@ export class SyncEngine extends EventEmitter {
|
|
|
105
110
|
const promises = [];
|
|
106
111
|
|
|
107
112
|
const worker = async () => {
|
|
113
|
+
let consecutiveErrors = 0;
|
|
114
|
+
|
|
108
115
|
while (queue.length > 0) {
|
|
109
116
|
const file = queue.shift();
|
|
110
117
|
const existing = stateMap.get(file.relativePath);
|
|
@@ -141,6 +148,7 @@ export class SyncEngine extends EventEmitter {
|
|
|
141
148
|
// Update sync state
|
|
142
149
|
this.db.updateSyncState(folder.id, file.relativePath, hash, file.mtime);
|
|
143
150
|
uploaded++;
|
|
151
|
+
consecutiveErrors = 0; // Reset on success
|
|
144
152
|
|
|
145
153
|
this.emit('file-upload-complete', { file: file.relativePath });
|
|
146
154
|
} catch (err) {
|
|
@@ -148,9 +156,12 @@ export class SyncEngine extends EventEmitter {
|
|
|
148
156
|
if (err.message.includes('duplicate')) {
|
|
149
157
|
this.db.updateSyncState(folder.id, file.relativePath, hash, file.mtime);
|
|
150
158
|
skipped++;
|
|
159
|
+
consecutiveErrors = 0;
|
|
151
160
|
} else {
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
consecutiveErrors++;
|
|
162
|
+
// Exponential backoff: 2s → 4s → 8s → 16s → ... → 60s max
|
|
163
|
+
const delay = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, consecutiveErrors - 1), MAX_BACKOFF_MS);
|
|
164
|
+
await new Promise(r => setTimeout(r, delay));
|
|
154
165
|
this.emit('file-upload-error', { file: file.relativePath, error: err.message });
|
|
155
166
|
}
|
|
156
167
|
}
|
|
@@ -238,37 +249,148 @@ export class SyncEngine extends EventEmitter {
|
|
|
238
249
|
}
|
|
239
250
|
}
|
|
240
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Recursively list all subdirectories of a directory
|
|
254
|
+
*/
|
|
255
|
+
getSubdirectories(dirPath) {
|
|
256
|
+
const subdirs = [];
|
|
257
|
+
try {
|
|
258
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
if (entry.isDirectory()) {
|
|
261
|
+
if (this.shouldIgnore(entry.name)) continue;
|
|
262
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
263
|
+
subdirs.push(fullPath);
|
|
264
|
+
subdirs.push(...this.getSubdirectories(fullPath));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
// Ignore folder reading errors
|
|
269
|
+
}
|
|
270
|
+
return subdirs;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Watch a single directory (non-recursively) and dynamically watch new folders
|
|
275
|
+
*/
|
|
276
|
+
watchSingleDir(rootPath, dir) {
|
|
277
|
+
if (this.watchers.has(dir)) return;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const watcher = fs.watch(dir, { recursive: false }, (event, filename) => {
|
|
281
|
+
if (!filename) return;
|
|
282
|
+
|
|
283
|
+
const fullPath = path.join(dir, filename);
|
|
284
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
285
|
+
|
|
286
|
+
// If a new directory is created, watch it recursively
|
|
287
|
+
try {
|
|
288
|
+
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
|
|
289
|
+
if (!this.shouldIgnore(filename)) {
|
|
290
|
+
this.watchDirRecursively(rootPath, fullPath);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch (e) {
|
|
294
|
+
// Ignore stats errors
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.handleFileChange(rootPath, relativePath);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
watcher.on('error', (err) => {
|
|
301
|
+
this.emit('watch-error', { folder: rootPath, error: err.message });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
this.watchers.set(dir, watcher);
|
|
305
|
+
|
|
306
|
+
if (!this.rootWatchers.has(rootPath)) {
|
|
307
|
+
this.rootWatchers.set(rootPath, new Set());
|
|
308
|
+
}
|
|
309
|
+
this.rootWatchers.get(rootPath).add(dir);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
this.emit('watch-error', { folder: rootPath, error: err.message });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Recursively watch a newly created directory and its subdirectories
|
|
317
|
+
*/
|
|
318
|
+
watchDirRecursively(rootPath, dirPath) {
|
|
319
|
+
this.watchSingleDir(rootPath, dirPath);
|
|
320
|
+
const subdirs = this.getSubdirectories(dirPath);
|
|
321
|
+
for (const subdir of subdirs) {
|
|
322
|
+
this.watchSingleDir(rootPath, subdir);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
241
326
|
/**
|
|
242
327
|
* Start watching a folder
|
|
243
328
|
*/
|
|
244
329
|
watchFolder(folderPath) {
|
|
245
|
-
|
|
246
|
-
return; // Already watching
|
|
247
|
-
}
|
|
330
|
+
const isRecursiveSupported = process.platform === 'darwin' || process.platform === 'win32';
|
|
248
331
|
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
|
|
332
|
+
if (isRecursiveSupported) {
|
|
333
|
+
if (this.watchers.has(folderPath)) {
|
|
334
|
+
return; // Already watching
|
|
252
335
|
}
|
|
253
|
-
});
|
|
254
336
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
337
|
+
const watcher = fs.watch(folderPath, { recursive: true }, (event, filename) => {
|
|
338
|
+
if (filename) {
|
|
339
|
+
this.handleFileChange(folderPath, filename);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
watcher.on('error', (err) => {
|
|
344
|
+
this.emit('watch-error', { folder: folderPath, error: err.message });
|
|
345
|
+
});
|
|
258
346
|
|
|
259
|
-
|
|
260
|
-
|
|
347
|
+
this.watchers.set(folderPath, watcher);
|
|
348
|
+
this.emit('watch-start', { folder: folderPath });
|
|
349
|
+
} else {
|
|
350
|
+
// Manual recursive watch for Linux/other platforms
|
|
351
|
+
if (this.rootWatchers.has(folderPath)) {
|
|
352
|
+
return; // Already watching
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Watch root
|
|
356
|
+
this.watchSingleDir(folderPath, folderPath);
|
|
357
|
+
|
|
358
|
+
// Watch all existing subdirectories
|
|
359
|
+
const subdirs = this.getSubdirectories(folderPath);
|
|
360
|
+
for (const subdir of subdirs) {
|
|
361
|
+
this.watchSingleDir(folderPath, subdir);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.emit('watch-start', { folder: folderPath });
|
|
365
|
+
}
|
|
261
366
|
}
|
|
262
367
|
|
|
263
368
|
/**
|
|
264
369
|
* Stop watching a folder
|
|
265
370
|
*/
|
|
266
371
|
unwatchFolder(folderPath) {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
this.watchers.
|
|
271
|
-
|
|
372
|
+
const isRecursiveSupported = process.platform === 'darwin' || process.platform === 'win32';
|
|
373
|
+
|
|
374
|
+
if (isRecursiveSupported) {
|
|
375
|
+
const watcher = this.watchers.get(folderPath);
|
|
376
|
+
if (watcher) {
|
|
377
|
+
watcher.close();
|
|
378
|
+
this.watchers.delete(folderPath);
|
|
379
|
+
this.emit('watch-stop', { folder: folderPath });
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
if (this.rootWatchers.has(folderPath)) {
|
|
383
|
+
const dirs = this.rootWatchers.get(folderPath);
|
|
384
|
+
for (const dir of dirs) {
|
|
385
|
+
const watcher = this.watchers.get(dir);
|
|
386
|
+
if (watcher) {
|
|
387
|
+
watcher.close();
|
|
388
|
+
this.watchers.delete(dir);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
this.rootWatchers.delete(folderPath);
|
|
392
|
+
this.emit('watch-stop', { folder: folderPath });
|
|
393
|
+
}
|
|
272
394
|
}
|
|
273
395
|
}
|
|
274
396
|
|
|
@@ -302,11 +424,25 @@ export class SyncEngine extends EventEmitter {
|
|
|
302
424
|
this.pendingChanges.clear();
|
|
303
425
|
|
|
304
426
|
// Close all watchers
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
427
|
+
const isRecursiveSupported = process.platform === 'darwin' || process.platform === 'win32';
|
|
428
|
+
if (isRecursiveSupported) {
|
|
429
|
+
for (const [folderPath, watcher] of this.watchers) {
|
|
430
|
+
watcher.close();
|
|
431
|
+
this.emit('watch-stop', { folder: folderPath });
|
|
432
|
+
}
|
|
433
|
+
this.watchers.clear();
|
|
434
|
+
} else {
|
|
435
|
+
for (const rootPath of this.rootWatchers.keys()) {
|
|
436
|
+
this.unwatchFolder(rootPath);
|
|
437
|
+
}
|
|
438
|
+
this.rootWatchers.clear();
|
|
439
|
+
|
|
440
|
+
// Just in case, close any stray watchers
|
|
441
|
+
for (const watcher of this.watchers.values()) {
|
|
442
|
+
try { watcher.close(); } catch (e) {}
|
|
443
|
+
}
|
|
444
|
+
this.watchers.clear();
|
|
308
445
|
}
|
|
309
|
-
this.watchers.clear();
|
|
310
446
|
|
|
311
447
|
if (this.db) {
|
|
312
448
|
this.db.close();
|
package/src/telegram/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram Bot client wrapper
|
|
3
3
|
* Uses official Telegram Bot API - 2GB file limit, FREE, no ban risk!
|
|
4
|
+
* Includes exponential backoff retry and rate limiting for production reliability.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import TelegramBot from 'node-telegram-bot-api';
|
|
@@ -8,11 +9,46 @@ import fs from 'fs';
|
|
|
8
9
|
import path from 'path';
|
|
9
10
|
import { pipeline } from 'stream/promises';
|
|
10
11
|
|
|
12
|
+
const MAX_RETRIES = 5;
|
|
13
|
+
const BASE_DELAY_MS = 1000;
|
|
14
|
+
const MAX_DELAY_MS = 60000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Retry an async function with exponential backoff + jitter.
|
|
18
|
+
* Handles Telegram 429 (rate limit) errors by respecting retry_after.
|
|
19
|
+
*/
|
|
20
|
+
async function withRetry(fn, label = 'operation', retries = MAX_RETRIES) {
|
|
21
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
return await fn();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const isRateLimit = err?.response?.statusCode === 429 || err?.message?.includes('429');
|
|
26
|
+
const isTransient = err?.code === 'ETIMEOUT' || err?.code === 'ECONNRESET' ||
|
|
27
|
+
err?.code === 'ENOTFOUND' || err?.message?.includes('ETIMEDOUT');
|
|
28
|
+
|
|
29
|
+
if (attempt >= retries) throw err;
|
|
30
|
+
if (!isRateLimit && !isTransient) throw err;
|
|
31
|
+
|
|
32
|
+
let delay;
|
|
33
|
+
if (isRateLimit && err?.response?.body?.parameters?.retry_after) {
|
|
34
|
+
delay = err.response.body.parameters.retry_after * 1000 + 500;
|
|
35
|
+
} else {
|
|
36
|
+
delay = Math.min(BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 1000, MAX_DELAY_MS);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sec = (delay / 1000).toFixed(1);
|
|
40
|
+
console.log(`⏳ ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${sec}s...`);
|
|
41
|
+
await new Promise(r => setTimeout(r, delay));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
11
46
|
export class TelegramClient {
|
|
12
47
|
constructor(dataDir) {
|
|
13
48
|
this.dataDir = dataDir;
|
|
14
49
|
this.bot = null;
|
|
15
50
|
this.chatId = null;
|
|
51
|
+
this._lastSendTime = 0;
|
|
16
52
|
}
|
|
17
53
|
|
|
18
54
|
/**
|
|
@@ -76,9 +112,22 @@ export class TelegramClient {
|
|
|
76
112
|
});
|
|
77
113
|
}
|
|
78
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Rate-limit: ensure at least 1s between sends to same chat (Telegram limit)
|
|
117
|
+
*/
|
|
118
|
+
async _rateLimit() {
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const elapsed = now - this._lastSendTime;
|
|
121
|
+
if (elapsed < 1000) {
|
|
122
|
+
await new Promise(r => setTimeout(r, 1000 - elapsed));
|
|
123
|
+
}
|
|
124
|
+
this._lastSendTime = Date.now();
|
|
125
|
+
}
|
|
126
|
+
|
|
79
127
|
/**
|
|
80
128
|
* Send a file to the storage chat
|
|
81
129
|
* Telegram supports up to 2GB for documents!
|
|
130
|
+
* Includes automatic retry with exponential backoff.
|
|
82
131
|
*/
|
|
83
132
|
async sendFile(filePath, caption = '', options = {}) {
|
|
84
133
|
if (!this.chatId) {
|
|
@@ -89,9 +138,12 @@ export class TelegramClient {
|
|
|
89
138
|
throw new Error(`File not found: ${filePath}`);
|
|
90
139
|
}
|
|
91
140
|
|
|
92
|
-
|
|
141
|
+
const filename = path.basename(filePath);
|
|
142
|
+
|
|
143
|
+
return withRetry(async () => {
|
|
144
|
+
await this._rateLimit();
|
|
145
|
+
|
|
93
146
|
let fileStream = fs.createReadStream(filePath);
|
|
94
|
-
const filename = path.basename(filePath);
|
|
95
147
|
|
|
96
148
|
if (options.limitRate) {
|
|
97
149
|
const { Throttle } = await import('../utils/throttle.js');
|
|
@@ -110,28 +162,24 @@ export class TelegramClient {
|
|
|
110
162
|
fileId: message.document.file_id,
|
|
111
163
|
timestamp: message.date
|
|
112
164
|
};
|
|
113
|
-
}
|
|
114
|
-
throw new Error(`Failed to upload to Telegram: ${err.message}`);
|
|
115
|
-
}
|
|
165
|
+
}, `Upload ${filename}`);
|
|
116
166
|
}
|
|
117
167
|
|
|
118
168
|
/**
|
|
119
169
|
* Download a file from Telegram (In-Memory buffer)
|
|
170
|
+
* Includes automatic retry with exponential backoff.
|
|
120
171
|
*/
|
|
121
172
|
async downloadFile(fileId) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Download the file
|
|
126
|
-
const fileStream = await this.bot.getFileStream(fileId);
|
|
173
|
+
return withRetry(async () => {
|
|
174
|
+
const fileStream = await this.bot.getFileStream(fileId);
|
|
127
175
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
176
|
+
const chunks = [];
|
|
177
|
+
for await (const chunk of fileStream) {
|
|
178
|
+
chunks.push(chunk);
|
|
179
|
+
}
|
|
133
180
|
|
|
134
|
-
|
|
181
|
+
return Buffer.concat(chunks);
|
|
182
|
+
}, `Download ${fileId.substring(0, 12)}...`);
|
|
135
183
|
}
|
|
136
184
|
|
|
137
185
|
/**
|
package/src/utils/branding.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
6
13
|
|
|
7
14
|
export const LOGO = `
|
|
8
15
|
████████╗ █████╗ ███████╗
|
|
@@ -14,7 +21,7 @@ export const LOGO = `
|
|
|
14
21
|
`;
|
|
15
22
|
|
|
16
23
|
export const TAGLINE = 'Telegram as Storage';
|
|
17
|
-
export const VERSION =
|
|
24
|
+
export const VERSION = pkg.version;
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
27
|
* Print the TAS banner
|
|
@@ -57,10 +64,10 @@ export function warn(msg) {
|
|
|
57
64
|
* Format file size
|
|
58
65
|
*/
|
|
59
66
|
export function formatSize(bytes) {
|
|
60
|
-
if (bytes
|
|
67
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
|
61
68
|
const k = 1024;
|
|
62
69
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
63
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
70
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
|
|
64
71
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
65
72
|
}
|
|
66
73
|
|
package/src/utils/chunker.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File chunking utilities for large files
|
|
3
|
-
*
|
|
3
|
+
* Telegram Bot API limit is 50 MB for bot uploads (sendDocument).
|
|
4
|
+
* We use 49 MB to leave room for the 64-byte WAS1 header.
|
|
5
|
+
* See: https://core.telegram.org/bots/api#senddocument
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
const MAX_CHUNK_SIZE =
|
|
8
|
+
const MAX_CHUNK_SIZE = 49 * 1024 * 1024; // 49 MB — Telegram Bot API safe limit
|
|
7
9
|
|
|
8
10
|
export class Chunker {
|
|
9
11
|
/**
|
package/src/utils/cli-helpers.js
CHANGED
|
@@ -39,14 +39,13 @@ export async function getPassword(passwordOption, allowCache = true) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Verify password against config
|
|
42
|
+
* Verify password against config (supports both legacy and new hash formats)
|
|
43
43
|
* @param {string} password - Password to verify
|
|
44
44
|
* @param {Object} config - Config object with passwordHash
|
|
45
45
|
* @returns {boolean}
|
|
46
46
|
*/
|
|
47
47
|
export function verifyPassword(password, config) {
|
|
48
|
-
|
|
49
|
-
return encryptor.getPasswordHash() === config.passwordHash;
|
|
48
|
+
return Encryptor.verifyPasswordHash(password, config.passwordHash);
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
/**
|
|
@@ -62,11 +61,13 @@ export function validateConfig(config) {
|
|
|
62
61
|
return { valid: false, errors };
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// v2: encrypted token, v1: plaintext token
|
|
65
|
+
const hasToken = config.encryptedBotToken || config.botToken;
|
|
66
|
+
if (!hasToken) {
|
|
67
|
+
errors.push('Missing bot token (botToken or encryptedBotToken)');
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
if (!config.botToken
|
|
70
|
+
if (config.botToken && !config.botToken.includes(':')) {
|
|
70
71
|
errors.push('Invalid bot token format (should contain :)');
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -84,6 +85,29 @@ export function validateConfig(config) {
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Decrypt bot token from config using password
|
|
90
|
+
* Supports both v1 (plaintext) and v2 (encrypted) configs
|
|
91
|
+
* @param {Object} config - Config object
|
|
92
|
+
* @param {string} password - User's password
|
|
93
|
+
* @returns {string} - Decrypted bot token
|
|
94
|
+
*/
|
|
95
|
+
export function decryptBotToken(config, password) {
|
|
96
|
+
// v1: plaintext token (backward compatibility)
|
|
97
|
+
if (config.botToken) {
|
|
98
|
+
return config.botToken;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// v2: encrypted token
|
|
102
|
+
if (config.encryptedBotToken) {
|
|
103
|
+
const encryptor = new Encryptor(password);
|
|
104
|
+
const encryptedBuffer = Buffer.from(config.encryptedBotToken, 'base64');
|
|
105
|
+
return encryptor.decrypt(encryptedBuffer).toString('utf-8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw new Error('No bot token found in config');
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
/**
|
|
88
112
|
* Load and validate config
|
|
89
113
|
* @param {string} dataDir - Data directory path
|
|
@@ -143,3 +167,17 @@ export async function getAndVerifyPassword(passwordOption, dataDir) {
|
|
|
143
167
|
|
|
144
168
|
return password;
|
|
145
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve config by decrypting bot token if needed.
|
|
173
|
+
* Returns a config object with a guaranteed plaintext `botToken` field.
|
|
174
|
+
* @param {Object} config - Raw config from disk
|
|
175
|
+
* @param {string} password - Verified password
|
|
176
|
+
* @returns {Object} - Config with decrypted botToken
|
|
177
|
+
*/
|
|
178
|
+
export function resolveConfig(config, password) {
|
|
179
|
+
return {
|
|
180
|
+
...config,
|
|
181
|
+
botToken: decryptBotToken(config, password)
|
|
182
|
+
};
|
|
183
|
+
}
|
package/src/utils/progress.js
CHANGED
|
@@ -76,10 +76,10 @@ export class ProgressBar {
|
|
|
76
76
|
* Format bytes to human readable
|
|
77
77
|
*/
|
|
78
78
|
formatBytes(bytes) {
|
|
79
|
-
if (bytes
|
|
79
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
|
80
80
|
const k = 1024;
|
|
81
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
82
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
81
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
82
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
|
|
83
83
|
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
|
|
84
84
|
}
|
|
85
85
|
|