@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/README.md +92 -113
- package/package.json +1 -1
- package/src/cli.js +20 -1
- package/src/crypto/encryption.js +128 -0
- package/src/fuse/mount.js +104 -40
- package/src/index.js +173 -103
- package/src/share/server.js +56 -27
- package/src/sync/sync.js +54 -35
- package/src/telegram/client.js +45 -17
- package/src/utils/chunker.js +11 -1
- package/src/utils/compression.js +30 -0
- package/src/utils/throttle.js +26 -0
package/src/telegram/client.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import TelegramBot from 'node-telegram-bot-api';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
import { pipeline } from 'stream/promises';
|
|
9
10
|
|
|
10
11
|
export class TelegramClient {
|
|
11
12
|
constructor(dataDir) {
|
|
@@ -18,8 +19,12 @@ export class TelegramClient {
|
|
|
18
19
|
* Initialize with bot token
|
|
19
20
|
* Get token from @BotFather on Telegram
|
|
20
21
|
*/
|
|
21
|
-
async initialize(token) {
|
|
22
|
-
|
|
22
|
+
async initialize(token, customApiUrl = null) {
|
|
23
|
+
const options = { polling: false };
|
|
24
|
+
if (customApiUrl) {
|
|
25
|
+
options.baseApiUrl = customApiUrl;
|
|
26
|
+
}
|
|
27
|
+
this.bot = new TelegramBot(token, options);
|
|
23
28
|
|
|
24
29
|
// Verify the token works
|
|
25
30
|
try {
|
|
@@ -75,30 +80,43 @@ export class TelegramClient {
|
|
|
75
80
|
* Send a file to the storage chat
|
|
76
81
|
* Telegram supports up to 2GB for documents!
|
|
77
82
|
*/
|
|
78
|
-
async sendFile(filePath, caption = '') {
|
|
83
|
+
async sendFile(filePath, caption = '', options = {}) {
|
|
79
84
|
if (!this.chatId) {
|
|
80
85
|
throw new Error('Chat ID not set. Run init first.');
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
if (!fs.existsSync(filePath)) {
|
|
89
|
+
throw new Error(`File not found: ${filePath}`);
|
|
90
|
+
}
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
try {
|
|
93
|
+
let fileStream = fs.createReadStream(filePath);
|
|
94
|
+
const filename = path.basename(filePath);
|
|
95
|
+
|
|
96
|
+
if (options.limitRate) {
|
|
97
|
+
const { Throttle } = await import('../utils/throttle.js');
|
|
98
|
+
fileStream = fileStream.pipe(new Throttle(options.limitRate));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const message = await this.bot.sendDocument(this.chatId, fileStream, {
|
|
102
|
+
caption: caption
|
|
103
|
+
}, {
|
|
104
|
+
filename: filename,
|
|
105
|
+
contentType: 'application/octet-stream'
|
|
106
|
+
});
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
return {
|
|
109
|
+
messageId: message.message_id,
|
|
110
|
+
fileId: message.document.file_id,
|
|
111
|
+
timestamp: message.date
|
|
112
|
+
};
|
|
113
|
+
} catch (err) {
|
|
114
|
+
throw new Error(`Failed to upload to Telegram: ${err.message}`);
|
|
115
|
+
}
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
/**
|
|
101
|
-
* Download a file from Telegram
|
|
119
|
+
* Download a file from Telegram (In-Memory buffer)
|
|
102
120
|
*/
|
|
103
121
|
async downloadFile(fileId) {
|
|
104
122
|
// Get file path from Telegram servers
|
|
@@ -116,6 +134,16 @@ export class TelegramClient {
|
|
|
116
134
|
return Buffer.concat(chunks);
|
|
117
135
|
}
|
|
118
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Efficiently download a file from Telegram straight to disk
|
|
139
|
+
*/
|
|
140
|
+
async downloadFileToPath(fileId, destPath) {
|
|
141
|
+
const fileStream = await this.bot.getFileStream(fileId);
|
|
142
|
+
const writeStream = fs.createWriteStream(destPath);
|
|
143
|
+
await pipeline(fileStream, writeStream);
|
|
144
|
+
return destPath;
|
|
145
|
+
}
|
|
146
|
+
|
|
119
147
|
/**
|
|
120
148
|
* Delete a message (optional cleanup)
|
|
121
149
|
*/
|
package/src/utils/chunker.js
CHANGED
|
@@ -91,7 +91,17 @@ export function createHeader(filename, originalSize, chunkIndex, totalChunks, fl
|
|
|
91
91
|
offset += 2;
|
|
92
92
|
|
|
93
93
|
// Filename length
|
|
94
|
-
|
|
94
|
+
let filenameBytes = Buffer.from(filename, 'utf-8');
|
|
95
|
+
if (filenameBytes.length > 42) {
|
|
96
|
+
// We carefully truncate by chars instead of bytes to avoid splitting a UTF-8 character in half!
|
|
97
|
+
let truncated = filename;
|
|
98
|
+
while (Buffer.from(truncated, 'utf-8').length > 42) {
|
|
99
|
+
// Remove one character at a time from the end
|
|
100
|
+
truncated = truncated.slice(0, -1);
|
|
101
|
+
}
|
|
102
|
+
filenameBytes = Buffer.from(truncated, 'utf-8');
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
header.writeUInt16LE(filenameBytes.length, offset);
|
|
96
106
|
offset += 2;
|
|
97
107
|
|
package/src/utils/compression.js
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'path';
|
|
|
8
8
|
|
|
9
9
|
const gzip = promisify(zlib.gzip);
|
|
10
10
|
const gunzip = promisify(zlib.gunzip);
|
|
11
|
+
import { PassThrough } from 'stream';
|
|
11
12
|
|
|
12
13
|
// File extensions that are already compressed (skip compression for these)
|
|
13
14
|
const SKIP_COMPRESSION = new Set([
|
|
@@ -71,6 +72,24 @@ export class Compressor {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Get a compression transform stream
|
|
77
|
+
* Returns: { stream: Transform, compressed: boolean }
|
|
78
|
+
*/
|
|
79
|
+
getCompressStream(filename = '') {
|
|
80
|
+
if (this.shouldSkip(filename)) {
|
|
81
|
+
return {
|
|
82
|
+
stream: new PassThrough(),
|
|
83
|
+
compressed: false
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
stream: zlib.createGzip({ level: 6 }),
|
|
89
|
+
compressed: true
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
/**
|
|
75
94
|
* Decompress gzip data
|
|
76
95
|
*/
|
|
@@ -81,4 +100,15 @@ export class Compressor {
|
|
|
81
100
|
|
|
82
101
|
return await gunzip(data);
|
|
83
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get a decompression transform stream
|
|
106
|
+
*/
|
|
107
|
+
getDecompressStream(wasCompressed) {
|
|
108
|
+
if (!wasCompressed) {
|
|
109
|
+
return new PassThrough();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return zlib.createGunzip();
|
|
113
|
+
}
|
|
84
114
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
|
|
3
|
+
export class Throttle extends Transform {
|
|
4
|
+
constructor(bytesPerSecond) {
|
|
5
|
+
super();
|
|
6
|
+
this.bytesPerSecond = bytesPerSecond;
|
|
7
|
+
this.passed = 0;
|
|
8
|
+
this.startTime = Date.now();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_transform(chunk, encoding, callback) {
|
|
12
|
+
this.passed += chunk.length;
|
|
13
|
+
const elapsed = Date.now() - this.startTime;
|
|
14
|
+
const expectedTime = (this.passed / this.bytesPerSecond) * 1000;
|
|
15
|
+
|
|
16
|
+
if (expectedTime > elapsed) {
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
this.push(chunk);
|
|
19
|
+
callback();
|
|
20
|
+
}, expectedTime - elapsed);
|
|
21
|
+
} else {
|
|
22
|
+
this.push(chunk);
|
|
23
|
+
callback();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|