@exreve/exk 1.0.37 → 1.0.38
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/dist/index.js +6 -6
- package/dist/transferService.js +169 -138
- package/dist/ttc-cli.tar.gz +0 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1545,17 +1545,17 @@ async function runDaemon(foreground = false, email) {
|
|
|
1545
1545
|
console.log(`[CLI] Emitted error app:control:response`);
|
|
1546
1546
|
}
|
|
1547
1547
|
});
|
|
1548
|
-
// Handle folder transfer — this device is the source (
|
|
1549
|
-
socket.on('transfer:
|
|
1548
|
+
// Handle folder transfer — this device is the source (pack & upload)
|
|
1549
|
+
socket.on('transfer:pack', (data) => {
|
|
1550
1550
|
if (foreground) {
|
|
1551
|
-
console.log(`[transfer]
|
|
1551
|
+
console.log(`[transfer] Pack request: ${data.sourcePath} (${data.selectedItems?.length || 0} items) → device ${data.destDeviceId.slice(0, 8)}`);
|
|
1552
1552
|
}
|
|
1553
1553
|
startSending(socket, data, foreground);
|
|
1554
1554
|
});
|
|
1555
|
-
// Handle folder transfer — this device is the destination (
|
|
1556
|
-
socket.on('transfer:
|
|
1555
|
+
// Handle folder transfer — this device is the destination (download & extract)
|
|
1556
|
+
socket.on('transfer:pull', (data) => {
|
|
1557
1557
|
if (foreground) {
|
|
1558
|
-
console.log(`[transfer]
|
|
1558
|
+
console.log(`[transfer] Pull request: device ${data.sourceDeviceId.slice(0, 8)} → ${data.destPath} (${(data.totalBytes / (1024 * 1024)).toFixed(1)} MB)`);
|
|
1559
1559
|
}
|
|
1560
1560
|
startReceiving(socket, data, foreground);
|
|
1561
1561
|
});
|
package/dist/transferService.js
CHANGED
|
@@ -4,26 +4,30 @@ import fsSync from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
|
+
import { Readable, Transform } from 'stream';
|
|
7
8
|
// ============ Transfer Service (CLI side) ============
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
9
|
+
// Two-phase transfer via HTTP:
|
|
10
|
+
// Phase 1 (source): tar selected items → HTTP POST to backend
|
|
11
|
+
// Phase 2 (dest): HTTP GET from backend → tar extract
|
|
12
|
+
// Socket.IO carries control events + progress/log.
|
|
11
13
|
// ---- Sender ----
|
|
12
14
|
export function startSending(socket, transfer, foreground) {
|
|
13
|
-
const { transferId, sourcePath } = transfer;
|
|
15
|
+
const { transferId, sourcePath, selectedItems } = transfer;
|
|
14
16
|
const startTime = Date.now();
|
|
17
|
+
const apiUrl = getApiUrl(socket);
|
|
15
18
|
const log = (message) => {
|
|
16
19
|
if (foreground)
|
|
17
20
|
console.log(`[transfer:send] ${message}`);
|
|
18
21
|
socket.emit('transfer:log', { transferId, side: 'source', level: 'info', message });
|
|
19
22
|
};
|
|
20
|
-
const sendProgress = (bytesTransferred, totalBytes) => {
|
|
23
|
+
const sendProgress = (phase, bytesTransferred, totalBytes) => {
|
|
21
24
|
const elapsed = (Date.now() - startTime) / 1000;
|
|
22
25
|
const speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
|
|
23
26
|
const eta = speed > 0 ? (totalBytes - bytesTransferred) / speed : 0;
|
|
24
27
|
socket.emit('transfer:progress', {
|
|
25
28
|
transferId,
|
|
26
29
|
side: 'source',
|
|
30
|
+
phase,
|
|
27
31
|
bytesTransferred,
|
|
28
32
|
totalBytes,
|
|
29
33
|
speed: Math.round(speed),
|
|
@@ -34,51 +38,35 @@ export function startSending(socket, transfer, foreground) {
|
|
|
34
38
|
console.error(`[transfer:send] Error: ${message}`);
|
|
35
39
|
socket.emit('transfer:error', { transferId, side: 'source', message });
|
|
36
40
|
};
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
const items = selectedItems || ['.'];
|
|
42
|
+
log(`Packing ${items.length} item(s) from ${sourcePath}`);
|
|
43
|
+
// Estimate size first
|
|
39
44
|
let totalBytes = 0;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const sizeEstimate = spawn('du', ['-sb', ...items.map(i => path.join(sourcePath, i))], { cwd: sourcePath });
|
|
46
|
+
let duOutput = '';
|
|
47
|
+
sizeEstimate.stdout.on('data', (d) => { duOutput += d.toString(); });
|
|
48
|
+
sizeEstimate.on('close', (code) => {
|
|
49
|
+
if (code === 0) {
|
|
50
|
+
const lines = duOutput.trim().split('\n');
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const match = line.match(/^(\d+)/);
|
|
47
53
|
if (match)
|
|
48
|
-
totalBytes
|
|
54
|
+
totalBytes += parseInt(match[1], 10);
|
|
49
55
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Spawn tar to stream the folder
|
|
60
|
-
const tar = spawn('tar', ['cf', '-', '-C', sourcePath, '.']);
|
|
56
|
+
}
|
|
57
|
+
log(`Estimated size: ${formatBytes(totalBytes)}`);
|
|
58
|
+
doPackAndUpload();
|
|
59
|
+
});
|
|
60
|
+
sizeEstimate.on('error', () => doPackAndUpload());
|
|
61
|
+
function doPackAndUpload() {
|
|
62
|
+
// Create tar with selected items
|
|
63
|
+
const tarArgs = ['cf', '-', '-C', sourcePath, ...items];
|
|
64
|
+
const tar = spawn('tar', tarArgs);
|
|
61
65
|
const hash = createHash('sha256');
|
|
62
66
|
let bytesTransferred = 0;
|
|
63
|
-
let seq = 0;
|
|
64
67
|
let lastProgressTime = Date.now();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
bytesTransferred += chunk.length;
|
|
68
|
-
// Emit binary chunk
|
|
69
|
-
socket.emit('transfer:chunk', {
|
|
70
|
-
transferId,
|
|
71
|
-
seq,
|
|
72
|
-
data: chunk,
|
|
73
|
-
size: chunk.length,
|
|
74
|
-
});
|
|
75
|
-
seq++;
|
|
76
|
-
// Throttle progress updates to every 500ms
|
|
77
|
-
if (Date.now() - lastProgressTime > 500) {
|
|
78
|
-
sendProgress(bytesTransferred, totalBytes || bytesTransferred);
|
|
79
|
-
lastProgressTime = Date.now();
|
|
80
|
-
}
|
|
81
|
-
});
|
|
68
|
+
let fileCount = 0;
|
|
69
|
+
// Count files as we go (approximate from tar output)
|
|
82
70
|
tar.stderr.on('data', (data) => {
|
|
83
71
|
const msg = data.toString().trim();
|
|
84
72
|
if (msg)
|
|
@@ -92,19 +80,52 @@ export function startSending(socket, transfer, foreground) {
|
|
|
92
80
|
sendError(`tar exited with code ${code}`);
|
|
93
81
|
return;
|
|
94
82
|
}
|
|
95
|
-
|
|
83
|
+
log(`Packing complete. Final size: ${formatBytes(bytesTransferred)}`);
|
|
96
84
|
const sha256 = hash.digest('hex');
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// Instead, we send a source:done so the dest knows the stream is over
|
|
101
|
-
socket.emit('transfer:source:done', {
|
|
85
|
+
const duration = Date.now() - startTime;
|
|
86
|
+
// Notify backend that upload is done
|
|
87
|
+
socket.emit('transfer:uploaded', {
|
|
102
88
|
transferId,
|
|
103
89
|
totalBytes: bytesTransferred,
|
|
104
90
|
sha256,
|
|
105
|
-
|
|
91
|
+
fileCount,
|
|
106
92
|
});
|
|
107
|
-
log(`
|
|
93
|
+
log(`Upload complete: ${formatBytes(bytesTransferred)} in ${(duration / 1000).toFixed(1)}s (sha256: ${sha256.slice(0, 12)}...)`);
|
|
94
|
+
});
|
|
95
|
+
// Stream tar output through hash, then HTTP POST
|
|
96
|
+
const uploadUrl = `${apiUrl}/transfer/${transferId}/upload`;
|
|
97
|
+
// Pipe tar stdout → hash transform → HTTP upload
|
|
98
|
+
const hashTransform = new Transform({
|
|
99
|
+
transform(chunk, _encoding, callback) {
|
|
100
|
+
hash.update(chunk);
|
|
101
|
+
bytesTransferred += chunk.length;
|
|
102
|
+
if (Date.now() - lastProgressTime > 500) {
|
|
103
|
+
sendProgress('uploading', bytesTransferred, totalBytes || bytesTransferred);
|
|
104
|
+
lastProgressTime = Date.now();
|
|
105
|
+
}
|
|
106
|
+
this.push(chunk);
|
|
107
|
+
callback();
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
// Use fetch with streaming body for the upload
|
|
111
|
+
const tarStream = tar.stdout.pipe(hashTransform);
|
|
112
|
+
// Node 18+ supports ReadableStream in fetch body
|
|
113
|
+
const nodeStream = Readable.toWeb(tarStream);
|
|
114
|
+
fetch(uploadUrl, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
body: nodeStream,
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/x-tar',
|
|
119
|
+
'Transfer-Encoding': 'chunked',
|
|
120
|
+
},
|
|
121
|
+
duplex: 'half',
|
|
122
|
+
}).then(async (res) => {
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
sendError(`Upload failed: HTTP ${res.status} ${text}`);
|
|
126
|
+
}
|
|
127
|
+
}).catch((err) => {
|
|
128
|
+
sendError(`Upload network error: ${err.message}`);
|
|
108
129
|
});
|
|
109
130
|
// Handle cancellation
|
|
110
131
|
const cancelHandler = (data) => {
|
|
@@ -121,18 +142,22 @@ export function startSending(socket, transfer, foreground) {
|
|
|
121
142
|
export function startReceiving(socket, transfer, foreground) {
|
|
122
143
|
const { transferId, destPath } = transfer;
|
|
123
144
|
const startTime = Date.now();
|
|
145
|
+
const apiUrl = getApiUrl(socket);
|
|
146
|
+
const expectedBytes = transfer.totalBytes || 0;
|
|
147
|
+
const expectedSha256 = transfer.sha256 || '';
|
|
124
148
|
const log = (message) => {
|
|
125
149
|
if (foreground)
|
|
126
150
|
console.log(`[transfer:recv] ${message}`);
|
|
127
151
|
socket.emit('transfer:log', { transferId, side: 'dest', level: 'info', message });
|
|
128
152
|
};
|
|
129
|
-
const sendProgress = (bytesTransferred, totalBytes) => {
|
|
153
|
+
const sendProgress = (phase, bytesTransferred, totalBytes) => {
|
|
130
154
|
const elapsed = (Date.now() - startTime) / 1000;
|
|
131
155
|
const speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
|
|
132
156
|
const eta = speed > 0 ? (totalBytes - bytesTransferred) / speed : 0;
|
|
133
157
|
socket.emit('transfer:progress', {
|
|
134
158
|
transferId,
|
|
135
159
|
side: 'dest',
|
|
160
|
+
phase,
|
|
136
161
|
bytesTransferred,
|
|
137
162
|
totalBytes,
|
|
138
163
|
speed: Math.round(speed),
|
|
@@ -144,103 +169,109 @@ export function startReceiving(socket, transfer, foreground) {
|
|
|
144
169
|
socket.emit('transfer:error', { transferId, side: 'dest', message });
|
|
145
170
|
};
|
|
146
171
|
const tmpFile = path.join(os.tmpdir(), `ttc-transfer-${transferId}.tar`);
|
|
147
|
-
log(`
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let lastProgressTime = Date.now();
|
|
155
|
-
let sourceSha256 = '';
|
|
156
|
-
// Handle incoming chunks
|
|
157
|
-
const chunkHandler = (data) => {
|
|
158
|
-
if (data.transferId !== transferId)
|
|
172
|
+
log(`Downloading from backend...`);
|
|
173
|
+
mkdir(destPath, { recursive: true }).then(async () => {
|
|
174
|
+
try {
|
|
175
|
+
const downloadUrl = `${apiUrl}/transfer/${transferId}/download`;
|
|
176
|
+
const res = await fetch(downloadUrl);
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
sendError(`Download failed: HTTP ${res.status}`);
|
|
159
179
|
return;
|
|
160
|
-
const chunk = Buffer.isBuffer(data.data) ? data.data : Buffer.from(data.data);
|
|
161
|
-
writeStream.write(chunk);
|
|
162
|
-
hash.update(chunk);
|
|
163
|
-
bytesReceived += chunk.length;
|
|
164
|
-
if (Date.now() - lastProgressTime > 500) {
|
|
165
|
-
sendProgress(bytesReceived, totalExpected || bytesReceived);
|
|
166
|
-
lastProgressTime = Date.now();
|
|
167
180
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Handle source:done — source finished sending
|
|
171
|
-
const sourceDoneHandler = (data) => {
|
|
172
|
-
if (data.transferId !== transferId)
|
|
181
|
+
if (!res.body) {
|
|
182
|
+
sendError('Download failed: no response body');
|
|
173
183
|
return;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const extractProc = spawn('tar', ['xf', tmpFile, '-C', destPath]);
|
|
193
|
-
extractProc.on('close', async (code) => {
|
|
194
|
-
// Cleanup temp file
|
|
195
|
-
try {
|
|
196
|
-
await unlink(tmpFile);
|
|
197
|
-
}
|
|
198
|
-
catch { }
|
|
199
|
-
if (code !== 0) {
|
|
200
|
-
sendError(`tar extraction failed with code ${code}`);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const duration = Date.now() - startTime;
|
|
204
|
-
// Notify completion
|
|
205
|
-
socket.emit('transfer:complete', {
|
|
206
|
-
transferId,
|
|
207
|
-
totalBytes: bytesReceived,
|
|
208
|
-
sha256: receivedHash,
|
|
209
|
-
duration,
|
|
210
|
-
});
|
|
211
|
-
log(`Complete! ${formatBytes(bytesReceived)} extracted to ${destPath} in ${(duration / 1000).toFixed(1)}s`);
|
|
212
|
-
// Cleanup listeners
|
|
213
|
-
socket.off('transfer:chunk', chunkHandler);
|
|
214
|
-
socket.off('transfer:source:done', sourceDoneHandler);
|
|
215
|
-
});
|
|
216
|
-
extractProc.on('error', (err) => {
|
|
217
|
-
sendError(`tar extraction error: ${err.message}`);
|
|
218
|
-
});
|
|
184
|
+
}
|
|
185
|
+
// Stream download to temp file while hashing
|
|
186
|
+
const writeStream = fsSync.createWriteStream(tmpFile);
|
|
187
|
+
const hash = createHash('sha256');
|
|
188
|
+
let bytesReceived = 0;
|
|
189
|
+
let lastProgressTime = Date.now();
|
|
190
|
+
const reader = res.body.getReader();
|
|
191
|
+
// Read chunks from the response stream
|
|
192
|
+
while (true) {
|
|
193
|
+
const { done, value } = await reader.read();
|
|
194
|
+
if (done)
|
|
195
|
+
break;
|
|
196
|
+
writeStream.write(value);
|
|
197
|
+
hash.update(value);
|
|
198
|
+
bytesReceived += value.length;
|
|
199
|
+
if (Date.now() - lastProgressTime > 500) {
|
|
200
|
+
sendProgress('downloading', bytesReceived, expectedBytes || bytesReceived);
|
|
201
|
+
lastProgressTime = Date.now();
|
|
219
202
|
}
|
|
220
|
-
|
|
221
|
-
|
|
203
|
+
}
|
|
204
|
+
writeStream.end();
|
|
205
|
+
log(`Downloaded ${formatBytes(bytesReceived)}. Verifying...`);
|
|
206
|
+
// Verify hash
|
|
207
|
+
const receivedHash = hash.digest('hex');
|
|
208
|
+
if (expectedSha256 && receivedHash !== expectedSha256) {
|
|
209
|
+
sendError(`Hash mismatch! Expected ${expectedSha256.slice(0, 16)}... got ${receivedHash.slice(0, 16)}...`);
|
|
210
|
+
try {
|
|
211
|
+
await unlink(tmpFile);
|
|
222
212
|
}
|
|
213
|
+
catch { }
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
log(`Hash verified ✓. Extracting to ${destPath}...`);
|
|
217
|
+
sendProgress('extracting', bytesReceived, bytesReceived);
|
|
218
|
+
// Extract tar
|
|
219
|
+
const extractProc = spawn('tar', ['xf', tmpFile, '-C', destPath]);
|
|
220
|
+
await new Promise((resolve, reject) => {
|
|
221
|
+
extractProc.on('close', (code) => {
|
|
222
|
+
if (code !== 0)
|
|
223
|
+
reject(new Error(`tar extraction failed with code ${code}`));
|
|
224
|
+
else
|
|
225
|
+
resolve();
|
|
226
|
+
});
|
|
227
|
+
extractProc.on('error', reject);
|
|
223
228
|
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const cancelHandler = (data) => {
|
|
228
|
-
if (data.transferId === transferId) {
|
|
229
|
-
writeStream.destroy();
|
|
230
|
-
socket.off('transfer:chunk', chunkHandler);
|
|
231
|
-
socket.off('transfer:source:done', sourceDoneHandler);
|
|
232
|
-
socket.off('transfer:cancel', cancelHandler);
|
|
233
|
-
log('Transfer cancelled');
|
|
234
|
-
// Cleanup temp file
|
|
235
|
-
unlink(tmpFile).catch(() => { });
|
|
229
|
+
// Cleanup temp file
|
|
230
|
+
try {
|
|
231
|
+
await unlink(tmpFile);
|
|
236
232
|
}
|
|
237
|
-
|
|
238
|
-
|
|
233
|
+
catch { }
|
|
234
|
+
const duration = Date.now() - startTime;
|
|
235
|
+
socket.emit('transfer:complete', {
|
|
236
|
+
transferId,
|
|
237
|
+
totalBytes: bytesReceived,
|
|
238
|
+
sha256: receivedHash,
|
|
239
|
+
duration,
|
|
240
|
+
fileCount: 0,
|
|
241
|
+
});
|
|
242
|
+
log(`Complete! ${formatBytes(bytesReceived)} extracted to ${destPath} in ${(duration / 1000).toFixed(1)}s`);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
sendError(`Download/extract error: ${err.message}`);
|
|
246
|
+
try {
|
|
247
|
+
await unlink(tmpFile);
|
|
248
|
+
}
|
|
249
|
+
catch { }
|
|
250
|
+
}
|
|
239
251
|
}).catch((err) => {
|
|
240
252
|
sendError(`Failed to create destination directory: ${err.message}`);
|
|
241
253
|
});
|
|
242
254
|
}
|
|
243
255
|
// ---- Helpers ----
|
|
256
|
+
function getApiUrl(socket) {
|
|
257
|
+
// Extract the base URL from the socket connection
|
|
258
|
+
const io = socket.io;
|
|
259
|
+
const opts = io.opts;
|
|
260
|
+
if (opts?.hostname) {
|
|
261
|
+
const protocol = opts.secure !== false ? 'https' : 'http';
|
|
262
|
+
const port = opts.port ? `:${opts.port}` : '';
|
|
263
|
+
return `${protocol}://${opts.hostname}${port}`;
|
|
264
|
+
}
|
|
265
|
+
// Fallback: read from config
|
|
266
|
+
try {
|
|
267
|
+
const configPath = path.join(os.homedir(), '.talk-to-code', 'config.json');
|
|
268
|
+
const config = JSON.parse(fsSync.readFileSync(configPath, 'utf-8'));
|
|
269
|
+
return config.apiUrl || 'https://api.talk-to-code.com';
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return 'https://api.talk-to-code.com';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
244
275
|
function formatBytes(bytes) {
|
|
245
276
|
if (bytes === 0)
|
|
246
277
|
return '0 B';
|
package/dist/ttc-cli.tar.gz
CHANGED
|
Binary file
|