@nightowne/tas-cli 2.3.0 → 2.4.1
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 +347 -203
- package/package.json +4 -4
- package/src/cli.js +3 -1
- package/src/crypto/encryption.js +15 -2
- package/src/fuse/mount.js +137 -173
- package/src/index.js +126 -192
- package/src/share/server.js +19 -80
- package/src/sync/sync.js +23 -159
- package/src/telegram/client.js +9 -3
- package/src/utils/branding.js +1 -8
- package/src/utils/download-stream.js +87 -0
package/src/sync/sync.js
CHANGED
|
@@ -13,10 +13,6 @@ 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
|
-
|
|
20
16
|
// Ignore patterns
|
|
21
17
|
const IGNORE_PATTERNS = [
|
|
22
18
|
/^\./, // Hidden files
|
|
@@ -35,7 +31,6 @@ export class SyncEngine extends EventEmitter {
|
|
|
35
31
|
this.config = options.config;
|
|
36
32
|
this.limitRate = options.limitRate || null;
|
|
37
33
|
this.watchers = new Map(); // path -> FSWatcher
|
|
38
|
-
this.rootWatchers = new Map(); // rootPath -> Set of watched subpaths (for Linux recursive watch fallback)
|
|
39
34
|
this.pendingChanges = new Map(); // path -> timeout
|
|
40
35
|
this.db = null;
|
|
41
36
|
this.running = false;
|
|
@@ -110,8 +105,6 @@ export class SyncEngine extends EventEmitter {
|
|
|
110
105
|
const promises = [];
|
|
111
106
|
|
|
112
107
|
const worker = async () => {
|
|
113
|
-
let consecutiveErrors = 0;
|
|
114
|
-
|
|
115
108
|
while (queue.length > 0) {
|
|
116
109
|
const file = queue.shift();
|
|
117
110
|
const existing = stateMap.get(file.relativePath);
|
|
@@ -148,7 +141,6 @@ export class SyncEngine extends EventEmitter {
|
|
|
148
141
|
// Update sync state
|
|
149
142
|
this.db.updateSyncState(folder.id, file.relativePath, hash, file.mtime);
|
|
150
143
|
uploaded++;
|
|
151
|
-
consecutiveErrors = 0; // Reset on success
|
|
152
144
|
|
|
153
145
|
this.emit('file-upload-complete', { file: file.relativePath });
|
|
154
146
|
} catch (err) {
|
|
@@ -156,12 +148,9 @@ export class SyncEngine extends EventEmitter {
|
|
|
156
148
|
if (err.message.includes('duplicate')) {
|
|
157
149
|
this.db.updateSyncState(folder.id, file.relativePath, hash, file.mtime);
|
|
158
150
|
skipped++;
|
|
159
|
-
consecutiveErrors = 0;
|
|
160
151
|
} else {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const delay = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, consecutiveErrors - 1), MAX_BACKOFF_MS);
|
|
164
|
-
await new Promise(r => setTimeout(r, delay));
|
|
152
|
+
// Sleep briefly on non-duplicate error (potential rate limits)
|
|
153
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
165
154
|
this.emit('file-upload-error', { file: file.relativePath, error: err.message });
|
|
166
155
|
}
|
|
167
156
|
}
|
|
@@ -249,148 +238,37 @@ export class SyncEngine extends EventEmitter {
|
|
|
249
238
|
}
|
|
250
239
|
}
|
|
251
240
|
|
|
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
|
-
|
|
326
241
|
/**
|
|
327
242
|
* Start watching a folder
|
|
328
243
|
*/
|
|
329
244
|
watchFolder(folderPath) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (this.watchers.has(folderPath)) {
|
|
334
|
-
return; // Already watching
|
|
335
|
-
}
|
|
336
|
-
|
|
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
|
-
});
|
|
245
|
+
if (this.watchers.has(folderPath)) {
|
|
246
|
+
return; // Already watching
|
|
247
|
+
}
|
|
346
248
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
// Manual recursive watch for Linux/other platforms
|
|
351
|
-
if (this.rootWatchers.has(folderPath)) {
|
|
352
|
-
return; // Already watching
|
|
249
|
+
const watcher = fs.watch(folderPath, { recursive: true }, (event, filename) => {
|
|
250
|
+
if (filename) {
|
|
251
|
+
this.handleFileChange(folderPath, filename);
|
|
353
252
|
}
|
|
253
|
+
});
|
|
354
254
|
|
|
355
|
-
|
|
356
|
-
this.
|
|
255
|
+
watcher.on('error', (err) => {
|
|
256
|
+
this.emit('watch-error', { folder: folderPath, error: err.message });
|
|
257
|
+
});
|
|
357
258
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
for (const subdir of subdirs) {
|
|
361
|
-
this.watchSingleDir(folderPath, subdir);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
this.emit('watch-start', { folder: folderPath });
|
|
365
|
-
}
|
|
259
|
+
this.watchers.set(folderPath, watcher);
|
|
260
|
+
this.emit('watch-start', { folder: folderPath });
|
|
366
261
|
}
|
|
367
262
|
|
|
368
263
|
/**
|
|
369
264
|
* Stop watching a folder
|
|
370
265
|
*/
|
|
371
266
|
unwatchFolder(folderPath) {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
}
|
|
267
|
+
const watcher = this.watchers.get(folderPath);
|
|
268
|
+
if (watcher) {
|
|
269
|
+
watcher.close();
|
|
270
|
+
this.watchers.delete(folderPath);
|
|
271
|
+
this.emit('watch-stop', { folder: folderPath });
|
|
394
272
|
}
|
|
395
273
|
}
|
|
396
274
|
|
|
@@ -424,25 +302,11 @@ export class SyncEngine extends EventEmitter {
|
|
|
424
302
|
this.pendingChanges.clear();
|
|
425
303
|
|
|
426
304
|
// Close all watchers
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
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();
|
|
305
|
+
for (const [folderPath, watcher] of this.watchers) {
|
|
306
|
+
watcher.close();
|
|
307
|
+
this.emit('watch-stop', { folder: folderPath });
|
|
445
308
|
}
|
|
309
|
+
this.watchers.clear();
|
|
446
310
|
|
|
447
311
|
if (this.db) {
|
|
448
312
|
this.db.close();
|
package/src/telegram/client.js
CHANGED
|
@@ -86,13 +86,16 @@ export class TelegramClient {
|
|
|
86
86
|
async waitForChatId(timeout = 120000) {
|
|
87
87
|
return new Promise((resolve, reject) => {
|
|
88
88
|
const pollingBot = new TelegramBot(this.bot.token, { polling: true });
|
|
89
|
+
let isCompleted = false;
|
|
89
90
|
|
|
90
91
|
const timer = setTimeout(() => {
|
|
92
|
+
isCompleted = true;
|
|
91
93
|
pollingBot.stopPolling();
|
|
92
94
|
reject(new Error('Timeout waiting for message. Please message your bot on Telegram.'));
|
|
93
95
|
}, timeout);
|
|
94
96
|
|
|
95
97
|
pollingBot.on('message', (msg) => {
|
|
98
|
+
isCompleted = true;
|
|
96
99
|
clearTimeout(timer);
|
|
97
100
|
pollingBot.stopPolling();
|
|
98
101
|
this.chatId = msg.chat.id;
|
|
@@ -104,10 +107,13 @@ export class TelegramClient {
|
|
|
104
107
|
});
|
|
105
108
|
|
|
106
109
|
pollingBot.on('polling_error', (err) => {
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
// Only suppress errors after successful completion or during cleanup
|
|
111
|
+
// During active polling, log all errors for debugging
|
|
112
|
+
if (isCompleted && err.message.includes('ETELEGRAM')) {
|
|
113
|
+
// Suppress expected cleanup errors
|
|
114
|
+
return;
|
|
110
115
|
}
|
|
116
|
+
console.error('Polling error:', err.message);
|
|
111
117
|
});
|
|
112
118
|
});
|
|
113
119
|
}
|
package/src/utils/branding.js
CHANGED
|
@@ -3,13 +3,6 @@
|
|
|
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'));
|
|
13
6
|
|
|
14
7
|
export const LOGO = `
|
|
15
8
|
████████╗ █████╗ ███████╗
|
|
@@ -21,7 +14,7 @@ export const LOGO = `
|
|
|
21
14
|
`;
|
|
22
15
|
|
|
23
16
|
export const TAGLINE = 'Telegram as Storage';
|
|
24
|
-
export const VERSION =
|
|
17
|
+
export const VERSION = '2.4.1';
|
|
25
18
|
|
|
26
19
|
/**
|
|
27
20
|
* Print the TAS banner
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared download pipeline — Telegram → Decrypt → Decompress
|
|
3
|
+
*
|
|
4
|
+
* Eliminates the triplicated download-stream pattern across
|
|
5
|
+
* index.js, share/server.js, and fuse/mount.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Readable } from 'stream';
|
|
9
|
+
import { pipeline } from 'stream/promises';
|
|
10
|
+
import { parseHeader, HEADER_SIZE } from './chunker.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create the download pipeline streams for a file stored in Telegram.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} options
|
|
16
|
+
* @param {object} options.client – TelegramClient instance (initialized, with chatId set)
|
|
17
|
+
* @param {Array} options.chunks – Chunk records from DB, each with { chunk_index, file_telegram_id, size }
|
|
18
|
+
* @param {object} options.encryptor – Encryptor instance
|
|
19
|
+
* @param {object} options.compressor – Compressor instance
|
|
20
|
+
* @param {function} [options.onChunkDownloaded] – Optional callback({ chunkIndex, totalChunks, bytesDownloaded, totalBytes })
|
|
21
|
+
* @returns {Promise<{ readable: Readable, header: object }>}
|
|
22
|
+
* readable: a stream of decrypted (and decompressed) file content
|
|
23
|
+
* header: parsed WAS1 header from the first chunk
|
|
24
|
+
*/
|
|
25
|
+
export async function createDownloadPipeline({ client, chunks, encryptor, compressor, onChunkDownloaded }) {
|
|
26
|
+
if (chunks.length === 0) throw new Error('No chunks found');
|
|
27
|
+
|
|
28
|
+
// Sort by chunk_index
|
|
29
|
+
const sortedChunks = [...chunks].sort((a, b) => a.chunk_index - b.chunk_index);
|
|
30
|
+
|
|
31
|
+
// Download the first chunk to inspect the header
|
|
32
|
+
const firstChunkData = await client.downloadFile(sortedChunks[0].file_telegram_id);
|
|
33
|
+
const header = parseHeader(firstChunkData);
|
|
34
|
+
|
|
35
|
+
const decryptStream = encryptor.getDecryptStream();
|
|
36
|
+
const decompressStream = compressor.getDecompressStream(header.compressed);
|
|
37
|
+
|
|
38
|
+
const totalBytes = sortedChunks.reduce((acc, c) => acc + (c.size || 0), 0);
|
|
39
|
+
let bytesDownloaded = 0;
|
|
40
|
+
let currentIndex = 0;
|
|
41
|
+
let preloadedFirst = firstChunkData;
|
|
42
|
+
|
|
43
|
+
const telegramStream = new Readable({
|
|
44
|
+
async read() {
|
|
45
|
+
try {
|
|
46
|
+
if (currentIndex >= sortedChunks.length) {
|
|
47
|
+
this.push(null);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let data;
|
|
52
|
+
if (currentIndex === 0 && preloadedFirst) {
|
|
53
|
+
data = preloadedFirst;
|
|
54
|
+
preloadedFirst = null;
|
|
55
|
+
} else {
|
|
56
|
+
data = await client.downloadFile(sortedChunks[currentIndex].file_telegram_id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
bytesDownloaded += data.length;
|
|
60
|
+
onChunkDownloaded?.({
|
|
61
|
+
chunkIndex: currentIndex,
|
|
62
|
+
totalChunks: sortedChunks.length,
|
|
63
|
+
bytesDownloaded,
|
|
64
|
+
totalBytes
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Strip the WAS1 header before pushing into the decrypt pipeline
|
|
68
|
+
this.push(data.subarray(HEADER_SIZE));
|
|
69
|
+
currentIndex++;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this.destroy(err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Wire the internal pipeline: telegram → decrypt → decompress
|
|
77
|
+
// We use a PassThrough as the readable end so callers can pipe/pipeline it freely.
|
|
78
|
+
const { PassThrough } = await import('stream');
|
|
79
|
+
const output = new PassThrough();
|
|
80
|
+
|
|
81
|
+
// Run the internal pipeline in the background; errors propagate through the output stream.
|
|
82
|
+
pipeline(telegramStream, decryptStream, decompressStream, output).catch((err) => {
|
|
83
|
+
if (!output.destroyed) output.destroy(err);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return { readable: output, header };
|
|
87
|
+
}
|