@nightowne/tas-cli 2.0.0 → 2.3.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 +326 -132
- package/package.json +4 -2
- package/src/cli.js +199 -24
- package/src/crypto/encryption.js +153 -3
- package/src/db/index.js +19 -13
- package/src/fuse/mount.js +253 -155
- package/src/index.js +209 -112
- package/src/share/server.js +100 -32
- package/src/sync/sync.js +211 -56
- package/src/telegram/client.js +102 -26
- package/src/utils/branding.js +10 -3
- package/src/utils/chunker.js +15 -3
- package/src/utils/cli-helpers.js +44 -6
- package/src/utils/compression.js +30 -0
- package/src/utils/progress.js +3 -3
- package/src/utils/throttle.js +26 -0
package/src/fuse/mount.js
CHANGED
|
@@ -5,21 +5,38 @@
|
|
|
5
5
|
* This is the killer feature - use Telegram like a regular folder!
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import Fuse from 'fuse-native';
|
|
9
8
|
import path from 'path';
|
|
10
9
|
import fs from 'fs';
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
import { processFile } from '../index.js';
|
|
12
|
+
|
|
13
|
+
let Fuse;
|
|
14
|
+
try {
|
|
15
|
+
Fuse = (await import('fuse-native')).default;
|
|
16
|
+
} catch {
|
|
17
|
+
// fuse-native is optional — unavailable on ARM64 or systems without libfuse
|
|
18
|
+
}
|
|
11
19
|
import { TelegramClient } from '../telegram/client.js';
|
|
12
20
|
import { Encryptor } from '../crypto/encryption.js';
|
|
13
21
|
import { Compressor } from '../utils/compression.js';
|
|
14
22
|
import { FileIndex } from '../db/index.js';
|
|
15
23
|
import { createHeader, parseHeader, HEADER_SIZE } from '../utils/chunker.js';
|
|
16
24
|
|
|
17
|
-
// File cache for performance (avoid re-downloading)
|
|
18
|
-
const fileCache = new Map();
|
|
19
25
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
26
|
+
const CACHE_MAX_ENTRIES = 100; // Prevent unbounded memory growth
|
|
20
27
|
|
|
21
28
|
export class TelegramFS {
|
|
22
29
|
constructor(options) {
|
|
30
|
+
if (!Fuse) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'fuse-native is not available on this system.\n' +
|
|
33
|
+
' On Linux x86_64: npm install fuse-native && sudo apt install fuse libfuse-dev\n' +
|
|
34
|
+
' On macOS: brew install macfuse && npm install fuse-native\n' +
|
|
35
|
+
' On ARM64: see https://github.com/ixchio/tas/issues/1 for a workaround\n' +
|
|
36
|
+
' All other TAS commands (push, pull, sync, share) work without FUSE.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
this.dataDir = options.dataDir;
|
|
24
41
|
this.password = options.password;
|
|
25
42
|
this.config = options.config;
|
|
@@ -35,6 +52,9 @@ export class TelegramFS {
|
|
|
35
52
|
|
|
36
53
|
// Pending writes buffer
|
|
37
54
|
this.writeBuffers = new Map();
|
|
55
|
+
|
|
56
|
+
// Instance-level file cache to prevent cross-talk
|
|
57
|
+
this.fileCache = new Map();
|
|
38
58
|
}
|
|
39
59
|
|
|
40
60
|
async initialize() {
|
|
@@ -70,7 +90,7 @@ export class TelegramFS {
|
|
|
70
90
|
mtime: new Date(),
|
|
71
91
|
atime: new Date(),
|
|
72
92
|
ctime: new Date(),
|
|
73
|
-
size: wb.
|
|
93
|
+
size: wb.size,
|
|
74
94
|
mode: 0o100644, // regular file
|
|
75
95
|
uid: process.getuid?.() || 0,
|
|
76
96
|
gid: process.getgid?.() || 0
|
|
@@ -104,9 +124,14 @@ export class TelegramFS {
|
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
const files = this.db.listAll();
|
|
107
|
-
const
|
|
127
|
+
const nameSet = new Set(files.map(f => f.filename));
|
|
128
|
+
|
|
129
|
+
// Include files currently being written (not yet released/uploaded)
|
|
130
|
+
for (const name of this.writeBuffers.keys()) {
|
|
131
|
+
nameSet.add(name);
|
|
132
|
+
}
|
|
108
133
|
|
|
109
|
-
return cb(0,
|
|
134
|
+
return cb(0, [...nameSet]);
|
|
110
135
|
}
|
|
111
136
|
|
|
112
137
|
/**
|
|
@@ -130,26 +155,34 @@ export class TelegramFS {
|
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
/**
|
|
133
|
-
* Read file contents
|
|
158
|
+
* Read file contents from disk cache
|
|
134
159
|
*/
|
|
135
160
|
async read(filepath, fd, buffer, length, position, cb) {
|
|
136
161
|
const filename = path.basename(filepath);
|
|
137
162
|
|
|
138
163
|
try {
|
|
164
|
+
// Check write buffers first
|
|
165
|
+
const wb = this.writeBuffers.get(filename);
|
|
166
|
+
if (wb) {
|
|
167
|
+
const bytesRead = fs.readSync(wb.fd, buffer, 0, length, position);
|
|
168
|
+
return cb(bytesRead);
|
|
169
|
+
}
|
|
170
|
+
|
|
139
171
|
// Check cache first
|
|
140
|
-
let
|
|
172
|
+
let cachedPath = this.getCached(filename);
|
|
141
173
|
|
|
142
|
-
if (!
|
|
143
|
-
// Download and
|
|
144
|
-
|
|
145
|
-
this.setCache(filename,
|
|
174
|
+
if (!cachedPath) {
|
|
175
|
+
// Download, decrypt, and save to disk cache
|
|
176
|
+
cachedPath = await this.downloadFileToCache(filename);
|
|
177
|
+
this.setCache(filename, cachedPath);
|
|
146
178
|
}
|
|
147
179
|
|
|
148
|
-
// Copy requested portion to buffer
|
|
149
|
-
const
|
|
150
|
-
|
|
180
|
+
// Copy requested portion to buffer from disk
|
|
181
|
+
const fdDisk = fs.openSync(cachedPath, 'r');
|
|
182
|
+
const bytesRead = fs.readSync(fdDisk, buffer, 0, length, position);
|
|
183
|
+
fs.closeSync(fdDisk);
|
|
151
184
|
|
|
152
|
-
return cb(
|
|
185
|
+
return cb(bytesRead);
|
|
153
186
|
} catch (err) {
|
|
154
187
|
console.error('Read error:', err.message);
|
|
155
188
|
return cb(Fuse.EIO);
|
|
@@ -157,34 +190,55 @@ export class TelegramFS {
|
|
|
157
190
|
}
|
|
158
191
|
|
|
159
192
|
/**
|
|
160
|
-
*
|
|
193
|
+
* Helper to get or create a disk-buffered write structure
|
|
161
194
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
195
|
+
getOrCreateWriteBuffer(filename) {
|
|
196
|
+
if (this.writeBuffers.has(filename)) {
|
|
197
|
+
return this.writeBuffers.get(filename);
|
|
198
|
+
}
|
|
164
199
|
|
|
165
|
-
|
|
166
|
-
if (!
|
|
167
|
-
|
|
168
|
-
data: Buffer.alloc(0),
|
|
169
|
-
modified: true
|
|
170
|
-
});
|
|
200
|
+
const fuseTmpDir = path.join(this.dataDir, 'fuse-tmp');
|
|
201
|
+
if (!fs.existsSync(fuseTmpDir)) {
|
|
202
|
+
fs.mkdirSync(fuseTmpDir, { recursive: true });
|
|
171
203
|
}
|
|
172
204
|
|
|
173
|
-
const
|
|
205
|
+
const tmpPath = path.join(fuseTmpDir, crypto.randomBytes(16).toString('hex'));
|
|
206
|
+
const fdDisk = fs.openSync(tmpPath, 'w+');
|
|
174
207
|
|
|
175
|
-
//
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
208
|
+
// Check if there is an existing file cached
|
|
209
|
+
const cachedPath = this.getCached(filename);
|
|
210
|
+
let size = 0;
|
|
211
|
+
if (cachedPath && fs.existsSync(cachedPath)) {
|
|
212
|
+
const content = fs.readFileSync(cachedPath);
|
|
213
|
+
fs.writeSync(fdDisk, content, 0, content.length, 0);
|
|
214
|
+
size = content.length;
|
|
181
215
|
}
|
|
182
216
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
217
|
+
const wb = {
|
|
218
|
+
tmpPath,
|
|
219
|
+
fd: fdDisk,
|
|
220
|
+
size,
|
|
221
|
+
modified: true
|
|
222
|
+
};
|
|
223
|
+
this.writeBuffers.set(filename, wb);
|
|
224
|
+
return wb;
|
|
225
|
+
}
|
|
186
226
|
|
|
187
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Write to a file (buffers until release)
|
|
229
|
+
*/
|
|
230
|
+
write(filepath, fd, buffer, length, position, cb) {
|
|
231
|
+
const filename = path.basename(filepath);
|
|
232
|
+
try {
|
|
233
|
+
const wb = this.getOrCreateWriteBuffer(filename);
|
|
234
|
+
fs.writeSync(wb.fd, buffer, 0, length, position);
|
|
235
|
+
wb.size = Math.max(wb.size, position + length);
|
|
236
|
+
wb.modified = true;
|
|
237
|
+
return cb(length);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.error('[FUSE] Write error:', err.message);
|
|
240
|
+
return cb(Fuse.EIO);
|
|
241
|
+
}
|
|
188
242
|
}
|
|
189
243
|
|
|
190
244
|
/**
|
|
@@ -195,9 +249,19 @@ export class TelegramFS {
|
|
|
195
249
|
|
|
196
250
|
console.log(`[FUSE] Creating file: ${filename}`);
|
|
197
251
|
|
|
198
|
-
// Initialize empty write
|
|
252
|
+
// Initialize empty disk-buffered write
|
|
253
|
+
const fuseTmpDir = path.join(this.dataDir, 'fuse-tmp');
|
|
254
|
+
if (!fs.existsSync(fuseTmpDir)) {
|
|
255
|
+
fs.mkdirSync(fuseTmpDir, { recursive: true });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const tmpPath = path.join(fuseTmpDir, crypto.randomBytes(16).toString('hex'));
|
|
259
|
+
const fdDisk = fs.openSync(tmpPath, 'w+');
|
|
260
|
+
|
|
199
261
|
this.writeBuffers.set(filename, {
|
|
200
|
-
|
|
262
|
+
tmpPath,
|
|
263
|
+
fd: fdDisk,
|
|
264
|
+
size: 0,
|
|
201
265
|
modified: true,
|
|
202
266
|
isNew: true
|
|
203
267
|
});
|
|
@@ -219,15 +283,26 @@ export class TelegramFS {
|
|
|
219
283
|
const filename = path.basename(filepath);
|
|
220
284
|
|
|
221
285
|
const wb = this.writeBuffers.get(filename);
|
|
222
|
-
if (!wb
|
|
286
|
+
if (!wb) {
|
|
287
|
+
return cb(0);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!wb.modified) {
|
|
291
|
+
try { fs.closeSync(wb.fd); } catch (e) {}
|
|
292
|
+
try { fs.unlinkSync(wb.tmpPath); } catch (e) {}
|
|
293
|
+
this.writeBuffers.delete(filename);
|
|
223
294
|
return cb(0);
|
|
224
295
|
}
|
|
225
296
|
|
|
226
297
|
try {
|
|
227
|
-
//
|
|
228
|
-
|
|
298
|
+
// Close fd so we can read and upload cleanly
|
|
299
|
+
try { fs.closeSync(wb.fd); } catch (e) {}
|
|
300
|
+
|
|
301
|
+
// Upload to Telegram from the temp path
|
|
302
|
+
await this.uploadFile(filename, wb.tmpPath);
|
|
229
303
|
|
|
230
|
-
//
|
|
304
|
+
// Clean up temp file
|
|
305
|
+
try { fs.unlinkSync(wb.tmpPath); } catch (e) {}
|
|
231
306
|
this.writeBuffers.delete(filename);
|
|
232
307
|
|
|
233
308
|
// Invalidate cache
|
|
@@ -235,7 +310,9 @@ export class TelegramFS {
|
|
|
235
310
|
|
|
236
311
|
return cb(0);
|
|
237
312
|
} catch (err) {
|
|
238
|
-
console.error('Release error:', err.message);
|
|
313
|
+
console.error('[FUSE] Release error:', err.message);
|
|
314
|
+
try { fs.unlinkSync(wb.tmpPath); } catch (e) {}
|
|
315
|
+
this.writeBuffers.delete(filename);
|
|
239
316
|
return cb(Fuse.EIO);
|
|
240
317
|
}
|
|
241
318
|
}
|
|
@@ -252,7 +329,7 @@ export class TelegramFS {
|
|
|
252
329
|
}
|
|
253
330
|
|
|
254
331
|
try {
|
|
255
|
-
// Delete from Telegram
|
|
332
|
+
// Delete from Telegram
|
|
256
333
|
const chunks = this.db.getChunks(file.id);
|
|
257
334
|
for (const chunk of chunks) {
|
|
258
335
|
await this.client.deleteMessage(chunk.message_id);
|
|
@@ -266,7 +343,7 @@ export class TelegramFS {
|
|
|
266
343
|
|
|
267
344
|
return cb(0);
|
|
268
345
|
} catch (err) {
|
|
269
|
-
console.error('Unlink error:', err.message);
|
|
346
|
+
console.error('[FUSE] Unlink error:', err.message);
|
|
270
347
|
return cb(Fuse.EIO);
|
|
271
348
|
}
|
|
272
349
|
}
|
|
@@ -288,10 +365,10 @@ export class TelegramFS {
|
|
|
288
365
|
.run(newName, file.id);
|
|
289
366
|
|
|
290
367
|
// Update cache key
|
|
291
|
-
const cached = fileCache.get(oldName);
|
|
368
|
+
const cached = this.fileCache.get(oldName);
|
|
292
369
|
if (cached) {
|
|
293
|
-
fileCache.delete(oldName);
|
|
294
|
-
fileCache.set(newName, cached);
|
|
370
|
+
this.fileCache.delete(oldName);
|
|
371
|
+
this.fileCache.set(newName, cached);
|
|
295
372
|
}
|
|
296
373
|
|
|
297
374
|
return cb(0);
|
|
@@ -302,77 +379,98 @@ export class TelegramFS {
|
|
|
302
379
|
*/
|
|
303
380
|
truncate(filepath, size, cb) {
|
|
304
381
|
const filename = path.basename(filepath);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
} else {
|
|
315
|
-
this.writeBuffers.set(filename, {
|
|
316
|
-
data: Buffer.alloc(0),
|
|
317
|
-
modified: true
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const wb = this.writeBuffers.get(filename);
|
|
323
|
-
|
|
324
|
-
if (size < wb.data.length) {
|
|
325
|
-
wb.data = wb.data.subarray(0, size);
|
|
326
|
-
} else if (size > wb.data.length) {
|
|
327
|
-
const newBuf = Buffer.alloc(size);
|
|
328
|
-
wb.data.copy(newBuf);
|
|
329
|
-
wb.data = newBuf;
|
|
382
|
+
try {
|
|
383
|
+
const wb = this.getOrCreateWriteBuffer(filename);
|
|
384
|
+
fs.ftruncateSync(wb.fd, size);
|
|
385
|
+
wb.size = size;
|
|
386
|
+
wb.modified = true;
|
|
387
|
+
return cb(0);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.error('[FUSE] Truncate error:', err.message);
|
|
390
|
+
return cb(Fuse.EIO);
|
|
330
391
|
}
|
|
331
|
-
|
|
332
|
-
wb.modified = true;
|
|
333
|
-
return cb(0);
|
|
334
392
|
}
|
|
335
393
|
|
|
336
394
|
// ============== Helper Methods ==============
|
|
337
395
|
|
|
338
|
-
async
|
|
396
|
+
async downloadFileToCache(filename) {
|
|
339
397
|
const file = this.db.findByName(filename);
|
|
340
398
|
if (!file) throw new Error('File not found');
|
|
341
399
|
|
|
400
|
+
const cacheDir = path.join(this.dataDir, 'cache');
|
|
401
|
+
if (!fs.existsSync(cacheDir)) {
|
|
402
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const outputPath = path.join(cacheDir, file.hash);
|
|
406
|
+
|
|
407
|
+
// If it's already fully downloaded and cached on disk, return path
|
|
408
|
+
if (fs.existsSync(outputPath)) {
|
|
409
|
+
const stats = fs.statSync(outputPath);
|
|
410
|
+
if (stats.size === file.original_size) {
|
|
411
|
+
return outputPath;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
342
415
|
const chunks = this.db.getChunks(file.id);
|
|
343
416
|
if (chunks.length === 0) throw new Error('No chunks found');
|
|
344
417
|
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
418
|
+
// Pre-sort chunks
|
|
419
|
+
chunks.sort((a, b) => a.chunk_index - b.chunk_index);
|
|
420
|
+
|
|
421
|
+
const firstChunkData = await this.client.downloadFile(chunks[0].file_telegram_id);
|
|
422
|
+
const header = parseHeader(firstChunkData);
|
|
423
|
+
let wasCompressed = header.compressed;
|
|
424
|
+
|
|
425
|
+
const decryptStream = this.encryptor.getDecryptStream();
|
|
426
|
+
const decompressStream = this.compressor.getDecompressStream(wasCompressed);
|
|
427
|
+
|
|
428
|
+
const { Readable } = await import('stream');
|
|
429
|
+
const { pipeline } = await import('stream/promises');
|
|
430
|
+
|
|
431
|
+
const self = this;
|
|
432
|
+
let currentChunkIndex = 0;
|
|
433
|
+
let preloadedFirstChunk = firstChunkData;
|
|
434
|
+
|
|
435
|
+
const downloadStream = new Readable({
|
|
436
|
+
async read() {
|
|
437
|
+
try {
|
|
438
|
+
if (currentChunkIndex >= chunks.length) {
|
|
439
|
+
this.push(null);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const chunk = chunks[currentChunkIndex];
|
|
444
|
+
let data;
|
|
445
|
+
if (currentChunkIndex === 0 && preloadedFirstChunk) {
|
|
446
|
+
data = preloadedFirstChunk;
|
|
447
|
+
preloadedFirstChunk = null;
|
|
448
|
+
} else {
|
|
449
|
+
data = await self.client.downloadFile(chunk.file_telegram_id);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const payload = data.subarray(HEADER_SIZE);
|
|
453
|
+
this.push(payload);
|
|
454
|
+
currentChunkIndex++;
|
|
455
|
+
} catch (err) {
|
|
456
|
+
this.destroy(err);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
});
|
|
352
460
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
data: payload,
|
|
356
|
-
compressed: header.compressed
|
|
357
|
-
});
|
|
358
|
-
}
|
|
461
|
+
const tmpOutputPath = outputPath + '.tmp';
|
|
462
|
+
const writeStream = fs.createWriteStream(tmpOutputPath);
|
|
359
463
|
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
const encryptedData = Buffer.concat(downloadedChunks.map(c => c.data));
|
|
464
|
+
// Pipeline: Telegram -> Decrypt -> Decompress -> Disk Cache
|
|
465
|
+
await pipeline(downloadStream, decryptStream, decompressStream, writeStream);
|
|
363
466
|
|
|
364
|
-
//
|
|
365
|
-
|
|
467
|
+
// Rename to final atomic path
|
|
468
|
+
fs.renameSync(tmpOutputPath, outputPath);
|
|
366
469
|
|
|
367
|
-
|
|
368
|
-
const wasCompressed = downloadedChunks[0].compressed;
|
|
369
|
-
return await this.compressor.decompress(compressedData, wasCompressed);
|
|
470
|
+
return outputPath;
|
|
370
471
|
}
|
|
371
472
|
|
|
372
|
-
async uploadFile(filename,
|
|
373
|
-
const { hashData } = await import('../crypto/encryption.js');
|
|
374
|
-
const hash = hashData(data);
|
|
375
|
-
|
|
473
|
+
async uploadFile(filename, tempPath) {
|
|
376
474
|
// Check if already exists by name
|
|
377
475
|
const existingByName = this.db.findByName(filename);
|
|
378
476
|
if (existingByName) {
|
|
@@ -384,76 +482,65 @@ export class TelegramFS {
|
|
|
384
482
|
this.db.delete(existingByName.id);
|
|
385
483
|
}
|
|
386
484
|
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Compress
|
|
396
|
-
const { data: compressedData, compressed } = await this.compressor.compress(data, filename);
|
|
397
|
-
|
|
398
|
-
// Encrypt
|
|
399
|
-
const encryptedData = this.encryptor.encrypt(compressedData);
|
|
400
|
-
|
|
401
|
-
// Create temp file with header
|
|
402
|
-
const tempDir = path.join(this.dataDir, 'tmp');
|
|
403
|
-
if (!fs.existsSync(tempDir)) {
|
|
404
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const flags = compressed ? 1 : 0;
|
|
408
|
-
const header = createHeader(filename, data.length, 0, 1, flags);
|
|
409
|
-
const fileData = Buffer.concat([header, encryptedData]);
|
|
410
|
-
|
|
411
|
-
const tempPath = path.join(tempDir, `${hash.substring(0, 12)}.tas`);
|
|
412
|
-
fs.writeFileSync(tempPath, fileData);
|
|
413
|
-
|
|
414
|
-
// Upload to Telegram
|
|
415
|
-
const result = await this.client.sendFile(tempPath, `📦 ${filename}`);
|
|
416
|
-
|
|
417
|
-
// Add to index
|
|
418
|
-
const fileId = this.db.addFile({
|
|
419
|
-
filename,
|
|
420
|
-
hash,
|
|
421
|
-
originalSize: data.length,
|
|
422
|
-
storedSize: encryptedData.length,
|
|
423
|
-
chunks: 1,
|
|
424
|
-
compressed
|
|
485
|
+
// Run the standard chunked upload pipeline (this handles chunking, encryption, compression, and DB entry)
|
|
486
|
+
await processFile(tempPath, {
|
|
487
|
+
password: this.password,
|
|
488
|
+
dataDir: this.dataDir,
|
|
489
|
+
customName: filename,
|
|
490
|
+
config: this.config
|
|
425
491
|
});
|
|
426
|
-
|
|
427
|
-
this.db.addChunk(fileId, 0, result.messageId.toString(), fileData.length);
|
|
428
|
-
this.db.db.prepare('UPDATE chunks SET file_telegram_id = ? WHERE file_id = ? AND chunk_index = ?')
|
|
429
|
-
.run(result.fileId, fileId, 0);
|
|
430
|
-
|
|
431
|
-
// Cleanup
|
|
432
|
-
fs.unlinkSync(tempPath);
|
|
433
|
-
try { fs.rmdirSync(tempDir); } catch (e) { }
|
|
434
492
|
}
|
|
435
493
|
|
|
436
494
|
getCached(filename) {
|
|
437
|
-
const entry = fileCache.get(filename);
|
|
495
|
+
const entry = this.fileCache.get(filename);
|
|
438
496
|
if (!entry) return null;
|
|
439
497
|
|
|
440
498
|
if (Date.now() - entry.timestamp > CACHE_TTL) {
|
|
441
|
-
|
|
499
|
+
// Expired, delete the file if possible
|
|
500
|
+
try {
|
|
501
|
+
if (fs.existsSync(entry.path)) fs.unlinkSync(entry.path);
|
|
502
|
+
} catch (e) { }
|
|
503
|
+
this.fileCache.delete(filename);
|
|
442
504
|
return null;
|
|
443
505
|
}
|
|
444
506
|
|
|
445
|
-
|
|
507
|
+
// Extend cache TTL on read
|
|
508
|
+
entry.timestamp = Date.now();
|
|
509
|
+
return entry.path;
|
|
446
510
|
}
|
|
447
511
|
|
|
448
|
-
setCache(filename,
|
|
449
|
-
|
|
450
|
-
|
|
512
|
+
setCache(filename, cachePath) {
|
|
513
|
+
// Evict oldest entry if cache is full
|
|
514
|
+
if (this.fileCache.size >= CACHE_MAX_ENTRIES) {
|
|
515
|
+
let oldestKey = null;
|
|
516
|
+
let oldestTime = Infinity;
|
|
517
|
+
for (const [key, entry] of this.fileCache) {
|
|
518
|
+
if (entry.timestamp < oldestTime) {
|
|
519
|
+
oldestTime = entry.timestamp;
|
|
520
|
+
oldestKey = key;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (oldestKey) {
|
|
524
|
+
const evicted = this.fileCache.get(oldestKey);
|
|
525
|
+
try { if (fs.existsSync(evicted.path)) fs.unlinkSync(evicted.path); } catch (e) { }
|
|
526
|
+
this.fileCache.delete(oldestKey);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.fileCache.set(filename, {
|
|
531
|
+
path: cachePath,
|
|
451
532
|
timestamp: Date.now()
|
|
452
533
|
});
|
|
453
534
|
}
|
|
454
535
|
|
|
455
536
|
invalidateCache(filename) {
|
|
456
|
-
fileCache.
|
|
537
|
+
const entry = this.fileCache.get(filename);
|
|
538
|
+
if (entry) {
|
|
539
|
+
try {
|
|
540
|
+
if (fs.existsSync(entry.path)) fs.unlinkSync(entry.path);
|
|
541
|
+
} catch (e) { }
|
|
542
|
+
this.fileCache.delete(filename);
|
|
543
|
+
}
|
|
457
544
|
}
|
|
458
545
|
|
|
459
546
|
/**
|
|
@@ -465,6 +552,17 @@ export class TelegramFS {
|
|
|
465
552
|
fs.mkdirSync(this.mountPoint, { recursive: true });
|
|
466
553
|
}
|
|
467
554
|
|
|
555
|
+
// Clean up any stray temp files from previous sessions
|
|
556
|
+
const fuseTmpDir = path.join(this.dataDir, 'fuse-tmp');
|
|
557
|
+
if (fs.existsSync(fuseTmpDir)) {
|
|
558
|
+
try {
|
|
559
|
+
const files = fs.readdirSync(fuseTmpDir);
|
|
560
|
+
for (const file of files) {
|
|
561
|
+
fs.unlinkSync(path.join(fuseTmpDir, file));
|
|
562
|
+
}
|
|
563
|
+
} catch (e) {}
|
|
564
|
+
}
|
|
565
|
+
|
|
468
566
|
const ops = {
|
|
469
567
|
getattr: this.getattr.bind(this),
|
|
470
568
|
readdir: this.readdir.bind(this),
|