@nightowne/tas-cli 1.0.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/LICENSE +21 -0
- package/README.md +174 -0
- package/package.json +57 -0
- package/src/cli.js +1010 -0
- package/src/crypto/encryption.js +116 -0
- package/src/db/index.js +356 -0
- package/src/fuse/mount.js +516 -0
- package/src/index.js +219 -0
- package/src/sync/sync.js +297 -0
- package/src/telegram/client.js +131 -0
- package/src/utils/branding.js +94 -0
- package/src/utils/chunker.js +155 -0
- package/src/utils/compression.js +84 -0
- package/systemd/README.md +32 -0
- package/systemd/tas-sync.service +21 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TAS CLI - Telegram as Storage
|
|
5
|
+
* Main command-line interface
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import inquirer from 'inquirer';
|
|
12
|
+
import { TelegramClient } from './telegram/client.js';
|
|
13
|
+
import { Encryptor } from './crypto/encryption.js';
|
|
14
|
+
import { Compressor } from './utils/compression.js';
|
|
15
|
+
import { FileIndex } from './db/index.js';
|
|
16
|
+
import { processFile, retrieveFile } from './index.js';
|
|
17
|
+
import { printBanner, LOGO, TAGLINE, VERSION } from './utils/branding.js';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const DATA_DIR = path.join(__dirname, '..', 'data');
|
|
24
|
+
|
|
25
|
+
const program = new Command();
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.name('tas')
|
|
29
|
+
.description(chalk.cyan('š¦ TAS') + chalk.dim(' - Telegram as Storage | Free ⢠Encrypted ⢠Unlimited'))
|
|
30
|
+
.version(VERSION)
|
|
31
|
+
.hook('preAction', (thisCommand) => {
|
|
32
|
+
// Show banner for main commands
|
|
33
|
+
if (['init', 'status'].includes(thisCommand.args[0])) {
|
|
34
|
+
printBanner();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ============== INIT COMMAND ==============
|
|
39
|
+
program
|
|
40
|
+
.command('init')
|
|
41
|
+
.description('Initialize TAS and connect to Telegram')
|
|
42
|
+
.action(async () => {
|
|
43
|
+
console.log(chalk.cyan('\nš Initializing Telegram as Storage...\n'));
|
|
44
|
+
|
|
45
|
+
// Ensure data directory exists
|
|
46
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
47
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get bot token
|
|
51
|
+
console.log(chalk.yellow('š± First, create a Telegram bot:'));
|
|
52
|
+
console.log(chalk.dim(' 1. Open Telegram and message @BotFather'));
|
|
53
|
+
console.log(chalk.dim(' 2. Send /newbot and follow the prompts'));
|
|
54
|
+
console.log(chalk.dim(' 3. Copy the bot token\n'));
|
|
55
|
+
|
|
56
|
+
const { token } = await inquirer.prompt([
|
|
57
|
+
{
|
|
58
|
+
type: 'password',
|
|
59
|
+
name: 'token',
|
|
60
|
+
message: 'Enter your Telegram bot token:',
|
|
61
|
+
mask: '*',
|
|
62
|
+
validate: (input) => input.includes(':') || 'Invalid token format (should contain :)'
|
|
63
|
+
}
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Get encryption password
|
|
67
|
+
const { password } = await inquirer.prompt([
|
|
68
|
+
{
|
|
69
|
+
type: 'password',
|
|
70
|
+
name: 'password',
|
|
71
|
+
message: 'Set your encryption password (used for all files):',
|
|
72
|
+
mask: '*',
|
|
73
|
+
validate: (input) => input.length >= 8 || 'Password must be at least 8 characters'
|
|
74
|
+
}
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const { confirmPassword } = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: 'password',
|
|
80
|
+
name: 'confirmPassword',
|
|
81
|
+
message: 'Confirm password:',
|
|
82
|
+
mask: '*',
|
|
83
|
+
validate: (input) => input === password || 'Passwords do not match'
|
|
84
|
+
}
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// Initialize encryption
|
|
88
|
+
const encryptor = new Encryptor(password);
|
|
89
|
+
|
|
90
|
+
// Initialize Telegram
|
|
91
|
+
const spinner = ora('Connecting to Telegram...').start();
|
|
92
|
+
const client = new TelegramClient(DATA_DIR);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const botInfo = await client.initialize(token);
|
|
96
|
+
spinner.succeed(`Connected as @${botInfo.username}`);
|
|
97
|
+
|
|
98
|
+
// Wait for user to message the bot
|
|
99
|
+
console.log(chalk.yellow(`\nš© Now message your bot @${botInfo.username} on Telegram`));
|
|
100
|
+
console.log(chalk.dim(' (Just send any message to link your account)\n'));
|
|
101
|
+
|
|
102
|
+
spinner.start('Waiting for your message...');
|
|
103
|
+
const userInfo = await client.waitForChatId(120000);
|
|
104
|
+
spinner.succeed(`Linked to ${userInfo.firstName} (@${userInfo.username})`);
|
|
105
|
+
|
|
106
|
+
// Save config
|
|
107
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
108
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
109
|
+
botToken: token,
|
|
110
|
+
chatId: userInfo.chatId,
|
|
111
|
+
passwordHash: encryptor.getPasswordHash(),
|
|
112
|
+
username: userInfo.username,
|
|
113
|
+
createdAt: new Date().toISOString()
|
|
114
|
+
}, null, 2));
|
|
115
|
+
|
|
116
|
+
// Initialize database
|
|
117
|
+
spinner.start('Initializing local index...');
|
|
118
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
119
|
+
db.init();
|
|
120
|
+
spinner.succeed('Local index ready');
|
|
121
|
+
|
|
122
|
+
// Send welcome message
|
|
123
|
+
await client.bot.sendMessage(userInfo.chatId,
|
|
124
|
+
'š¦ *TAS - Telegram as Storage*\n\n' +
|
|
125
|
+
'ā
Setup complete! This chat will store your encrypted files.\n\n' +
|
|
126
|
+
'_Do not delete messages in this chat._',
|
|
127
|
+
{ parse_mode: 'Markdown' }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
console.log(chalk.cyan('\nš TAS is ready! Use `tas push <file>` to upload files.\n'));
|
|
131
|
+
|
|
132
|
+
} catch (err) {
|
|
133
|
+
spinner.fail(`Telegram initialization failed: ${err.message}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ============== PUSH COMMAND ==============
|
|
139
|
+
program
|
|
140
|
+
.command('push <file>')
|
|
141
|
+
.description('Upload a file to Telegram storage')
|
|
142
|
+
.option('-n, --name <name>', 'Custom name for the file')
|
|
143
|
+
.action(async (file, options) => {
|
|
144
|
+
const spinner = ora('Preparing...').start();
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Check file exists
|
|
148
|
+
if (!fs.existsSync(file)) {
|
|
149
|
+
spinner.fail(`File not found: ${file}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Load config
|
|
154
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
155
|
+
if (!fs.existsSync(configPath)) {
|
|
156
|
+
spinner.fail('TAS not initialized. Run `tas init` first.');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
160
|
+
|
|
161
|
+
spinner.stop();
|
|
162
|
+
|
|
163
|
+
// Get password
|
|
164
|
+
const { password } = await inquirer.prompt([
|
|
165
|
+
{
|
|
166
|
+
type: 'password',
|
|
167
|
+
name: 'password',
|
|
168
|
+
message: 'Enter your encryption password:',
|
|
169
|
+
mask: '*'
|
|
170
|
+
}
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
// Verify password
|
|
174
|
+
const encryptor = new Encryptor(password);
|
|
175
|
+
if (encryptor.getPasswordHash() !== config.passwordHash) {
|
|
176
|
+
console.log(chalk.red('ā Incorrect password'));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
spinner.start('Processing file...');
|
|
181
|
+
|
|
182
|
+
// Process and upload
|
|
183
|
+
const result = await processFile(file, {
|
|
184
|
+
password,
|
|
185
|
+
dataDir: DATA_DIR,
|
|
186
|
+
customName: options.name,
|
|
187
|
+
config,
|
|
188
|
+
onProgress: (msg) => { spinner.text = msg; }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
spinner.succeed(`Uploaded: ${chalk.green(result.filename)}`);
|
|
192
|
+
console.log(chalk.dim(` Hash: ${result.hash}`));
|
|
193
|
+
console.log(chalk.dim(` Size: ${formatBytes(result.originalSize)} ā ${formatBytes(result.storedSize)}`));
|
|
194
|
+
console.log(chalk.dim(` Chunks: ${result.chunks}`));
|
|
195
|
+
|
|
196
|
+
} catch (err) {
|
|
197
|
+
spinner.fail(`Upload failed: ${err.message}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ============== PULL COMMAND ==============
|
|
203
|
+
program
|
|
204
|
+
.command('pull <identifier>')
|
|
205
|
+
.description('Download a file from Telegram storage (by filename or hash)')
|
|
206
|
+
.option('-o, --output <path>', 'Output path for the file')
|
|
207
|
+
.action(async (identifier, options) => {
|
|
208
|
+
const spinner = ora('Looking up file...').start();
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// Load config
|
|
212
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
213
|
+
if (!fs.existsSync(configPath)) {
|
|
214
|
+
spinner.fail('TAS not initialized. Run `tas init` first.');
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
218
|
+
|
|
219
|
+
// Find file in index
|
|
220
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
221
|
+
db.init();
|
|
222
|
+
|
|
223
|
+
let fileRecord = db.findByHash(identifier) || db.findByName(identifier);
|
|
224
|
+
if (!fileRecord) {
|
|
225
|
+
spinner.fail(`File not found: ${identifier}`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
spinner.stop();
|
|
230
|
+
|
|
231
|
+
// Get password
|
|
232
|
+
const { password } = await inquirer.prompt([
|
|
233
|
+
{
|
|
234
|
+
type: 'password',
|
|
235
|
+
name: 'password',
|
|
236
|
+
message: 'Enter your encryption password:',
|
|
237
|
+
mask: '*'
|
|
238
|
+
}
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// Verify password
|
|
242
|
+
const encryptor = new Encryptor(password);
|
|
243
|
+
if (encryptor.getPasswordHash() !== config.passwordHash) {
|
|
244
|
+
console.log(chalk.red('ā Incorrect password'));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
spinner.start('Downloading...');
|
|
249
|
+
|
|
250
|
+
const outputPath = options.output || fileRecord.filename;
|
|
251
|
+
await retrieveFile(fileRecord, {
|
|
252
|
+
password,
|
|
253
|
+
dataDir: DATA_DIR,
|
|
254
|
+
outputPath,
|
|
255
|
+
config,
|
|
256
|
+
onProgress: (msg) => { spinner.text = msg; }
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
spinner.succeed(`Downloaded: ${chalk.green(outputPath)}`);
|
|
260
|
+
|
|
261
|
+
} catch (err) {
|
|
262
|
+
spinner.fail(`Download failed: ${err.message}`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ============== LIST COMMAND ==============
|
|
268
|
+
program
|
|
269
|
+
.command('list')
|
|
270
|
+
.alias('ls')
|
|
271
|
+
.description('List all stored files')
|
|
272
|
+
.option('-l, --long', 'Show detailed information')
|
|
273
|
+
.action(async (options) => {
|
|
274
|
+
try {
|
|
275
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
276
|
+
db.init();
|
|
277
|
+
|
|
278
|
+
const files = db.listAll();
|
|
279
|
+
|
|
280
|
+
if (files.length === 0) {
|
|
281
|
+
console.log(chalk.yellow('\nš No files stored yet. Use `tas push <file>` to upload.\n'));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log(chalk.cyan(`\nš¦ Stored Files (${files.length})\n`));
|
|
286
|
+
|
|
287
|
+
if (options.long) {
|
|
288
|
+
console.log(chalk.dim('HASH'.padEnd(16) + 'SIZE'.padEnd(12) + 'CHUNKS'.padEnd(8) + 'DATE'.padEnd(12) + 'FILENAME'));
|
|
289
|
+
console.log(chalk.dim('ā'.repeat(70)));
|
|
290
|
+
|
|
291
|
+
for (const file of files) {
|
|
292
|
+
const hash = file.hash.substring(0, 12) + '...';
|
|
293
|
+
const size = formatBytes(file.original_size).padEnd(12);
|
|
294
|
+
const chunks = String(file.chunks).padEnd(8);
|
|
295
|
+
const date = new Date(file.created_at).toLocaleDateString().padEnd(12);
|
|
296
|
+
console.log(`${chalk.dim(hash.padEnd(16))}${size}${chunks}${date}${chalk.white(file.filename)}`);
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
for (const file of files) {
|
|
300
|
+
console.log(` ${chalk.blue('ā')} ${file.filename} ${chalk.dim(`(${formatBytes(file.original_size)})`)}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log();
|
|
305
|
+
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error(chalk.red('Error listing files:'), err.message);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ============== DELETE COMMAND ==============
|
|
313
|
+
program
|
|
314
|
+
.command('delete <identifier>')
|
|
315
|
+
.alias('rm')
|
|
316
|
+
.description('Remove a file from the index (optionally from Telegram too)')
|
|
317
|
+
.option('--hard', 'Also delete from Telegram')
|
|
318
|
+
.action(async (identifier, options) => {
|
|
319
|
+
try {
|
|
320
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
321
|
+
db.init();
|
|
322
|
+
|
|
323
|
+
let fileRecord = db.findByHash(identifier) || db.findByName(identifier);
|
|
324
|
+
if (!fileRecord) {
|
|
325
|
+
console.log(chalk.red(`ā File not found: ${identifier}`));
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const { confirm } = await inquirer.prompt([
|
|
330
|
+
{
|
|
331
|
+
type: 'confirm',
|
|
332
|
+
name: 'confirm',
|
|
333
|
+
message: `Delete "${fileRecord.filename}" from index${options.hard ? ' and Telegram' : ''}?`,
|
|
334
|
+
default: false
|
|
335
|
+
}
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
if (confirm) {
|
|
339
|
+
// If hard delete, also remove from Telegram
|
|
340
|
+
if (options.hard) {
|
|
341
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
342
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
343
|
+
|
|
344
|
+
const client = new TelegramClient(DATA_DIR);
|
|
345
|
+
await client.initialize(config.botToken);
|
|
346
|
+
client.setChatId(config.chatId);
|
|
347
|
+
|
|
348
|
+
const chunks = db.getChunks(fileRecord.id);
|
|
349
|
+
for (const chunk of chunks) {
|
|
350
|
+
await client.deleteMessage(chunk.message_id);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
db.delete(fileRecord.id);
|
|
355
|
+
console.log(chalk.green(`ā Removed "${fileRecord.filename}"`));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error(chalk.red('Error deleting file:'), err.message);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ============== STATUS COMMAND ==============
|
|
365
|
+
program
|
|
366
|
+
.command('status')
|
|
367
|
+
.description('Show TAS status and statistics')
|
|
368
|
+
.action(async () => {
|
|
369
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
370
|
+
|
|
371
|
+
if (!fs.existsSync(configPath)) {
|
|
372
|
+
console.log(chalk.yellow('\nā ļø TAS not initialized. Run `tas init` first.\n'));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
377
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
378
|
+
db.init();
|
|
379
|
+
|
|
380
|
+
const files = db.listAll();
|
|
381
|
+
const totalSize = files.reduce((acc, f) => acc + f.original_size, 0);
|
|
382
|
+
const storedSize = files.reduce((acc, f) => acc + f.stored_size, 0);
|
|
383
|
+
const savings = totalSize > 0 ? Math.round((1 - storedSize / totalSize) * 100) : 0;
|
|
384
|
+
|
|
385
|
+
console.log(chalk.cyan('\nš TAS Status\n'));
|
|
386
|
+
console.log(` Initialized: ${chalk.white(new Date(config.createdAt).toLocaleDateString())}`);
|
|
387
|
+
console.log(` Telegram user: ${chalk.white('@' + (config.username || 'unknown'))}`);
|
|
388
|
+
console.log(` Files stored: ${chalk.white(files.length)}`);
|
|
389
|
+
console.log(` Total size: ${chalk.white(formatBytes(totalSize))}`);
|
|
390
|
+
console.log(` Compressed: ${chalk.white(formatBytes(storedSize))} ${chalk.dim(`(${savings}% saved)`)}`);
|
|
391
|
+
console.log();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ============== MOUNT COMMAND ==============
|
|
395
|
+
program
|
|
396
|
+
.command('mount <mountpoint>')
|
|
397
|
+
.description('š„ Mount Telegram storage as a local folder (FUSE)')
|
|
398
|
+
.action(async (mountpoint) => {
|
|
399
|
+
console.log(chalk.cyan('\nšļø Mounting Telegram as filesystem...\n'));
|
|
400
|
+
|
|
401
|
+
// Load config
|
|
402
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
403
|
+
if (!fs.existsSync(configPath)) {
|
|
404
|
+
console.log(chalk.red('ā TAS not initialized. Run `tas init` first.'));
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
408
|
+
|
|
409
|
+
// Get password
|
|
410
|
+
const { password } = await inquirer.prompt([
|
|
411
|
+
{
|
|
412
|
+
type: 'password',
|
|
413
|
+
name: 'password',
|
|
414
|
+
message: 'Enter your encryption password:',
|
|
415
|
+
mask: '*'
|
|
416
|
+
}
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
// Verify password
|
|
420
|
+
const encryptor = new Encryptor(password);
|
|
421
|
+
if (encryptor.getPasswordHash() !== config.passwordHash) {
|
|
422
|
+
console.log(chalk.red('ā Incorrect password'));
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const spinner = ora('Initializing filesystem...').start();
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
// Resolve mount point to absolute path
|
|
430
|
+
const absMount = path.resolve(mountpoint);
|
|
431
|
+
|
|
432
|
+
// Dynamic import to avoid loading fuse-native if not needed
|
|
433
|
+
const { TelegramFS } = await import('./fuse/mount.js');
|
|
434
|
+
|
|
435
|
+
const tfs = new TelegramFS({
|
|
436
|
+
dataDir: DATA_DIR,
|
|
437
|
+
password,
|
|
438
|
+
config,
|
|
439
|
+
mountPoint: absMount
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
await tfs.initialize();
|
|
443
|
+
await tfs.mount();
|
|
444
|
+
|
|
445
|
+
spinner.succeed(`Mounted at ${chalk.green(absMount)}`);
|
|
446
|
+
|
|
447
|
+
console.log(chalk.cyan('\nš Telegram storage is now a folder!\n'));
|
|
448
|
+
console.log(chalk.dim(' Commands you can use:'));
|
|
449
|
+
console.log(chalk.dim(` ls ${absMount} # List files`));
|
|
450
|
+
console.log(chalk.dim(` cp file.pdf ${absMount}/ # Upload`));
|
|
451
|
+
console.log(chalk.dim(` cat ${absMount}/file.txt # Read`));
|
|
452
|
+
console.log(chalk.dim(` rm ${absMount}/file.pdf # Delete`));
|
|
453
|
+
console.log();
|
|
454
|
+
console.log(chalk.yellow('Press Ctrl+C to unmount'));
|
|
455
|
+
|
|
456
|
+
// Handle graceful shutdown
|
|
457
|
+
const cleanup = async () => {
|
|
458
|
+
console.log(chalk.dim('\n\nUnmounting...'));
|
|
459
|
+
await tfs.unmount();
|
|
460
|
+
console.log(chalk.green('ā Unmounted successfully'));
|
|
461
|
+
process.exit(0);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
process.on('SIGINT', cleanup);
|
|
465
|
+
process.on('SIGTERM', cleanup);
|
|
466
|
+
|
|
467
|
+
// Keep process running
|
|
468
|
+
await new Promise(() => { });
|
|
469
|
+
|
|
470
|
+
} catch (err) {
|
|
471
|
+
spinner.fail(`Mount failed: ${err.message}`);
|
|
472
|
+
console.log(chalk.dim('\nNote: FUSE requires libfuse to be installed:'));
|
|
473
|
+
console.log(chalk.dim(' Ubuntu/Debian: sudo apt install fuse libfuse-dev'));
|
|
474
|
+
console.log(chalk.dim(' Fedora: sudo dnf install fuse fuse-devel'));
|
|
475
|
+
console.log(chalk.dim(' macOS: brew install macfuse\n'));
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// ============== UNMOUNT COMMAND ==============
|
|
481
|
+
program
|
|
482
|
+
.command('unmount <mountpoint>')
|
|
483
|
+
.alias('umount')
|
|
484
|
+
.description('Unmount a previously mounted Telegram folder')
|
|
485
|
+
.action(async (mountpoint) => {
|
|
486
|
+
const absMount = path.resolve(mountpoint);
|
|
487
|
+
|
|
488
|
+
const spinner = ora('Unmounting...').start();
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const { execSync } = await import('child_process');
|
|
492
|
+
|
|
493
|
+
// Use fusermount on Linux, umount on macOS
|
|
494
|
+
const isMac = process.platform === 'darwin';
|
|
495
|
+
const cmd = isMac ? `umount "${absMount}"` : `fusermount -u "${absMount}"`;
|
|
496
|
+
|
|
497
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
498
|
+
|
|
499
|
+
spinner.succeed(`Unmounted ${chalk.green(absMount)}`);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
spinner.fail(`Unmount failed: ${err.message}`);
|
|
502
|
+
console.log(chalk.dim('\nTry: fusermount -u ' + absMount));
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// ============== TAG COMMAND ==============
|
|
508
|
+
const tagCmd = program
|
|
509
|
+
.command('tag')
|
|
510
|
+
.description('Manage file tags');
|
|
511
|
+
|
|
512
|
+
tagCmd
|
|
513
|
+
.command('add <file> <tags...>')
|
|
514
|
+
.description('Add tags to a file')
|
|
515
|
+
.action(async (file, tags) => {
|
|
516
|
+
try {
|
|
517
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
518
|
+
db.init();
|
|
519
|
+
|
|
520
|
+
const fileRecord = db.findByHash(file) || db.findByName(file);
|
|
521
|
+
if (!fileRecord) {
|
|
522
|
+
console.log(chalk.red(`ā File not found: ${file}`));
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
for (const tag of tags) {
|
|
527
|
+
db.addTag(fileRecord.id, tag);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const allTags = db.getFileTags(fileRecord.id);
|
|
531
|
+
console.log(chalk.green(`ā Tags updated for "${fileRecord.filename}"`));
|
|
532
|
+
console.log(chalk.dim(` Tags: ${allTags.join(', ')}`));
|
|
533
|
+
|
|
534
|
+
db.close();
|
|
535
|
+
} catch (err) {
|
|
536
|
+
console.error(chalk.red('Error:'), err.message);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
tagCmd
|
|
542
|
+
.command('remove <file> <tags...>')
|
|
543
|
+
.description('Remove tags from a file')
|
|
544
|
+
.action(async (file, tags) => {
|
|
545
|
+
try {
|
|
546
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
547
|
+
db.init();
|
|
548
|
+
|
|
549
|
+
const fileRecord = db.findByHash(file) || db.findByName(file);
|
|
550
|
+
if (!fileRecord) {
|
|
551
|
+
console.log(chalk.red(`ā File not found: ${file}`));
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
for (const tag of tags) {
|
|
556
|
+
db.removeTag(fileRecord.id, tag);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const allTags = db.getFileTags(fileRecord.id);
|
|
560
|
+
console.log(chalk.green(`ā Tags updated for "${fileRecord.filename}"`));
|
|
561
|
+
console.log(chalk.dim(` Tags: ${allTags.length > 0 ? allTags.join(', ') : '(none)'}`));
|
|
562
|
+
|
|
563
|
+
db.close();
|
|
564
|
+
} catch (err) {
|
|
565
|
+
console.error(chalk.red('Error:'), err.message);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
tagCmd
|
|
571
|
+
.command('list [tag]')
|
|
572
|
+
.description('List all tags, or files with a specific tag')
|
|
573
|
+
.action(async (tag) => {
|
|
574
|
+
try {
|
|
575
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
576
|
+
db.init();
|
|
577
|
+
|
|
578
|
+
if (tag) {
|
|
579
|
+
// List files with this tag
|
|
580
|
+
const files = db.findByTag(tag);
|
|
581
|
+
if (files.length === 0) {
|
|
582
|
+
console.log(chalk.yellow(`\nš No files with tag "${tag}"\n`));
|
|
583
|
+
} else {
|
|
584
|
+
console.log(chalk.cyan(`\nš·ļø Files tagged "${tag}" (${files.length})\n`));
|
|
585
|
+
for (const file of files) {
|
|
586
|
+
console.log(` ${chalk.blue('ā')} ${file.filename} ${chalk.dim(`(${formatBytes(file.original_size)})`)}`);
|
|
587
|
+
}
|
|
588
|
+
console.log();
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
// List all tags
|
|
592
|
+
const tags = db.getAllTags();
|
|
593
|
+
if (tags.length === 0) {
|
|
594
|
+
console.log(chalk.yellow('\nš No tags created yet. Use `tas tag add <file> <tag>` to add tags.\n'));
|
|
595
|
+
} else {
|
|
596
|
+
console.log(chalk.cyan(`\nš·ļø All Tags (${tags.length})\n`));
|
|
597
|
+
for (const t of tags) {
|
|
598
|
+
console.log(` ${chalk.blue('ā')} ${t.tag} ${chalk.dim(`(${t.count} file${t.count > 1 ? 's' : ''})`)}`);
|
|
599
|
+
}
|
|
600
|
+
console.log();
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
db.close();
|
|
605
|
+
} catch (err) {
|
|
606
|
+
console.error(chalk.red('Error:'), err.message);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// ============== SYNC COMMAND ==============
|
|
612
|
+
const syncCmd = program
|
|
613
|
+
.command('sync')
|
|
614
|
+
.description('Folder sync (Dropbox-like auto-sync)');
|
|
615
|
+
|
|
616
|
+
syncCmd
|
|
617
|
+
.command('add <folder>')
|
|
618
|
+
.description('Register a folder for sync')
|
|
619
|
+
.action(async (folder) => {
|
|
620
|
+
try {
|
|
621
|
+
const absPath = path.resolve(folder);
|
|
622
|
+
|
|
623
|
+
if (!fs.existsSync(absPath)) {
|
|
624
|
+
console.log(chalk.red(`ā Folder not found: ${absPath}`));
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!fs.statSync(absPath).isDirectory()) {
|
|
629
|
+
console.log(chalk.red(`ā Not a directory: ${absPath}`));
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
634
|
+
db.init();
|
|
635
|
+
|
|
636
|
+
db.addSyncFolder(absPath);
|
|
637
|
+
console.log(chalk.green(`ā Added sync folder: ${absPath}`));
|
|
638
|
+
console.log(chalk.dim(' Use `tas sync start` to begin syncing'));
|
|
639
|
+
|
|
640
|
+
db.close();
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.error(chalk.red('Error:'), err.message);
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
syncCmd
|
|
648
|
+
.command('remove <folder>')
|
|
649
|
+
.description('Remove a folder from sync')
|
|
650
|
+
.action(async (folder) => {
|
|
651
|
+
try {
|
|
652
|
+
const absPath = path.resolve(folder);
|
|
653
|
+
|
|
654
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
655
|
+
db.init();
|
|
656
|
+
|
|
657
|
+
db.removeSyncFolder(absPath);
|
|
658
|
+
console.log(chalk.green(`ā Removed sync folder: ${absPath}`));
|
|
659
|
+
|
|
660
|
+
db.close();
|
|
661
|
+
} catch (err) {
|
|
662
|
+
console.error(chalk.red('Error:'), err.message);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
syncCmd
|
|
668
|
+
.command('status')
|
|
669
|
+
.description('Show sync status')
|
|
670
|
+
.action(async () => {
|
|
671
|
+
try {
|
|
672
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
673
|
+
db.init();
|
|
674
|
+
|
|
675
|
+
const folders = db.getSyncFolders();
|
|
676
|
+
|
|
677
|
+
if (folders.length === 0) {
|
|
678
|
+
console.log(chalk.yellow('\nš No folders registered for sync.'));
|
|
679
|
+
console.log(chalk.dim(' Use `tas sync add <folder>` to add a folder.\n'));
|
|
680
|
+
} else {
|
|
681
|
+
console.log(chalk.cyan(`\nš Sync Folders (${folders.length})\n`));
|
|
682
|
+
for (const folder of folders) {
|
|
683
|
+
const states = db.getFolderSyncStates(folder.id);
|
|
684
|
+
const status = folder.enabled ? chalk.green('enabled') : chalk.dim('disabled');
|
|
685
|
+
console.log(` ${chalk.blue('ā')} ${folder.local_path}`);
|
|
686
|
+
console.log(chalk.dim(` Status: ${status} | Files tracked: ${states.length}`));
|
|
687
|
+
}
|
|
688
|
+
console.log();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
db.close();
|
|
692
|
+
} catch (err) {
|
|
693
|
+
console.error(chalk.red('Error:'), err.message);
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
syncCmd
|
|
699
|
+
.command('start')
|
|
700
|
+
.description('Start syncing all registered folders')
|
|
701
|
+
.action(async () => {
|
|
702
|
+
console.log(chalk.cyan('\nš Starting folder sync...\n'));
|
|
703
|
+
|
|
704
|
+
// Load config
|
|
705
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
706
|
+
if (!fs.existsSync(configPath)) {
|
|
707
|
+
console.log(chalk.red('ā TAS not initialized. Run `tas init` first.'));
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
711
|
+
|
|
712
|
+
// Get password
|
|
713
|
+
const { password } = await inquirer.prompt([
|
|
714
|
+
{
|
|
715
|
+
type: 'password',
|
|
716
|
+
name: 'password',
|
|
717
|
+
message: 'Enter your encryption password:',
|
|
718
|
+
mask: '*'
|
|
719
|
+
}
|
|
720
|
+
]);
|
|
721
|
+
|
|
722
|
+
// Verify password
|
|
723
|
+
const encryptor = new Encryptor(password);
|
|
724
|
+
if (encryptor.getPasswordHash() !== config.passwordHash) {
|
|
725
|
+
console.log(chalk.red('ā Incorrect password'));
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
const { SyncEngine } = await import('./sync/sync.js');
|
|
731
|
+
|
|
732
|
+
const syncEngine = new SyncEngine({
|
|
733
|
+
dataDir: DATA_DIR,
|
|
734
|
+
password,
|
|
735
|
+
config
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
await syncEngine.initialize();
|
|
739
|
+
|
|
740
|
+
// Set up event handlers
|
|
741
|
+
syncEngine.on('sync-start', ({ folder }) => {
|
|
742
|
+
console.log(chalk.blue(`š Scanning: ${folder}`));
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
syncEngine.on('sync-complete', ({ folder, uploaded, skipped }) => {
|
|
746
|
+
console.log(chalk.green(`ā Synced: ${uploaded} uploaded, ${skipped} unchanged`));
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
syncEngine.on('file-upload-start', ({ file }) => {
|
|
750
|
+
console.log(chalk.dim(` ā Uploading: ${file}`));
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
syncEngine.on('file-upload-complete', ({ file }) => {
|
|
754
|
+
console.log(chalk.green(` ā Uploaded: ${file}`));
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
syncEngine.on('file-upload-error', ({ file, error }) => {
|
|
758
|
+
console.log(chalk.red(` ā Failed: ${file} - ${error}`));
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
syncEngine.on('watch-start', ({ folder }) => {
|
|
762
|
+
console.log(chalk.cyan(`šļø Watching: ${folder}`));
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Start syncing
|
|
766
|
+
await syncEngine.start();
|
|
767
|
+
|
|
768
|
+
console.log(chalk.cyan('\n⨠Sync active! Watching for changes...'));
|
|
769
|
+
console.log(chalk.yellow('Press Ctrl+C to stop\n'));
|
|
770
|
+
|
|
771
|
+
// Handle graceful shutdown
|
|
772
|
+
const cleanup = () => {
|
|
773
|
+
console.log(chalk.dim('\n\nStopping sync...'));
|
|
774
|
+
syncEngine.stop();
|
|
775
|
+
console.log(chalk.green('ā Sync stopped'));
|
|
776
|
+
process.exit(0);
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
process.on('SIGINT', cleanup);
|
|
780
|
+
process.on('SIGTERM', cleanup);
|
|
781
|
+
|
|
782
|
+
// Keep process running
|
|
783
|
+
await new Promise(() => { });
|
|
784
|
+
|
|
785
|
+
} catch (err) {
|
|
786
|
+
console.error(chalk.red('Sync failed:'), err.message);
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
syncCmd
|
|
792
|
+
.command('pull')
|
|
793
|
+
.description('Download all Telegram files to sync folders (two-way sync)')
|
|
794
|
+
.action(async () => {
|
|
795
|
+
console.log(chalk.cyan('\nš„ Pulling files from Telegram...\n'));
|
|
796
|
+
|
|
797
|
+
// Load config
|
|
798
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
799
|
+
if (!fs.existsSync(configPath)) {
|
|
800
|
+
console.log(chalk.red('ā TAS not initialized. Run `tas init` first.'));
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
804
|
+
|
|
805
|
+
// Get password
|
|
806
|
+
const { password } = await inquirer.prompt([
|
|
807
|
+
{
|
|
808
|
+
type: 'password',
|
|
809
|
+
name: 'password',
|
|
810
|
+
message: 'Enter your encryption password:',
|
|
811
|
+
mask: '*'
|
|
812
|
+
}
|
|
813
|
+
]);
|
|
814
|
+
|
|
815
|
+
// Verify password
|
|
816
|
+
const encryptor = new Encryptor(password);
|
|
817
|
+
if (encryptor.getPasswordHash() !== config.passwordHash) {
|
|
818
|
+
console.log(chalk.red('ā Incorrect password'));
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const spinner = ora('Loading...').start();
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
826
|
+
db.init();
|
|
827
|
+
|
|
828
|
+
const folders = db.getSyncFolders();
|
|
829
|
+
if (folders.length === 0) {
|
|
830
|
+
spinner.warn('No sync folders registered. Use `tas sync add <folder>` first.');
|
|
831
|
+
process.exit(0);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Get all files from Telegram index
|
|
835
|
+
const files = db.listAll();
|
|
836
|
+
if (files.length === 0) {
|
|
837
|
+
spinner.info('No files in Telegram storage.');
|
|
838
|
+
process.exit(0);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
spinner.succeed(`Found ${files.length} files in Telegram`);
|
|
842
|
+
|
|
843
|
+
// Download each file that matches a sync folder
|
|
844
|
+
let downloaded = 0;
|
|
845
|
+
let skipped = 0;
|
|
846
|
+
|
|
847
|
+
for (const file of files) {
|
|
848
|
+
// Check if file belongs to any sync folder (by name prefix)
|
|
849
|
+
for (const folder of folders) {
|
|
850
|
+
const folderName = path.basename(folder.local_path);
|
|
851
|
+
const targetPath = path.join(folder.local_path, file.filename);
|
|
852
|
+
|
|
853
|
+
// Check if file already exists locally with same hash
|
|
854
|
+
if (fs.existsSync(targetPath)) {
|
|
855
|
+
skipped++;
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Ensure directory exists
|
|
860
|
+
const targetDir = path.dirname(targetPath);
|
|
861
|
+
if (!fs.existsSync(targetDir)) {
|
|
862
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
console.log(chalk.dim(` ā Downloading: ${file.filename}`));
|
|
866
|
+
|
|
867
|
+
try {
|
|
868
|
+
await retrieveFile(file, {
|
|
869
|
+
password,
|
|
870
|
+
dataDir: DATA_DIR,
|
|
871
|
+
outputPath: targetPath,
|
|
872
|
+
config,
|
|
873
|
+
onProgress: () => { }
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// Update sync state
|
|
877
|
+
const { hashFile } = await import('./crypto/encryption.js');
|
|
878
|
+
const hash = await hashFile(targetPath);
|
|
879
|
+
const stats = fs.statSync(targetPath);
|
|
880
|
+
db.updateSyncState(folder.id, file.filename, hash, stats.mtimeMs);
|
|
881
|
+
|
|
882
|
+
console.log(chalk.green(` ā Downloaded: ${file.filename}`));
|
|
883
|
+
downloaded++;
|
|
884
|
+
} catch (err) {
|
|
885
|
+
console.log(chalk.red(` ā Failed: ${file.filename} - ${err.message}`));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
break; // Only download to first matching folder
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
console.log(chalk.green(`\nā Pull complete: ${downloaded} downloaded, ${skipped} skipped\n`));
|
|
893
|
+
|
|
894
|
+
db.close();
|
|
895
|
+
} catch (err) {
|
|
896
|
+
spinner.fail(`Pull failed: ${err.message}`);
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// ============== VERIFY COMMAND ==============
|
|
902
|
+
program
|
|
903
|
+
.command('verify')
|
|
904
|
+
.description('Verify file integrity and check for missing Telegram messages')
|
|
905
|
+
.action(async () => {
|
|
906
|
+
console.log(chalk.cyan('\nš Verifying file integrity...\n'));
|
|
907
|
+
|
|
908
|
+
// Load config
|
|
909
|
+
const configPath = path.join(DATA_DIR, 'config.json');
|
|
910
|
+
if (!fs.existsSync(configPath)) {
|
|
911
|
+
console.log(chalk.red('ā TAS not initialized. Run `tas init` first.'));
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
915
|
+
|
|
916
|
+
const spinner = ora('Checking files...').start();
|
|
917
|
+
|
|
918
|
+
try {
|
|
919
|
+
const db = new FileIndex(path.join(DATA_DIR, 'index.db'));
|
|
920
|
+
db.init();
|
|
921
|
+
|
|
922
|
+
const files = db.listAll();
|
|
923
|
+
if (files.length === 0) {
|
|
924
|
+
spinner.info('No files in storage.');
|
|
925
|
+
process.exit(0);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
spinner.text = 'Connecting to Telegram...';
|
|
929
|
+
|
|
930
|
+
const client = new TelegramClient(DATA_DIR);
|
|
931
|
+
await client.initialize(config.botToken);
|
|
932
|
+
client.setChatId(config.chatId);
|
|
933
|
+
|
|
934
|
+
spinner.succeed(`Checking ${files.length} files...`);
|
|
935
|
+
|
|
936
|
+
let valid = 0;
|
|
937
|
+
let missing = 0;
|
|
938
|
+
let errors = [];
|
|
939
|
+
|
|
940
|
+
for (const file of files) {
|
|
941
|
+
const chunks = db.getChunks(file.id);
|
|
942
|
+
let fileValid = true;
|
|
943
|
+
|
|
944
|
+
for (const chunk of chunks) {
|
|
945
|
+
try {
|
|
946
|
+
// Try to get file info from Telegram
|
|
947
|
+
if (!chunk.file_telegram_id) {
|
|
948
|
+
fileValid = false;
|
|
949
|
+
errors.push({ file: file.filename, error: 'Missing Telegram file ID' });
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Check if file is accessible (will throw if deleted)
|
|
954
|
+
await client.bot.getFile(chunk.file_telegram_id);
|
|
955
|
+
} catch (err) {
|
|
956
|
+
fileValid = false;
|
|
957
|
+
errors.push({
|
|
958
|
+
file: file.filename,
|
|
959
|
+
chunk: chunk.chunk_index,
|
|
960
|
+
error: err.message.includes('file') ? 'File deleted from Telegram' : err.message
|
|
961
|
+
});
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (fileValid) {
|
|
967
|
+
console.log(` ${chalk.green('ā')} ${file.filename}`);
|
|
968
|
+
valid++;
|
|
969
|
+
} else {
|
|
970
|
+
console.log(` ${chalk.red('ā')} ${file.filename} ${chalk.dim('(missing)')}`);
|
|
971
|
+
missing++;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
console.log();
|
|
976
|
+
console.log(chalk.cyan('š Verification Results'));
|
|
977
|
+
console.log(` Valid: ${chalk.green(valid)}`);
|
|
978
|
+
console.log(` Missing: ${chalk.red(missing)}`);
|
|
979
|
+
|
|
980
|
+
if (errors.length > 0) {
|
|
981
|
+
console.log(chalk.yellow('\nā ļø Issues found:'));
|
|
982
|
+
for (const err of errors) {
|
|
983
|
+
console.log(chalk.dim(` ${err.file}: ${err.error}`));
|
|
984
|
+
}
|
|
985
|
+
console.log(chalk.dim('\n Tip: Re-upload missing files with `tas push <file>`'));
|
|
986
|
+
} else {
|
|
987
|
+
console.log(chalk.green('\n⨠All files intact!'));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
console.log();
|
|
991
|
+
db.close();
|
|
992
|
+
|
|
993
|
+
} catch (err) {
|
|
994
|
+
spinner.fail(`Verification failed: ${err.message}`);
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// Helper function
|
|
1000
|
+
function formatBytes(bytes) {
|
|
1001
|
+
if (bytes === 0) return '0 B';
|
|
1002
|
+
const k = 1024;
|
|
1003
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1004
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1005
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
program.parse();
|
|
1009
|
+
|
|
1010
|
+
|