@nightowne/tas-cli 1.1.1 → 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 +102 -97
- package/package.json +3 -3
- package/src/cli.js +212 -147
- package/src/crypto/encryption.js +128 -0
- package/src/db/index.js +86 -0
- package/src/fuse/mount.js +104 -40
- package/src/index.js +173 -103
- package/src/share/server.js +441 -0
- package/src/sync/sync.js +54 -35
- package/src/telegram/client.js +45 -17
- package/src/utils/chunker.js +11 -1
- package/src/utils/cli-helpers.js +145 -0
- package/src/utils/compression.js +30 -0
- package/src/utils/throttle.js +26 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Helper utilities
|
|
3
|
+
* Shared logic for commands
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { Encryptor } from '../crypto/encryption.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get password from command-line option, environment variable, or interactive prompt
|
|
14
|
+
* @param {string} passwordOption - Password from --password flag (if provided)
|
|
15
|
+
* @param {boolean} allowCache - Allow caching via TAS_PASSWORD env var
|
|
16
|
+
* @returns {Promise<string>}
|
|
17
|
+
*/
|
|
18
|
+
export async function getPassword(passwordOption, allowCache = true) {
|
|
19
|
+
// Priority: CLI flag > Environment variable > Interactive prompt
|
|
20
|
+
|
|
21
|
+
if (passwordOption) {
|
|
22
|
+
return passwordOption;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (allowCache && process.env.TAS_PASSWORD) {
|
|
26
|
+
return process.env.TAS_PASSWORD;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { password } = await inquirer.prompt([
|
|
30
|
+
{
|
|
31
|
+
type: 'password',
|
|
32
|
+
name: 'password',
|
|
33
|
+
message: 'Enter your encryption password:',
|
|
34
|
+
mask: '*'
|
|
35
|
+
}
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
return password;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Verify password against config
|
|
43
|
+
* @param {string} password - Password to verify
|
|
44
|
+
* @param {Object} config - Config object with passwordHash
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function verifyPassword(password, config) {
|
|
48
|
+
const encryptor = new Encryptor(password);
|
|
49
|
+
return encryptor.getPasswordHash() === config.passwordHash;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate config structure and content
|
|
54
|
+
* @param {Object} config - Config to validate
|
|
55
|
+
* @returns {Object} - { valid: boolean, errors: string[] }
|
|
56
|
+
*/
|
|
57
|
+
export function validateConfig(config) {
|
|
58
|
+
const errors = [];
|
|
59
|
+
|
|
60
|
+
if (!config) {
|
|
61
|
+
errors.push('Config is null or undefined');
|
|
62
|
+
return { valid: false, errors };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!config.botToken || typeof config.botToken !== 'string') {
|
|
66
|
+
errors.push('Missing or invalid botToken');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!config.botToken?.includes(':')) {
|
|
70
|
+
errors.push('Invalid bot token format (should contain :)');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!config.chatId || (typeof config.chatId !== 'number' && typeof config.chatId !== 'string')) {
|
|
74
|
+
errors.push('Missing or invalid chatId');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!config.passwordHash || typeof config.passwordHash !== 'string') {
|
|
78
|
+
errors.push('Missing or invalid passwordHash');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
valid: errors.length === 0,
|
|
83
|
+
errors
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load and validate config
|
|
89
|
+
* @param {string} dataDir - Data directory path
|
|
90
|
+
* @returns {Object|null} - Config object or null if not found
|
|
91
|
+
*/
|
|
92
|
+
export function loadConfig(dataDir) {
|
|
93
|
+
const configPath = path.join(dataDir, 'config.json');
|
|
94
|
+
|
|
95
|
+
if (!fs.existsSync(configPath)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
101
|
+
const validation = validateConfig(config);
|
|
102
|
+
|
|
103
|
+
if (!validation.valid) {
|
|
104
|
+
throw new Error(`Invalid config: ${validation.errors.join(', ')}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return config;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
throw new Error(`Config error: ${err.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Ensure TAS is initialized
|
|
115
|
+
* @param {string} dataDir - Data directory path
|
|
116
|
+
* @returns {Object} - Config object
|
|
117
|
+
*/
|
|
118
|
+
export function requireConfig(dataDir) {
|
|
119
|
+
const config = loadConfig(dataDir);
|
|
120
|
+
|
|
121
|
+
if (!config) {
|
|
122
|
+
console.log(chalk.red('✗ TAS not initialized. Run `tas init` first.'));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return config;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get and verify password with proper error handling
|
|
131
|
+
* @param {string} passwordOption - Password from CLI flag
|
|
132
|
+
* @param {string} dataDir - Data directory path
|
|
133
|
+
* @returns {Promise<string>} - Verified password
|
|
134
|
+
*/
|
|
135
|
+
export async function getAndVerifyPassword(passwordOption, dataDir) {
|
|
136
|
+
const config = requireConfig(dataDir);
|
|
137
|
+
const password = await getPassword(passwordOption);
|
|
138
|
+
|
|
139
|
+
if (!verifyPassword(password, config)) {
|
|
140
|
+
console.log(chalk.red('✗ Incorrect password'));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return password;
|
|
145
|
+
}
|
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
|
+
}
|