@nightowne/tas-cli 2.3.0 → 2.4.1
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 +347 -203
- package/package.json +4 -4
- package/src/cli.js +3 -1
- package/src/crypto/encryption.js +15 -2
- package/src/fuse/mount.js +137 -173
- package/src/index.js +126 -192
- package/src/share/server.js +19 -80
- package/src/sync/sync.js +23 -159
- package/src/telegram/client.js +9 -3
- package/src/utils/branding.js +1 -8
- package/src/utils/download-stream.js +87 -0
package/src/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'path';
|
|
|
8
8
|
import { pipeline } from 'stream/promises';
|
|
9
9
|
import { Encryptor, hashFile } from './crypto/encryption.js';
|
|
10
10
|
import { Compressor } from './utils/compression.js';
|
|
11
|
-
import { createHeader,
|
|
11
|
+
import { createHeader, HEADER_SIZE } from './utils/chunker.js';
|
|
12
12
|
import { TelegramClient } from './telegram/client.js';
|
|
13
13
|
import { FileIndex } from './db/index.js';
|
|
14
14
|
|
|
@@ -37,46 +37,47 @@ export async function processFile(filePath, options) {
|
|
|
37
37
|
const db = new FileIndex(path.join(dataDir, 'index.db'));
|
|
38
38
|
db.init();
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
db.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Prepare processing components
|
|
46
|
-
const compressor = new Compressor();
|
|
47
|
-
const { stream: compressStream, compressed } = compressor.getCompressStream(filename);
|
|
48
|
-
const flags = compressed ? 1 : 0;
|
|
49
|
-
|
|
50
|
-
const encryptor = new Encryptor(password);
|
|
51
|
-
const encryptStream = encryptor.getEncryptStream();
|
|
52
|
-
|
|
53
|
-
const tempDir = process.env.TAS_TMP_DIR || path.join(dataDir, 'tmp');
|
|
54
|
-
if (!fs.existsSync(tempDir)) {
|
|
55
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
56
|
-
}
|
|
40
|
+
try {
|
|
41
|
+
if (db.exists(hash)) {
|
|
42
|
+
throw new Error('File already uploaded (duplicate hash)');
|
|
43
|
+
}
|
|
57
44
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
client.setChatId(config.chatId);
|
|
45
|
+
// Prepare processing components
|
|
46
|
+
const compressor = new Compressor();
|
|
47
|
+
const { stream: compressStream, compressed } = compressor.getCompressStream(filename);
|
|
48
|
+
const flags = compressed ? 1 : 0;
|
|
63
49
|
|
|
64
|
-
|
|
65
|
-
|
|
50
|
+
const encryptor = new Encryptor(password);
|
|
51
|
+
const encryptStream = encryptor.getEncryptStream();
|
|
66
52
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
53
|
+
const tempDir = process.env.TAS_TMP_DIR || path.join(dataDir, 'tmp');
|
|
54
|
+
if (!fs.existsSync(tempDir)) {
|
|
55
|
+
try {
|
|
56
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw new Error(`Failed to create temporary directory at ${tempDir}: ${err.message}. Check permissions or set TAS_TMP_DIR to a writable location.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
74
61
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
62
|
+
// Connect to Telegram
|
|
63
|
+
onProgress?.('Connecting to Telegram...');
|
|
64
|
+
const client = new TelegramClient(dataDir);
|
|
65
|
+
await client.initialize(config.botToken);
|
|
66
|
+
client.setChatId(config.chatId);
|
|
67
|
+
|
|
68
|
+
// We will stream through a custom Writable chunker
|
|
69
|
+
const { Writable } = await import('stream');
|
|
70
|
+
|
|
71
|
+
// First pass estimation (for calculating total chunks and progress)
|
|
72
|
+
// We don't know the exact final size due to compression and encryption overhead,
|
|
73
|
+
// so we'll estimate total chunks and update it if needed.
|
|
74
|
+
// For small files < 49MB we assume 1 chunk.
|
|
75
|
+
let estimatedSize = compressed ? originalSize : originalSize + 128; // Add encryption overhead
|
|
76
|
+
if (compressed && originalSize > 1024 * 1024) estimatedSize = originalSize * 0.8; // Rough guess
|
|
77
|
+
let estimatedChunks = Math.ceil(estimatedSize / TELEGRAM_CHUNK_SIZE) || 1;
|
|
78
|
+
|
|
79
|
+
// Register file in DB
|
|
80
|
+
const fileId = db.addFile({
|
|
80
81
|
filename,
|
|
81
82
|
hash,
|
|
82
83
|
originalSize,
|
|
@@ -84,129 +85,116 @@ export async function processFile(filePath, options) {
|
|
|
84
85
|
chunks: estimatedChunks,
|
|
85
86
|
compressed
|
|
86
87
|
});
|
|
87
|
-
} catch (err) {
|
|
88
|
-
db.db.exec('ROLLBACK');
|
|
89
|
-
db.close();
|
|
90
|
-
throw err;
|
|
91
|
-
}
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
onProgress?.('Processing and uploading streams...');
|
|
90
|
+
let uploadedBytes = 0;
|
|
91
|
+
let chunkIndex = 0;
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
let currentChunkBuffer = Buffer.alloc(0);
|
|
94
|
+
let totalStoredSize = 0;
|
|
99
95
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
// Helper to upload a single chunk
|
|
97
|
+
const uploadCurrentChunk = async (isFinal = false) => {
|
|
98
|
+
if (currentChunkBuffer.length === 0 && !isFinal) return; // Nothing to upload
|
|
99
|
+
if (currentChunkBuffer.length === 0 && isFinal && chunkIndex > 0) return; // Empty final chunk after perfect split
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
// At this point we know if it's the final chunk, so we know the total chunks
|
|
102
|
+
const totalChunks = isFinal ? chunkIndex + 1 : Math.max(estimatedChunks, chunkIndex + 1);
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
const header = createHeader(filename, originalSize, chunkIndex, totalChunks, flags);
|
|
105
|
+
const chunkData = Buffer.concat([header, currentChunkBuffer]);
|
|
110
106
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
const chunkFilename = totalChunks > 1
|
|
108
|
+
? `${hash.substring(0, 12)}.part${chunkIndex}.tas`
|
|
109
|
+
: `${hash.substring(0, 12)}.tas`;
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
const chunkPath = path.join(tempDir, chunkFilename);
|
|
112
|
+
fs.writeFileSync(chunkPath, chunkData);
|
|
117
113
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
const caption = totalChunks > 1
|
|
115
|
+
? `📦 ${filename} (${chunkIndex + 1}/${totalChunks})`
|
|
116
|
+
: `📦 ${filename}`;
|
|
121
117
|
|
|
122
|
-
|
|
118
|
+
onProgress?.(`Uploading chunk ${chunkIndex + 1}...`);
|
|
123
119
|
|
|
124
|
-
|
|
120
|
+
const result = await client.sendFile(chunkPath, caption);
|
|
125
121
|
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
uploadedBytes += chunkData.length;
|
|
123
|
+
totalStoredSize += currentChunkBuffer.length;
|
|
128
124
|
|
|
129
|
-
|
|
125
|
+
onByteProgress?.({ uploaded: uploadedBytes, total: estimatedSize, chunk: chunkIndex + 1, totalChunks });
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
// Store file_id
|
|
128
|
+
db.addChunk(fileId, chunkIndex, result.messageId.toString(), chunkData.length);
|
|
129
|
+
db.db.prepare('UPDATE chunks SET file_telegram_id = ? WHERE file_id = ? AND chunk_index = ?')
|
|
130
|
+
.run(result.fileId, fileId, chunkIndex);
|
|
135
131
|
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
// Clean up temp file immediately to save disk space
|
|
133
|
+
fs.unlinkSync(chunkPath);
|
|
138
134
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
chunkIndex++;
|
|
136
|
+
currentChunkBuffer = Buffer.alloc(0);
|
|
137
|
+
};
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
const chunkingStream = new Writable({
|
|
140
|
+
async write(chunk, encoding, callback) {
|
|
141
|
+
currentChunkBuffer = Buffer.concat([currentChunkBuffer, chunk]);
|
|
146
142
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
143
|
+
// If we exceeded the chunk limit, flush it
|
|
144
|
+
if (currentChunkBuffer.length >= TELEGRAM_CHUNK_SIZE) {
|
|
145
|
+
const overflow = currentChunkBuffer.subarray(TELEGRAM_CHUNK_SIZE);
|
|
146
|
+
currentChunkBuffer = currentChunkBuffer.subarray(0, TELEGRAM_CHUNK_SIZE);
|
|
151
147
|
|
|
148
|
+
try {
|
|
149
|
+
await uploadCurrentChunk(false);
|
|
150
|
+
currentChunkBuffer = overflow; // carry over
|
|
151
|
+
callback();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
callback(err);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
callback();
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
async final(callback) {
|
|
152
160
|
try {
|
|
153
|
-
await uploadCurrentChunk(
|
|
154
|
-
currentChunkBuffer = overflow; // carry over
|
|
161
|
+
await uploadCurrentChunk(true);
|
|
155
162
|
callback();
|
|
156
163
|
} catch (err) {
|
|
157
164
|
callback(err);
|
|
158
165
|
}
|
|
159
|
-
} else {
|
|
160
|
-
callback();
|
|
161
166
|
}
|
|
162
|
-
}
|
|
163
|
-
async final(callback) {
|
|
164
|
-
try {
|
|
165
|
-
await uploadCurrentChunk(true);
|
|
166
|
-
callback();
|
|
167
|
-
} catch (err) {
|
|
168
|
-
callback(err);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
});
|
|
167
|
+
});
|
|
172
168
|
|
|
173
|
-
try {
|
|
174
169
|
const readStream = fs.createReadStream(filePath);
|
|
175
170
|
|
|
176
171
|
// Run the pipeline: Read -> Compress -> Encrypt -> Chunk & Upload
|
|
177
172
|
await pipeline(readStream, compressStream, encryptStream, chunkingStream);
|
|
178
|
-
} catch (pipelineErr) {
|
|
179
|
-
// Pipeline failed — roll back the DB transaction so no orphaned rows remain
|
|
180
|
-
try { db.db.exec('ROLLBACK'); } catch (e) { /* already rolled back */ }
|
|
181
|
-
db.close();
|
|
182
|
-
throw pipelineErr;
|
|
183
|
-
}
|
|
184
173
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
174
|
+
// Update the DB with the final accurate values
|
|
175
|
+
db.db.prepare('UPDATE files SET stored_size = ?, chunks = ? WHERE id = ?')
|
|
176
|
+
.run(totalStoredSize, chunkIndex, fileId);
|
|
188
177
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
178
|
+
// Clean up temp dir (only if empty)
|
|
179
|
+
try {
|
|
180
|
+
const remaining = fs.readdirSync(tempDir);
|
|
181
|
+
if (remaining.length === 0) fs.rmdirSync(tempDir);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Ignore cleanup errors
|
|
184
|
+
}
|
|
193
185
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
186
|
+
return {
|
|
187
|
+
filename,
|
|
188
|
+
hash,
|
|
189
|
+
originalSize,
|
|
190
|
+
storedSize: totalStoredSize,
|
|
191
|
+
chunks: chunkIndex,
|
|
192
|
+
compressed
|
|
193
|
+
};
|
|
194
|
+
} finally {
|
|
195
|
+
// Always close DB connection, even on error
|
|
196
|
+
db.close();
|
|
200
197
|
}
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
filename,
|
|
204
|
-
hash,
|
|
205
|
-
originalSize,
|
|
206
|
-
storedSize: totalStoredSize,
|
|
207
|
-
chunks: chunkIndex,
|
|
208
|
-
compressed
|
|
209
|
-
};
|
|
210
198
|
}
|
|
211
199
|
|
|
212
200
|
/**
|
|
@@ -228,86 +216,32 @@ export async function retrieveFile(fileRecord, options) {
|
|
|
228
216
|
throw new Error('No chunk metadata found for this file');
|
|
229
217
|
}
|
|
230
218
|
|
|
231
|
-
// Prepare components
|
|
232
|
-
const encryptor = new Encryptor(password);
|
|
233
|
-
const decryptStream = encryptor.getDecryptStream();
|
|
234
|
-
|
|
235
|
-
const tempDir = process.env.TAS_TMP_DIR || path.join(dataDir, 'tmp');
|
|
236
|
-
if (!fs.existsSync(tempDir)) {
|
|
237
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
238
|
-
}
|
|
239
|
-
|
|
240
219
|
// Connect to Telegram
|
|
241
220
|
const client = new TelegramClient(dataDir);
|
|
242
221
|
await client.initialize(config.botToken);
|
|
243
222
|
client.setChatId(config.chatId);
|
|
244
223
|
|
|
245
|
-
|
|
246
|
-
const firstChunkData = await client.downloadFile(chunks[0].file_telegram_id);
|
|
247
|
-
const header = parseHeader(firstChunkData);
|
|
248
|
-
|
|
249
|
-
// Total original uncompressed size
|
|
250
|
-
let expectedOriginalSize = header.originalSize;
|
|
251
|
-
let wasCompressed = header.compressed;
|
|
252
|
-
|
|
224
|
+
const encryptor = new Encryptor(password);
|
|
253
225
|
const compressor = new Compressor();
|
|
254
|
-
const decompressStream = compressor.getDecompressStream(wasCompressed);
|
|
255
|
-
|
|
256
|
-
// We need a Readable stream that will lazily fetch chunks from Telegram
|
|
257
|
-
// and push them into the decryption pipeline.
|
|
258
|
-
const { Readable } = await import('stream');
|
|
259
|
-
|
|
260
|
-
const totalBytes = fileRecord.stored_size || chunks.reduce((acc, c) => acc + (c.size || 0), 0);
|
|
261
|
-
let downloadedBytes = 0;
|
|
262
|
-
|
|
263
|
-
// Pre-sort chunks by index so we download them in correct order
|
|
264
|
-
chunks.sort((a, b) => a.chunk_index - b.chunk_index);
|
|
265
226
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// We already downloaded the first chunk to inspect its header, we shouldn't discard it.
|
|
269
|
-
let preloadedFirstChunk = firstChunkData;
|
|
270
|
-
|
|
271
|
-
const downloadStream = new Readable({
|
|
272
|
-
async read() {
|
|
273
|
-
try {
|
|
274
|
-
if (currentChunkIndex >= chunks.length) {
|
|
275
|
-
this.push(null); // End of stream
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
227
|
+
const { createDownloadPipeline } = await import('./utils/download-stream.js');
|
|
278
228
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
data = await client.downloadFile(chunk.file_telegram_id);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
downloadedBytes += data.length;
|
|
291
|
-
onByteProgress?.({ downloaded: downloadedBytes, total: totalBytes, chunk: chunk.chunk_index + 1, totalChunks: chunks.length });
|
|
292
|
-
|
|
293
|
-
// Strip header before pushing
|
|
294
|
-
const payload = data.subarray(HEADER_SIZE);
|
|
295
|
-
this.push(payload);
|
|
296
|
-
|
|
297
|
-
currentChunkIndex++;
|
|
298
|
-
} catch (err) {
|
|
299
|
-
this.destroy(err);
|
|
300
|
-
}
|
|
229
|
+
const { readable } = await createDownloadPipeline({
|
|
230
|
+
client,
|
|
231
|
+
chunks,
|
|
232
|
+
encryptor,
|
|
233
|
+
compressor,
|
|
234
|
+
onChunkDownloaded({ chunkIndex, totalChunks, bytesDownloaded, totalBytes }) {
|
|
235
|
+
onProgress?.(`Downloading chunk ${chunkIndex + 1}/${totalChunks}...`);
|
|
236
|
+
onByteProgress?.({ downloaded: bytesDownloaded, total: totalBytes, chunk: chunkIndex + 1, totalChunks });
|
|
301
237
|
}
|
|
302
238
|
});
|
|
303
239
|
|
|
304
240
|
const writeStream = fs.createWriteStream(outputPath);
|
|
305
|
-
const { pipeline } = await import('stream/promises');
|
|
306
241
|
|
|
307
242
|
onProgress?.('Decrypting, decompressing, and writing file...');
|
|
308
243
|
|
|
309
|
-
|
|
310
|
-
await pipeline(downloadStream, decryptStream, decompressStream, writeStream);
|
|
244
|
+
await pipeline(readable, writeStream);
|
|
311
245
|
|
|
312
246
|
const finalStats = fs.statSync(outputPath);
|
|
313
247
|
|
package/src/share/server.js
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
import http from 'http';
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import path from 'path';
|
|
10
|
+
import { pipeline } from 'stream/promises';
|
|
10
11
|
import { FileIndex } from '../db/index.js';
|
|
11
12
|
import { TelegramClient } from '../telegram/client.js';
|
|
12
13
|
import { Encryptor } from '../crypto/encryption.js';
|
|
13
14
|
import { Compressor } from '../utils/compression.js';
|
|
14
|
-
import {
|
|
15
|
+
import { createDownloadPipeline } from '../utils/download-stream.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Generate a secure random share token
|
|
@@ -239,7 +240,7 @@ export class ShareServer {
|
|
|
239
240
|
this.password = options.password;
|
|
240
241
|
this.config = options.config;
|
|
241
242
|
this.port = options.port || 3000;
|
|
242
|
-
this.host = options.host || '
|
|
243
|
+
this.host = options.host || '127.0.0.1';
|
|
243
244
|
|
|
244
245
|
this.db = null;
|
|
245
246
|
this.client = null;
|
|
@@ -261,64 +262,19 @@ export class ShareServer {
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
/**
|
|
264
|
-
*
|
|
265
|
-
* the first chunk before returning, so the caller can decide
|
|
266
|
-
* whether to commit to a 200 response.
|
|
267
|
-
*
|
|
268
|
-
* Returns: { downloadStream, decryptStream, decompressStream }
|
|
265
|
+
* Stream a decrypted file from Telegram directly to the HTTP response
|
|
269
266
|
*/
|
|
270
|
-
async
|
|
267
|
+
async streamToResponse(fileRecord, res) {
|
|
271
268
|
const chunks = this.db.getChunks(fileRecord.id);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const firstChunkData = await this.client.downloadFile(chunks[0].file_telegram_id);
|
|
279
|
-
const header = parseHeader(firstChunkData);
|
|
280
|
-
let wasCompressed = header.compressed;
|
|
281
|
-
|
|
282
|
-
// Prepare streams
|
|
283
|
-
const decryptStream = this.encryptor.getDecryptStream();
|
|
284
|
-
const decompressStream = this.compressor.getDecompressStream(wasCompressed);
|
|
285
|
-
|
|
286
|
-
const { Readable } = await import('stream');
|
|
287
|
-
|
|
288
|
-
const self = this;
|
|
289
|
-
let currentChunkIndex = 0;
|
|
290
|
-
let preloadedFirstChunk = firstChunkData;
|
|
291
|
-
|
|
292
|
-
const downloadStream = new Readable({
|
|
293
|
-
async read() {
|
|
294
|
-
try {
|
|
295
|
-
if (currentChunkIndex >= chunks.length) {
|
|
296
|
-
this.push(null); // End of stream
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const chunk = chunks[currentChunkIndex];
|
|
301
|
-
let data;
|
|
302
|
-
|
|
303
|
-
if (currentChunkIndex === 0 && preloadedFirstChunk) {
|
|
304
|
-
data = preloadedFirstChunk;
|
|
305
|
-
preloadedFirstChunk = null;
|
|
306
|
-
} else {
|
|
307
|
-
data = await self.client.downloadFile(chunk.file_telegram_id);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Strip header before pushing
|
|
311
|
-
const payload = data.subarray(HEADER_SIZE);
|
|
312
|
-
this.push(payload);
|
|
313
|
-
|
|
314
|
-
currentChunkIndex++;
|
|
315
|
-
} catch (err) {
|
|
316
|
-
this.destroy(err);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
269
|
+
|
|
270
|
+
const { readable } = await createDownloadPipeline({
|
|
271
|
+
client: this.client,
|
|
272
|
+
chunks,
|
|
273
|
+
encryptor: this.encryptor,
|
|
274
|
+
compressor: this.compressor
|
|
319
275
|
});
|
|
320
276
|
|
|
321
|
-
|
|
277
|
+
await pipeline(readable, res);
|
|
322
278
|
}
|
|
323
279
|
|
|
324
280
|
/**
|
|
@@ -401,31 +357,14 @@ export class ShareServer {
|
|
|
401
357
|
const contentType = contentTypes[ext] || 'application/octet-stream';
|
|
402
358
|
const safeName = sanitizeFilenameForHeader(fileRecord.filename);
|
|
403
359
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
// First chunk verified — commit to the 200 response
|
|
410
|
-
res.writeHead(200, {
|
|
411
|
-
'Content-Type': contentType,
|
|
412
|
-
'Content-Disposition': `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(fileRecord.filename)}`,
|
|
413
|
-
'Transfer-Encoding': 'chunked'
|
|
414
|
-
});
|
|
360
|
+
res.writeHead(200, {
|
|
361
|
+
'Content-Type': contentType,
|
|
362
|
+
'Content-Disposition': `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(fileRecord.filename)}`,
|
|
363
|
+
'Transfer-Encoding': 'chunked'
|
|
364
|
+
});
|
|
415
365
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
} catch (streamErr) {
|
|
419
|
-
console.error('Share stream error:', streamErr.message);
|
|
420
|
-
// Headers not yet sent — we can still return a proper error
|
|
421
|
-
if (!res.headersSent) {
|
|
422
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
423
|
-
res.end('Download failed — file could not be decrypted or fetched.');
|
|
424
|
-
} else {
|
|
425
|
-
// Headers already sent (shouldn't happen now, but safety net)
|
|
426
|
-
res.end();
|
|
427
|
-
}
|
|
428
|
-
}
|
|
366
|
+
// Download the file from Telegram, decrypt, decompress and stream directly to 'res'
|
|
367
|
+
await this.streamToResponse(fileRecord, res);
|
|
429
368
|
|
|
430
369
|
} catch (err) {
|
|
431
370
|
console.error('Share server error:', err.message);
|