@nightowne/tas-cli 2.0.0 → 2.1.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/src/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
+ import { pipeline } from 'stream/promises';
8
9
  import { Encryptor, hashFile } from './crypto/encryption.js';
9
10
  import { Compressor } from './utils/compression.js';
10
11
  import { Chunker, createHeader, parseHeader, HEADER_SIZE } from './utils/chunker.js';
@@ -23,10 +24,10 @@ export async function processFile(filePath, options) {
23
24
 
24
25
  onProgress?.('Reading file...');
25
26
 
26
- // Read file
27
+ // Read file initially just to get size
27
28
  const filename = customName || path.basename(filePath);
28
- const fileData = fs.readFileSync(filePath);
29
- const originalSize = fileData.length;
29
+ const stats = fs.statSync(filePath);
30
+ const originalSize = stats.size;
30
31
 
31
32
  // Calculate hash
32
33
  onProgress?.('Calculating hash...');
@@ -41,99 +42,134 @@ export async function processFile(filePath, options) {
41
42
  throw new Error('File already uploaded (duplicate hash)');
42
43
  }
43
44
 
44
- // Compress
45
- onProgress?.('Compressing...');
45
+ // Prepare processing components
46
46
  const compressor = new Compressor();
47
- const { data: compressedData, compressed } = await compressor.compress(fileData, filename);
47
+ const { stream: compressStream, compressed } = compressor.getCompressStream(filename);
48
+ const flags = compressed ? 1 : 0;
48
49
 
49
- // Encrypt
50
- onProgress?.('Encrypting...');
51
50
  const encryptor = new Encryptor(password);
52
- const encryptedData = encryptor.encrypt(compressedData);
53
-
54
- // Chunk if needed (Telegram bot limit ~50MB per file)
55
- onProgress?.('Preparing chunks...');
56
- const chunks = [];
57
- const numChunks = Math.ceil(encryptedData.length / TELEGRAM_CHUNK_SIZE);
58
-
59
- for (let i = 0; i < numChunks; i++) {
60
- const start = i * TELEGRAM_CHUNK_SIZE;
61
- const end = Math.min(start + TELEGRAM_CHUNK_SIZE, encryptedData.length);
62
- chunks.push({
63
- index: i,
64
- total: numChunks,
65
- data: encryptedData.subarray(start, end)
66
- });
67
- }
51
+ const encryptStream = encryptor.getEncryptStream();
68
52
 
69
- // Prepare files with headers
70
- const tempDir = path.join(dataDir, 'tmp');
53
+ const tempDir = process.env.TAS_TMP_DIR || path.join(dataDir, 'tmp');
71
54
  if (!fs.existsSync(tempDir)) {
72
55
  fs.mkdirSync(tempDir, { recursive: true });
73
56
  }
74
57
 
75
- const chunkFiles = [];
76
- const flags = compressed ? 1 : 0;
77
-
78
- for (const chunk of chunks) {
79
- const header = createHeader(filename, originalSize, chunk.index, chunk.total, flags);
80
- const chunkData = Buffer.concat([header, chunk.data]);
81
-
82
- const chunkFilename = chunks.length > 1
83
- ? `${hash.substring(0, 12)}.part${chunk.index}.tas`
84
- : `${hash.substring(0, 12)}.tas`;
85
-
86
- const chunkPath = path.join(tempDir, chunkFilename);
87
- fs.writeFileSync(chunkPath, chunkData);
88
-
89
- chunkFiles.push({
90
- index: chunk.index,
91
- path: chunkPath,
92
- size: chunkData.length
93
- });
94
- }
95
-
96
58
  // Connect to Telegram
97
59
  onProgress?.('Connecting to Telegram...');
98
60
  const client = new TelegramClient(dataDir);
99
61
  await client.initialize(config.botToken);
100
62
  client.setChatId(config.chatId);
101
63
 
102
- // Upload chunks
64
+ // We will stream through a custom Writable chunker
65
+ const { Writable } = await import('stream');
66
+
67
+ // First pass estimation (for calculating total chunks and progress)
68
+ // We don't know the exact final size due to compression and encryption overhead,
69
+ // so we'll estimate total chunks and update it if needed.
70
+ // For small files < 49MB we assume 1 chunk.
71
+ let estimatedSize = compressed ? originalSize : originalSize + 128; // Add encryption overhead
72
+ if (compressed && originalSize > 1024 * 1024) estimatedSize = originalSize * 0.8; // Rough guess
73
+ let estimatedChunks = Math.ceil(estimatedSize / TELEGRAM_CHUNK_SIZE) || 1;
74
+
75
+ // Register file in DB
103
76
  const fileId = db.addFile({
104
77
  filename,
105
78
  hash,
106
79
  originalSize,
107
- storedSize: encryptedData.length,
108
- chunks: chunks.length,
80
+ storedSize: 0, // Will update later
81
+ chunks: estimatedChunks,
109
82
  compressed
110
83
  });
111
84
 
85
+ onProgress?.('Processing and uploading streams...');
112
86
  let uploadedBytes = 0;
113
- const totalBytes = chunkFiles.reduce((acc, c) => acc + c.size, 0);
87
+ let chunkIndex = 0;
88
+
89
+ let currentChunkBuffer = Buffer.alloc(0);
90
+ let totalStoredSize = 0;
91
+
92
+ // Helper to upload a single chunk
93
+ const uploadCurrentChunk = async (isFinal = false) => {
94
+ if (currentChunkBuffer.length === 0 && !isFinal) return; // Nothing to upload
95
+ if (currentChunkBuffer.length === 0 && isFinal && chunkIndex > 0) return; // Empty final chunk after perfect split
96
+
97
+ // At this point we know if it's the final chunk, so we know the total chunks
98
+ const totalChunks = isFinal ? chunkIndex + 1 : Math.max(estimatedChunks, chunkIndex + 1);
99
+
100
+ const header = createHeader(filename, originalSize, chunkIndex, totalChunks, flags);
101
+ const chunkData = Buffer.concat([header, currentChunkBuffer]);
114
102
 
115
- for (const chunk of chunkFiles) {
116
- onProgress?.(`Uploading chunk ${chunk.index + 1}/${chunkFiles.length}...`);
103
+ const chunkFilename = totalChunks > 1
104
+ ? `${hash.substring(0, 12)}.part${chunkIndex}.tas`
105
+ : `${hash.substring(0, 12)}.tas`;
106
+
107
+ const chunkPath = path.join(tempDir, chunkFilename);
108
+ fs.writeFileSync(chunkPath, chunkData);
117
109
 
118
- const caption = chunks.length > 1
119
- ? `📦 ${filename} (${chunk.index + 1}/${chunks.length})`
110
+ const caption = totalChunks > 1
111
+ ? `📦 ${filename} (${chunkIndex + 1}/${totalChunks})`
120
112
  : `📦 ${filename}`;
121
113
 
122
- const result = await client.sendFile(chunk.path, caption);
114
+ onProgress?.(`Uploading chunk ${chunkIndex + 1}...`);
115
+
116
+ const result = await client.sendFile(chunkPath, caption);
123
117
 
124
- uploadedBytes += chunk.size;
125
- onByteProgress?.({ uploaded: uploadedBytes, total: totalBytes, chunk: chunk.index + 1, totalChunks: chunkFiles.length });
118
+ uploadedBytes += chunkData.length;
119
+ totalStoredSize += currentChunkBuffer.length;
126
120
 
127
- // Store file_id instead of message_id for downloads
128
- db.addChunk(fileId, chunk.index, result.messageId.toString(), chunk.size);
121
+ onByteProgress?.({ uploaded: uploadedBytes, total: estimatedSize, chunk: chunkIndex + 1, totalChunks });
129
122
 
130
- // Also store file_id for easier retrieval
123
+ // Store file_id
124
+ db.addChunk(fileId, chunkIndex, result.messageId.toString(), chunkData.length);
131
125
  db.db.prepare('UPDATE chunks SET file_telegram_id = ? WHERE file_id = ? AND chunk_index = ?')
132
- .run(result.fileId, fileId, chunk.index);
126
+ .run(result.fileId, fileId, chunkIndex);
133
127
 
134
- // Clean up temp file
135
- fs.unlinkSync(chunk.path);
136
- }
128
+ // Clean up temp file immediately to save disk space
129
+ fs.unlinkSync(chunkPath);
130
+
131
+ chunkIndex++;
132
+ currentChunkBuffer = Buffer.alloc(0);
133
+ };
134
+
135
+ const chunkingStream = new Writable({
136
+ async write(chunk, encoding, callback) {
137
+ currentChunkBuffer = Buffer.concat([currentChunkBuffer, chunk]);
138
+
139
+ // If we exceeded the chunk limit, flush it
140
+ if (currentChunkBuffer.length >= TELEGRAM_CHUNK_SIZE) {
141
+ const overflow = currentChunkBuffer.subarray(TELEGRAM_CHUNK_SIZE);
142
+ currentChunkBuffer = currentChunkBuffer.subarray(0, TELEGRAM_CHUNK_SIZE);
143
+
144
+ try {
145
+ await uploadCurrentChunk(false);
146
+ currentChunkBuffer = overflow; // carry over
147
+ callback();
148
+ } catch (err) {
149
+ callback(err);
150
+ }
151
+ } else {
152
+ callback();
153
+ }
154
+ },
155
+ async final(callback) {
156
+ try {
157
+ await uploadCurrentChunk(true);
158
+ callback();
159
+ } catch (err) {
160
+ callback(err);
161
+ }
162
+ }
163
+ });
164
+
165
+ const readStream = fs.createReadStream(filePath);
166
+
167
+ // Run the pipeline: Read -> Compress -> Encrypt -> Chunk & Upload
168
+ await pipeline(readStream, compressStream, encryptStream, chunkingStream);
169
+
170
+ // Update the DB with the final accurate values
171
+ db.db.prepare('UPDATE files SET stored_size = ?, chunks = ? WHERE id = ?')
172
+ .run(totalStoredSize, chunkIndex, fileId);
137
173
 
138
174
  db.close();
139
175
 
@@ -148,8 +184,8 @@ export async function processFile(filePath, options) {
148
184
  filename,
149
185
  hash,
150
186
  originalSize,
151
- storedSize: encryptedData.length,
152
- chunks: chunks.length,
187
+ storedSize: totalStoredSize,
188
+ chunks: chunkIndex,
153
189
  compressed
154
190
  };
155
191
  }
@@ -173,57 +209,91 @@ export async function retrieveFile(fileRecord, options) {
173
209
  throw new Error('No chunk metadata found for this file');
174
210
  }
175
211
 
212
+ // Prepare components
213
+ const encryptor = new Encryptor(password);
214
+ const decryptStream = encryptor.getDecryptStream();
215
+
216
+ const tempDir = process.env.TAS_TMP_DIR || path.join(dataDir, 'tmp');
217
+ if (!fs.existsSync(tempDir)) {
218
+ fs.mkdirSync(tempDir, { recursive: true });
219
+ }
220
+
176
221
  // Connect to Telegram
177
222
  const client = new TelegramClient(dataDir);
178
223
  await client.initialize(config.botToken);
179
224
  client.setChatId(config.chatId);
180
225
 
181
- // Download all chunks
182
- const downloadedChunks = [];
183
- let downloadedBytes = 0;
184
- const totalBytes = fileRecord.stored_size || chunks.reduce((acc, c) => acc + (c.size || 0), 0);
226
+ // Get total size from first chunk's header, or from DB
227
+ const firstChunkData = await client.downloadFile(chunks[0].file_telegram_id);
228
+ const header = parseHeader(firstChunkData);
185
229
 
186
- for (const chunk of chunks) {
187
- onProgress?.(`Downloading chunk ${chunk.chunk_index + 1}/${chunks.length}...`);
230
+ // Total original uncompressed size
231
+ let expectedOriginalSize = header.originalSize;
232
+ let wasCompressed = header.compressed;
188
233
 
189
- const data = await client.downloadFile(chunk.file_telegram_id);
190
- downloadedBytes += data.length;
191
- onByteProgress?.({ downloaded: downloadedBytes, total: totalBytes, chunk: chunk.chunk_index + 1, totalChunks: chunks.length });
234
+ const compressor = new Compressor();
235
+ const decompressStream = compressor.getDecompressStream(wasCompressed);
192
236
 
193
- // Parse header
194
- const header = parseHeader(data);
195
- const payload = data.subarray(HEADER_SIZE);
237
+ // We need a Readable stream that will lazily fetch chunks from Telegram
238
+ // and push them into the decryption pipeline.
239
+ const { Readable } = await import('stream');
196
240
 
197
- downloadedChunks.push({
198
- index: header.chunkIndex,
199
- total: header.totalChunks,
200
- data: payload,
201
- compressed: header.compressed
202
- });
203
- }
241
+ const totalBytes = fileRecord.stored_size || chunks.reduce((acc, c) => acc + (c.size || 0), 0);
242
+ let downloadedBytes = 0;
204
243
 
205
- // Reassemble
206
- onProgress?.('Reassembling...');
207
- downloadedChunks.sort((a, b) => a.index - b.index);
208
- const encryptedData = Buffer.concat(downloadedChunks.map(c => c.data));
244
+ // Pre-sort chunks by index so we download them in correct order
245
+ chunks.sort((a, b) => a.chunk_index - b.chunk_index);
246
+
247
+ let currentChunkIndex = 0;
248
+
249
+ // We already downloaded the first chunk to inspect its header, we shouldn't discard it.
250
+ let preloadedFirstChunk = firstChunkData;
251
+
252
+ const downloadStream = new Readable({
253
+ async read() {
254
+ try {
255
+ if (currentChunkIndex >= chunks.length) {
256
+ this.push(null); // End of stream
257
+ return;
258
+ }
259
+
260
+ const chunk = chunks[currentChunkIndex];
261
+ onProgress?.(`Downloading chunk ${chunk.chunk_index + 1}/${chunks.length}...`);
262
+
263
+ let data;
264
+ if (currentChunkIndex === 0 && preloadedFirstChunk) {
265
+ data = preloadedFirstChunk;
266
+ preloadedFirstChunk = null;
267
+ } else {
268
+ data = await client.downloadFile(chunk.file_telegram_id);
269
+ }
270
+
271
+ downloadedBytes += data.length;
272
+ onByteProgress?.({ downloaded: downloadedBytes, total: totalBytes, chunk: chunk.chunk_index + 1, totalChunks: chunks.length });
273
+
274
+ // Strip header before pushing
275
+ const payload = data.subarray(HEADER_SIZE);
276
+ this.push(payload);
277
+
278
+ currentChunkIndex++;
279
+ } catch (err) {
280
+ this.destroy(err);
281
+ }
282
+ }
283
+ });
209
284
 
210
- // Decrypt
211
- onProgress?.('Decrypting...');
212
- const encryptor = new Encryptor(password);
213
- const compressedData = encryptor.decrypt(encryptedData);
285
+ const writeStream = fs.createWriteStream(outputPath);
286
+ const { pipeline } = await import('stream/promises');
214
287
 
215
- // Decompress
216
- onProgress?.('Decompressing...');
217
- const compressor = new Compressor();
218
- const wasCompressed = downloadedChunks[0].compressed;
219
- const originalData = await compressor.decompress(compressedData, wasCompressed);
288
+ onProgress?.('Decrypting, decompressing, and writing file...');
289
+
290
+ // Pipeline: Download from Telegram -> Decrypt -> Decompress -> Disk
291
+ await pipeline(downloadStream, decryptStream, decompressStream, writeStream);
220
292
 
221
- // Write to output
222
- onProgress?.('Writing file...');
223
- fs.writeFileSync(outputPath, originalData);
293
+ const finalStats = fs.statSync(outputPath);
224
294
 
225
295
  return {
226
296
  path: outputPath,
227
- size: originalData.length
297
+ size: finalStats.size
228
298
  };
229
299
  }
@@ -241,33 +241,63 @@ export class ShareServer {
241
241
  }
242
242
 
243
243
  /**
244
- * Download and decrypt a file from Telegram
244
+ * Stream a decrypted file from Telegram directly to the HTTP response
245
245
  */
246
- async downloadAndDecrypt(fileRecord) {
246
+ async streamToResponse(fileRecord, res) {
247
247
  const chunks = this.db.getChunks(fileRecord.id);
248
248
  if (chunks.length === 0) throw new Error('No chunks found');
249
249
 
250
- const downloadedChunks = [];
251
-
252
- for (const chunk of chunks) {
253
- const data = await this.client.downloadFile(chunk.file_telegram_id);
254
- const header = parseHeader(data);
255
- const payload = data.subarray(HEADER_SIZE);
256
-
257
- downloadedChunks.push({
258
- index: header.chunkIndex,
259
- data: payload,
260
- compressed: header.compressed
261
- });
262
- }
263
-
264
- downloadedChunks.sort((a, b) => a.index - b.index);
265
- const encryptedData = Buffer.concat(downloadedChunks.map(c => c.data));
266
-
267
- const compressedData = this.encryptor.decrypt(encryptedData);
250
+ // Pre-sort chunks by index so we download them in correct order
251
+ chunks.sort((a, b) => a.chunk_index - b.chunk_index);
252
+
253
+ // Get total size from first chunk's header
254
+ const firstChunkData = await this.client.downloadFile(chunks[0].file_telegram_id);
255
+ const header = parseHeader(firstChunkData);
256
+ let wasCompressed = header.compressed;
257
+
258
+ // Prepare streams
259
+ const decryptStream = this.encryptor.getDecryptStream();
260
+ const decompressStream = this.compressor.getDecompressStream(wasCompressed);
261
+
262
+ // We need a Readable stream that will lazily fetch chunks from Telegram
263
+ const { Readable } = await import('stream');
264
+ const { pipeline } = await import('stream/promises');
265
+
266
+ const self = this;
267
+ let currentChunkIndex = 0;
268
+ let preloadedFirstChunk = firstChunkData;
269
+
270
+ const downloadStream = new Readable({
271
+ async read() {
272
+ try {
273
+ if (currentChunkIndex >= chunks.length) {
274
+ this.push(null); // End of stream
275
+ return;
276
+ }
277
+
278
+ const chunk = chunks[currentChunkIndex];
279
+ let data;
280
+
281
+ if (currentChunkIndex === 0 && preloadedFirstChunk) {
282
+ data = preloadedFirstChunk;
283
+ preloadedFirstChunk = null;
284
+ } else {
285
+ data = await self.client.downloadFile(chunk.file_telegram_id);
286
+ }
287
+
288
+ // Strip header before pushing
289
+ const payload = data.subarray(HEADER_SIZE);
290
+ this.push(payload);
291
+
292
+ currentChunkIndex++;
293
+ } catch (err) {
294
+ this.destroy(err);
295
+ }
296
+ }
297
+ });
268
298
 
269
- const wasCompressed = downloadedChunks[0].compressed;
270
- return await this.compressor.decompress(compressedData, wasCompressed);
299
+ // Pipeline: Download from Telegram -> Decrypt -> Decompress -> HTTP Response
300
+ await pipeline(downloadStream, decryptStream, decompressStream, res);
271
301
  }
272
302
 
273
303
  /**
@@ -330,9 +360,6 @@ export class ShareServer {
330
360
  return;
331
361
  }
332
362
 
333
- // Download the file from Telegram, decrypt, and serve
334
- const data = await this.downloadAndDecrypt(fileRecord);
335
-
336
363
  // Increment download count
337
364
  this.db.incrementShareDownload(token);
338
365
 
@@ -355,9 +382,11 @@ export class ShareServer {
355
382
  res.writeHead(200, {
356
383
  'Content-Type': contentType,
357
384
  'Content-Disposition': `attachment; filename="${fileRecord.filename}"`,
358
- 'Content-Length': data.length
385
+ 'Content-Length': fileRecord.original_size
359
386
  });
360
- res.end(data);
387
+
388
+ // Download the file from Telegram, decrypt, decompress and stream directly to 'res'
389
+ await this.streamToResponse(fileRecord, res);
361
390
 
362
391
  } catch (err) {
363
392
  console.error('Share server error:', err.message);
package/src/sync/sync.js CHANGED
@@ -29,6 +29,7 @@ export class SyncEngine extends EventEmitter {
29
29
  this.dataDir = options.dataDir;
30
30
  this.password = options.password;
31
31
  this.config = options.config;
32
+ this.limitRate = options.limitRate || null;
32
33
  this.watchers = new Map(); // path -> FSWatcher
33
34
  this.pendingChanges = new Map(); // path -> timeout
34
35
  this.db = null;
@@ -98,53 +99,70 @@ export class SyncEngine extends EventEmitter {
98
99
  let uploaded = 0;
99
100
  let skipped = 0;
100
101
 
101
- for (const file of files) {
102
- const existing = stateMap.get(file.relativePath);
102
+ // Process files with concurrency limit
103
+ const CONCURRENCY = 4;
104
+ const queue = [...files];
105
+ const promises = [];
103
106
 
104
- // Check if file has changed (by mtime)
105
- if (existing && existing.mtime >= file.mtime) {
106
- skipped++;
107
- continue;
108
- }
107
+ const worker = async () => {
108
+ while (queue.length > 0) {
109
+ const file = queue.shift();
110
+ const existing = stateMap.get(file.relativePath);
109
111
 
110
- // Calculate hash to detect actual changes
111
- const hash = await hashFile(file.path);
112
+ // Check if file has changed (by mtime)
113
+ if (existing && existing.mtime >= file.mtime) {
114
+ skipped++;
115
+ continue;
116
+ }
112
117
 
113
- if (existing && existing.file_hash === hash) {
114
- // File unchanged, just update mtime
115
- this.db.updateSyncState(folder.id, file.relativePath, hash, file.mtime);
116
- skipped++;
117
- continue;
118
- }
118
+ // Calculate hash to detect actual changes
119
+ const hash = await hashFile(file.path);
119
120
 
120
- // File is new or changed - upload it
121
- try {
122
- this.emit('file-upload-start', { file: file.relativePath });
121
+ if (existing && existing.file_hash === hash) {
122
+ // File unchanged, just update mtime
123
+ this.db.updateSyncState(folder.id, file.relativePath, hash, file.mtime);
124
+ skipped++;
125
+ continue;
126
+ }
123
127
 
124
- await processFile(file.path, {
125
- password: this.password,
126
- dataDir: this.dataDir,
127
- customName: file.relativePath, // Use relative path as name
128
- config: this.config,
129
- onProgress: (msg) => this.emit('progress', { file: file.relativePath, message: msg })
130
- });
128
+ // File is new or changed - upload it
129
+ try {
130
+ this.emit('file-upload-start', { file: file.relativePath });
131
131
 
132
- // Update sync state
133
- this.db.updateSyncState(folder.id, file.relativePath, hash, file.mtime);
134
- uploaded++;
132
+ await processFile(file.path, {
133
+ password: this.password,
134
+ dataDir: this.dataDir,
135
+ customName: file.relativePath, // Use relative path as name
136
+ config: this.config,
137
+ limitRate: this.limitRate ? Math.floor(this.limitRate / CONCURRENCY) : null,
138
+ onProgress: (msg) => this.emit('progress', { file: file.relativePath, message: msg })
139
+ });
135
140
 
136
- this.emit('file-upload-complete', { file: file.relativePath });
137
- } catch (err) {
138
- // File might already exist, skip
139
- if (err.message.includes('duplicate')) {
141
+ // Update sync state
140
142
  this.db.updateSyncState(folder.id, file.relativePath, hash, file.mtime);
141
- skipped++;
142
- } else {
143
- this.emit('file-upload-error', { file: file.relativePath, error: err.message });
143
+ uploaded++;
144
+
145
+ this.emit('file-upload-complete', { file: file.relativePath });
146
+ } catch (err) {
147
+ // File might already exist, skip
148
+ if (err.message.includes('duplicate')) {
149
+ this.db.updateSyncState(folder.id, file.relativePath, hash, file.mtime);
150
+ skipped++;
151
+ } else {
152
+ // Sleep briefly on non-duplicate error (potential rate limits)
153
+ await new Promise(r => setTimeout(r, 2000));
154
+ this.emit('file-upload-error', { file: file.relativePath, error: err.message });
155
+ }
144
156
  }
145
157
  }
158
+ };
159
+
160
+ for (let i = 0; i < CONCURRENCY; i++) {
161
+ promises.push(worker());
146
162
  }
147
163
 
164
+ await Promise.all(promises);
165
+
148
166
  this.emit('sync-complete', { folder: folderPath, uploaded, skipped });
149
167
 
150
168
  return { uploaded, skipped };
@@ -206,6 +224,7 @@ export class SyncEngine extends EventEmitter {
206
224
  dataDir: this.dataDir,
207
225
  customName: filename,
208
226
  config: this.config,
227
+ limitRate: this.limitRate,
209
228
  onProgress: (msg) => this.emit('progress', { file: filename, message: msg })
210
229
  });
211
230