@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/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
- 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));
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
- const isRecursiveSupported = process.platform === 'darwin' || process.platform === 'win32';
331
-
332
- if (isRecursiveSupported) {
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
- 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
249
+ const watcher = fs.watch(folderPath, { recursive: true }, (event, filename) => {
250
+ if (filename) {
251
+ this.handleFileChange(folderPath, filename);
353
252
  }
253
+ });
354
254
 
355
- // Watch root
356
- this.watchSingleDir(folderPath, folderPath);
255
+ watcher.on('error', (err) => {
256
+ this.emit('watch-error', { folder: folderPath, error: err.message });
257
+ });
357
258
 
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
- }
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 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
- }
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 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();
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();
@@ -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
- // Ignore polling errors during shutdown
108
- if (!err.message.includes('ETELEGRAM')) {
109
- console.error('Polling error:', err.message);
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
  }
@@ -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 = pkg.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
+ }