@balaji003/lantransfer 1.0.6 → 1.0.8
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/package.json +1 -1
- package/public/index.html +73 -14
- package/server.js +124 -28
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -158,6 +158,34 @@
|
|
|
158
158
|
}
|
|
159
159
|
.file-item-remove:hover { color: var(--red); }
|
|
160
160
|
|
|
161
|
+
.transfer-actions {
|
|
162
|
+
display: flex; align-items: center; gap: 10px;
|
|
163
|
+
margin-top: 8px; font-size: 12px;
|
|
164
|
+
}
|
|
165
|
+
.transfer-size { color: var(--text-dim); }
|
|
166
|
+
.btn-open-folder {
|
|
167
|
+
background: none; border: 1px solid var(--border); color: var(--accent);
|
|
168
|
+
padding: 3px 10px; border-radius: 4px; cursor: pointer;
|
|
169
|
+
font-size: 12px; transition: all 0.12s;
|
|
170
|
+
}
|
|
171
|
+
.btn-open-folder:hover { background: var(--surface-hover); border-color: var(--accent); }
|
|
172
|
+
|
|
173
|
+
.download-dir {
|
|
174
|
+
display: flex; align-items: center; gap: 8px; padding: 8px 24px;
|
|
175
|
+
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
176
|
+
font-size: 12px; color: var(--text-dim); flex-shrink: 0;
|
|
177
|
+
}
|
|
178
|
+
.download-dir-path {
|
|
179
|
+
color: var(--text); font-weight: 500; overflow: hidden;
|
|
180
|
+
text-overflow: ellipsis; white-space: nowrap; max-width: 400px;
|
|
181
|
+
}
|
|
182
|
+
.btn-change-dir {
|
|
183
|
+
background: none; border: 1px solid var(--border); color: var(--accent);
|
|
184
|
+
padding: 2px 8px; border-radius: 4px; cursor: pointer;
|
|
185
|
+
font-size: 11px; transition: all 0.12s; white-space: nowrap;
|
|
186
|
+
}
|
|
187
|
+
.btn-change-dir:hover { background: var(--surface-hover); border-color: var(--accent); }
|
|
188
|
+
|
|
161
189
|
/* ── Transfers ── */
|
|
162
190
|
.transfers-section { flex: 1; overflow-y: auto; padding: 16px 24px; }
|
|
163
191
|
.transfer {
|
|
@@ -256,6 +284,13 @@
|
|
|
256
284
|
</div>
|
|
257
285
|
</div>
|
|
258
286
|
|
|
287
|
+
<!-- Download location -->
|
|
288
|
+
<div class="download-dir">
|
|
289
|
+
<span>Save to:</span>
|
|
290
|
+
<span class="download-dir-path" id="download-dir" title=""></span>
|
|
291
|
+
<button class="btn-change-dir" onclick="api('set-download-dir')">Change</button>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
259
294
|
<!-- Main -->
|
|
260
295
|
<div class="main">
|
|
261
296
|
<!-- Sidebar: peers -->
|
|
@@ -312,6 +347,8 @@
|
|
|
312
347
|
evtSource = new EventSource('/events');
|
|
313
348
|
evtSource.onmessage = e => {
|
|
314
349
|
state = JSON.parse(e.data);
|
|
350
|
+
console.log('[sse] State update — peers:', state.peers?.length, 'files:', state.sharedFiles?.length,
|
|
351
|
+
'transfers:', state.transfers?.length, 'incoming:', !!state.incomingRequest);
|
|
315
352
|
// Auto-select the latest file if none selected
|
|
316
353
|
if (!selectedFileId && state.sharedFiles && state.sharedFiles.length > 0) {
|
|
317
354
|
selectedFileId = state.sharedFiles[state.sharedFiles.length - 1].id;
|
|
@@ -319,9 +356,11 @@
|
|
|
319
356
|
render();
|
|
320
357
|
};
|
|
321
358
|
evtSource.onopen = () => {
|
|
359
|
+
console.log('[sse] Connected');
|
|
322
360
|
document.getElementById('conn-status').textContent = '';
|
|
323
361
|
};
|
|
324
|
-
evtSource.onerror = () => {
|
|
362
|
+
evtSource.onerror = (e) => {
|
|
363
|
+
console.error('[sse] Error/disconnected', e);
|
|
325
364
|
document.getElementById('conn-status').textContent = 'Reconnecting...';
|
|
326
365
|
};
|
|
327
366
|
}
|
|
@@ -331,10 +370,16 @@
|
|
|
331
370
|
// API
|
|
332
371
|
// ──────────────────────────────────────────────────────────────────────────
|
|
333
372
|
function api(endpoint, data) {
|
|
373
|
+
console.log('[api]', endpoint, data || '');
|
|
334
374
|
return fetch('/api/' + endpoint, {
|
|
335
375
|
method: 'POST',
|
|
336
376
|
headers: { 'Content-Type': 'application/json' },
|
|
337
377
|
body: JSON.stringify(data || {}),
|
|
378
|
+
}).then(r => {
|
|
379
|
+
console.log('[api]', endpoint, 'response:', r.status);
|
|
380
|
+
return r;
|
|
381
|
+
}).catch(err => {
|
|
382
|
+
console.error('[api]', endpoint, 'error:', err);
|
|
338
383
|
});
|
|
339
384
|
}
|
|
340
385
|
|
|
@@ -353,13 +398,9 @@
|
|
|
353
398
|
if (selectedFileId === id) selectedFileId = null;
|
|
354
399
|
}
|
|
355
400
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
document.getElementById('browse-btn').textContent = 'Opening...';
|
|
360
|
-
await api('browse');
|
|
361
|
-
browsing = false;
|
|
362
|
-
document.getElementById('browse-btn').textContent = 'Browse';
|
|
401
|
+
function doBrowse() {
|
|
402
|
+
console.log('[browse] Clicked');
|
|
403
|
+
api('browse');
|
|
363
404
|
}
|
|
364
405
|
|
|
365
406
|
function doSend() {
|
|
@@ -368,8 +409,14 @@
|
|
|
368
409
|
}
|
|
369
410
|
}
|
|
370
411
|
|
|
371
|
-
function respondTransfer(
|
|
372
|
-
|
|
412
|
+
function respondTransfer(accepted) {
|
|
413
|
+
console.log('[respondTransfer] accepted:', accepted);
|
|
414
|
+
api('respond', { accepted: accepted });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function openFolder(transferId) {
|
|
418
|
+
console.log('[openFolder] transferId:', transferId);
|
|
419
|
+
api('open-folder', { transferId: transferId });
|
|
373
420
|
}
|
|
374
421
|
|
|
375
422
|
function doShutdown() {
|
|
@@ -403,6 +450,11 @@
|
|
|
403
450
|
}
|
|
404
451
|
document.getElementById('local-ip').textContent = state.localIP || '';
|
|
405
452
|
|
|
453
|
+
// Download dir
|
|
454
|
+
const dirEl = document.getElementById('download-dir');
|
|
455
|
+
dirEl.textContent = state.downloadDir || '';
|
|
456
|
+
dirEl.title = state.downloadDir || '';
|
|
457
|
+
|
|
406
458
|
// Toggle
|
|
407
459
|
const toggle = document.getElementById('toggle');
|
|
408
460
|
const toggleText = document.getElementById('toggle-text');
|
|
@@ -487,8 +539,8 @@
|
|
|
487
539
|
// Modal
|
|
488
540
|
const overlay = document.getElementById('modal-overlay');
|
|
489
541
|
const modal = document.getElementById('modal');
|
|
490
|
-
if (state.
|
|
491
|
-
const req = state.
|
|
542
|
+
if (state.incomingRequest) {
|
|
543
|
+
const req = state.incomingRequest;
|
|
492
544
|
overlay.classList.add('show');
|
|
493
545
|
modal.innerHTML =
|
|
494
546
|
'<div class="modal-title">Incoming File</div>'
|
|
@@ -496,8 +548,8 @@
|
|
|
496
548
|
+ '<div class="modal-file">' + esc(req.filename) + '</div>'
|
|
497
549
|
+ '<div class="modal-size">' + formatSize(req.size) + '</div>'
|
|
498
550
|
+ '<div class="modal-buttons">'
|
|
499
|
-
+ '<button class="btn-accept" onclick="respondTransfer(
|
|
500
|
-
+ '<button class="btn-reject" onclick="respondTransfer(
|
|
551
|
+
+ '<button class="btn-accept" onclick="respondTransfer(true)">Accept</button>'
|
|
552
|
+
+ '<button class="btn-reject" onclick="respondTransfer(false)">Reject</button>'
|
|
501
553
|
+ '</div>';
|
|
502
554
|
} else {
|
|
503
555
|
overlay.classList.remove('show');
|
|
@@ -525,6 +577,13 @@
|
|
|
525
577
|
+ '<span>' + formatSize(t.size) + '</span>'
|
|
526
578
|
+ '<span>' + formatSpeed(t.speed) + '</span>'
|
|
527
579
|
+ '</div>';
|
|
580
|
+
} else if (t.status === 'completed' || t.status === 'failed') {
|
|
581
|
+
extra = '<div class="transfer-actions">'
|
|
582
|
+
+ '<span class="transfer-size">' + formatSize(t.size) + '</span>';
|
|
583
|
+
if (t.status === 'completed' && t.direction === 'receive' && t.savePath) {
|
|
584
|
+
extra += '<button class="btn-open-folder" onclick="openFolder(\'' + esc(t.id) + '\')">Open Folder</button>';
|
|
585
|
+
}
|
|
586
|
+
extra += '</div>';
|
|
528
587
|
}
|
|
529
588
|
|
|
530
589
|
return '<div class="transfer">'
|
package/server.js
CHANGED
|
@@ -58,10 +58,11 @@ const config = loadConfig();
|
|
|
58
58
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
59
59
|
let isDiscoverable = true;
|
|
60
60
|
let deviceName = config.deviceName || os.hostname();
|
|
61
|
+
let downloadDir = config.downloadDir || path.join(os.homedir(), 'Downloads');
|
|
61
62
|
const sharedFiles = []; // [ { id, path, name, size } ]
|
|
62
63
|
const peers = new Map(); // id → { device_name, ip, tcp_port, last_seen }
|
|
63
64
|
const transfers = []; // [ TransferEntry ]
|
|
64
|
-
|
|
65
|
+
let pendingRequest = null; // { filename, size, senderName, resolve } or null
|
|
65
66
|
let idCounter = 0;
|
|
66
67
|
let browseInProgress = false;
|
|
67
68
|
|
|
@@ -99,6 +100,7 @@ function serializeState() {
|
|
|
99
100
|
return JSON.stringify({
|
|
100
101
|
isDiscoverable,
|
|
101
102
|
deviceName,
|
|
103
|
+
downloadDir,
|
|
102
104
|
localIP: getLocalIP(),
|
|
103
105
|
peers: [...peers.entries()].map(([id, p]) => ({
|
|
104
106
|
id, name: p.device_name, ip: p.ip,
|
|
@@ -110,10 +112,11 @@ function serializeState() {
|
|
|
110
112
|
direction: t.direction, peerName: t.peerName,
|
|
111
113
|
status: t.status, progress: t.progress,
|
|
112
114
|
speed: t.speed, error: t.error,
|
|
115
|
+
savePath: t.savePath || null,
|
|
113
116
|
})),
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
incomingRequest: pendingRequest
|
|
118
|
+
? { filename: pendingRequest.filename, size: pendingRequest.size, senderName: pendingRequest.senderName }
|
|
119
|
+
: null,
|
|
117
120
|
});
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -177,6 +180,8 @@ function readExact(socket, n) {
|
|
|
177
180
|
const htmlPath = path.join(__dirname, 'public', 'index.html');
|
|
178
181
|
|
|
179
182
|
const server = http.createServer(async (req, res) => {
|
|
183
|
+
console.log(` [http] ${req.method} ${req.url}`);
|
|
184
|
+
|
|
180
185
|
// ── Serve the web UI ──
|
|
181
186
|
if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
|
|
182
187
|
fs.readFile(htmlPath, (err, data) => {
|
|
@@ -214,7 +219,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
214
219
|
'Connection': 'keep-alive',
|
|
215
220
|
});
|
|
216
221
|
sseClients.add(res);
|
|
217
|
-
|
|
222
|
+
console.log(` [sse] Client connected (total: ${sseClients.size})`);
|
|
223
|
+
req.on('close', () => {
|
|
224
|
+
sseClients.delete(res);
|
|
225
|
+
console.log(` [sse] Client disconnected (total: ${sseClients.size})`);
|
|
226
|
+
});
|
|
218
227
|
res.write(`data: ${serializeState()}\n\n`);
|
|
219
228
|
return;
|
|
220
229
|
}
|
|
@@ -230,32 +239,40 @@ const server = http.createServer(async (req, res) => {
|
|
|
230
239
|
console.log(` [toggle] Discoverable: ${isDiscoverable}`);
|
|
231
240
|
}
|
|
232
241
|
else if (req.url === '/api/browse') {
|
|
242
|
+
// Respond immediately so we don't block the HTTP connection
|
|
243
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
244
|
+
res.end('ok');
|
|
233
245
|
if (browseInProgress) {
|
|
234
246
|
console.log(' [browse] Dialog already open, ignoring');
|
|
235
247
|
} else {
|
|
236
248
|
browseInProgress = true;
|
|
237
249
|
console.log(' [browse] Opening file dialog...');
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
250
|
+
try {
|
|
251
|
+
const fp = await openFileDialog();
|
|
252
|
+
browseInProgress = false;
|
|
253
|
+
if (fp) {
|
|
242
254
|
const s = fs.statSync(fp);
|
|
243
255
|
const file = { id: String(idCounter++), path: fp, name: path.basename(fp), size: s.size };
|
|
244
256
|
sharedFiles.push(file);
|
|
245
257
|
console.log(` [browse] Added: ${file.name} (${formatSize(file.size)})`);
|
|
246
|
-
}
|
|
247
|
-
console.
|
|
258
|
+
} else {
|
|
259
|
+
console.log(' [browse] Cancelled by user');
|
|
248
260
|
}
|
|
249
|
-
}
|
|
250
|
-
|
|
261
|
+
} catch (e) {
|
|
262
|
+
browseInProgress = false;
|
|
263
|
+
console.error(` [browse] Error: ${e.message}`);
|
|
251
264
|
}
|
|
265
|
+
broadcast();
|
|
252
266
|
}
|
|
267
|
+
return; // already responded
|
|
253
268
|
}
|
|
254
269
|
else if (req.url === '/api/remove-file') {
|
|
255
270
|
const idx = sharedFiles.findIndex(f => f.id === data.fileId);
|
|
256
271
|
if (idx !== -1) {
|
|
257
272
|
console.log(` [files] Removed: ${sharedFiles[idx].name}`);
|
|
258
273
|
sharedFiles.splice(idx, 1);
|
|
274
|
+
} else {
|
|
275
|
+
console.log(` [files] Remove failed — fileId not found: ${data.fileId}`);
|
|
259
276
|
}
|
|
260
277
|
}
|
|
261
278
|
else if (req.url === '/api/send') {
|
|
@@ -263,9 +280,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
263
280
|
const file = sharedFiles.find(f => f.id === data.fileId);
|
|
264
281
|
if (peer && file) {
|
|
265
282
|
console.log(` [send] ${file.name} -> ${peer.device_name} (${peer.ip})`);
|
|
266
|
-
sendFile(peer, { ...file });
|
|
283
|
+
sendFile(peer, { ...file }).catch(e => console.error(` [send] Unhandled: ${e.message}`));
|
|
267
284
|
} else {
|
|
268
|
-
console.log(` [send] Failed — peer: ${!!peer}, file: ${!!file}`);
|
|
285
|
+
console.log(` [send] Failed — peer: ${!!peer} (${data.peerId}), file: ${!!file} (${data.fileId})`);
|
|
269
286
|
}
|
|
270
287
|
}
|
|
271
288
|
else if (req.url === '/api/rename') {
|
|
@@ -275,14 +292,62 @@ const server = http.createServer(async (req, res) => {
|
|
|
275
292
|
config.deviceName = newName;
|
|
276
293
|
saveConfig(config);
|
|
277
294
|
console.log(` [rename] Device name set to: ${deviceName}`);
|
|
295
|
+
} else {
|
|
296
|
+
console.log(` [rename] Invalid name: "${data.name}"`);
|
|
278
297
|
}
|
|
279
298
|
}
|
|
299
|
+
else if (req.url === '/api/set-download-dir') {
|
|
300
|
+
// Respond immediately so we don't block the HTTP connection
|
|
301
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
302
|
+
res.end('ok');
|
|
303
|
+
console.log(' [config] Opening folder dialog...');
|
|
304
|
+
try {
|
|
305
|
+
const dir = await openFolderDialog();
|
|
306
|
+
if (dir) {
|
|
307
|
+
downloadDir = dir;
|
|
308
|
+
config.downloadDir = dir;
|
|
309
|
+
saveConfig(config);
|
|
310
|
+
console.log(` [config] Download dir set to: ${downloadDir}`);
|
|
311
|
+
} else {
|
|
312
|
+
console.log(' [config] Folder dialog cancelled');
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
console.error(` [config] Folder dialog error: ${e.message}`);
|
|
316
|
+
}
|
|
317
|
+
broadcast();
|
|
318
|
+
return; // already responded
|
|
319
|
+
}
|
|
280
320
|
else if (req.url === '/api/respond') {
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
321
|
+
console.log(` [respond] Received: accepted=${data.accepted}`);
|
|
322
|
+
if (pendingRequest) {
|
|
323
|
+
pendingRequest.resolve(!!data.accepted);
|
|
324
|
+
console.log(` [respond] ${data.accepted ? 'Accepted' : 'Rejected'}: ${pendingRequest.filename}`);
|
|
325
|
+
pendingRequest = null;
|
|
326
|
+
} else {
|
|
327
|
+
console.log(` [respond] WARNING: No pending request`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (req.url === '/api/open-folder') {
|
|
331
|
+
const transferId = data.transferId;
|
|
332
|
+
const t = transfers.find(tr => tr.id === transferId);
|
|
333
|
+
if (t && t.savePath) {
|
|
334
|
+
console.log(` [open] Opening folder for: ${t.savePath}`);
|
|
335
|
+
if (fs.existsSync(t.savePath)) {
|
|
336
|
+
if (process.platform === 'win32') exec(`explorer /select,"${t.savePath}"`);
|
|
337
|
+
else if (process.platform === 'darwin') exec(`open -R "${t.savePath}"`);
|
|
338
|
+
else exec(`xdg-open "${path.dirname(t.savePath)}"`);
|
|
339
|
+
} else {
|
|
340
|
+
console.log(` [open] File no longer exists: ${t.savePath}`);
|
|
341
|
+
// Open the directory instead
|
|
342
|
+
const dir = path.dirname(t.savePath);
|
|
343
|
+
if (fs.existsSync(dir)) {
|
|
344
|
+
if (process.platform === 'win32') exec(`explorer "${dir}"`);
|
|
345
|
+
else if (process.platform === 'darwin') exec(`open "${dir}"`);
|
|
346
|
+
else exec(`xdg-open "${dir}"`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
console.log(` [open] Failed — transferId: ${transferId}, found: ${!!t}, savePath: ${t ? t.savePath : 'N/A'}`);
|
|
286
351
|
}
|
|
287
352
|
}
|
|
288
353
|
else if (req.url === '/api/shutdown') {
|
|
@@ -292,6 +357,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
292
357
|
setTimeout(() => process.exit(0), 200);
|
|
293
358
|
return;
|
|
294
359
|
}
|
|
360
|
+
else {
|
|
361
|
+
console.log(` [http] Unknown POST endpoint: ${req.url}`);
|
|
362
|
+
}
|
|
295
363
|
|
|
296
364
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
297
365
|
res.end('ok');
|
|
@@ -322,6 +390,22 @@ function openFileDialog() {
|
|
|
322
390
|
});
|
|
323
391
|
}
|
|
324
392
|
|
|
393
|
+
function openFolderDialog() {
|
|
394
|
+
return new Promise(resolve => {
|
|
395
|
+
let cmd;
|
|
396
|
+
if (process.platform === 'win32') {
|
|
397
|
+
cmd = 'powershell -sta -command "Add-Type -AssemblyName System.Windows.Forms; $f = New-Object System.Windows.Forms.FolderBrowserDialog; $f.Description = \'Select download folder\'; if($f.ShowDialog() -eq \'OK\'){Write-Output $f.SelectedPath}"';
|
|
398
|
+
} else if (process.platform === 'darwin') {
|
|
399
|
+
cmd = 'osascript -e \'POSIX path of (choose folder with prompt "Select download folder")\'';
|
|
400
|
+
} else {
|
|
401
|
+
cmd = 'zenity --file-selection --directory --title="Select download folder" 2>/dev/null || kdialog --getexistingdirectory . 2>/dev/null';
|
|
402
|
+
}
|
|
403
|
+
exec(cmd, { encoding: 'utf-8', windowsHide: true, timeout: 120_000 }, (err, stdout) => {
|
|
404
|
+
resolve(err ? null : (stdout.trim() || null));
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
325
409
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
326
410
|
// UDP discovery
|
|
327
411
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -389,33 +473,41 @@ tcpServer.on('error', err => {
|
|
|
389
473
|
tcpServer.listen(TRANSFER_PORT);
|
|
390
474
|
|
|
391
475
|
async function handleIncoming(socket) {
|
|
476
|
+
const remoteAddr = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
477
|
+
console.log(` [recv] TCP connection from ${remoteAddr}`);
|
|
478
|
+
|
|
392
479
|
// Read header length (8 bytes, little-endian u64)
|
|
393
480
|
const lenBuf = await readExact(socket, 8);
|
|
394
481
|
const headerLen = Number(lenBuf.readBigUInt64LE(0));
|
|
482
|
+
console.log(` [recv] Header length: ${headerLen} bytes`);
|
|
395
483
|
|
|
396
484
|
// Read header JSON
|
|
397
485
|
const headerBuf = await readExact(socket, headerLen);
|
|
398
486
|
const header = JSON.parse(headerBuf.toString('utf-8'));
|
|
399
487
|
const { filename, size, sender_name } = header;
|
|
400
488
|
|
|
401
|
-
console.log(` Incoming: ${filename} (${formatSize(size)}) from ${sender_name}`);
|
|
489
|
+
console.log(` [recv] Incoming: "${filename}" (${formatSize(size)}) from ${sender_name}`);
|
|
402
490
|
|
|
403
491
|
// Prompt user for accept / reject
|
|
404
|
-
|
|
492
|
+
console.log(` [recv] Waiting for user decision...`);
|
|
493
|
+
|
|
405
494
|
const accepted = await new Promise(resolve => {
|
|
406
|
-
|
|
495
|
+
pendingRequest = { filename, size, senderName: sender_name, resolve };
|
|
407
496
|
broadcast();
|
|
408
497
|
|
|
409
498
|
// Auto-reject if sender disconnects while waiting
|
|
410
499
|
socket.once('close', () => {
|
|
411
|
-
if (
|
|
412
|
-
|
|
500
|
+
if (pendingRequest && pendingRequest.resolve === resolve) {
|
|
501
|
+
console.log(` [recv] Sender disconnected while waiting, auto-rejecting`);
|
|
502
|
+
pendingRequest = null;
|
|
413
503
|
resolve(false);
|
|
414
504
|
broadcast();
|
|
415
505
|
}
|
|
416
506
|
});
|
|
417
507
|
});
|
|
418
508
|
|
|
509
|
+
console.log(` [recv] User decision: ${accepted ? 'ACCEPTED' : 'REJECTED'}`);
|
|
510
|
+
|
|
419
511
|
// Send decision byte
|
|
420
512
|
socket.write(Buffer.from([accepted ? 1 : 0]));
|
|
421
513
|
if (!accepted) { socket.end(); return; }
|
|
@@ -430,8 +522,8 @@ async function handleIncoming(socket) {
|
|
|
430
522
|
transfers.push(t);
|
|
431
523
|
broadcast();
|
|
432
524
|
|
|
433
|
-
// Unique save path in
|
|
434
|
-
const dl =
|
|
525
|
+
// Unique save path in download directory
|
|
526
|
+
const dl = downloadDir;
|
|
435
527
|
if (!fs.existsSync(dl)) fs.mkdirSync(dl, { recursive: true });
|
|
436
528
|
let savePath = path.join(dl, filename);
|
|
437
529
|
let counter = 1;
|
|
@@ -441,6 +533,9 @@ async function handleIncoming(socket) {
|
|
|
441
533
|
savePath = path.join(dl, `${base} (${counter++})${ext}`);
|
|
442
534
|
}
|
|
443
535
|
|
|
536
|
+
t.savePath = savePath;
|
|
537
|
+
console.log(` [recv] Saving to: ${savePath}`);
|
|
538
|
+
|
|
444
539
|
const ws = fs.createWriteStream(savePath);
|
|
445
540
|
let received = 0;
|
|
446
541
|
let lastBc = 0;
|
|
@@ -467,7 +562,7 @@ async function handleIncoming(socket) {
|
|
|
467
562
|
t.status = 'completed';
|
|
468
563
|
t.progress = 100;
|
|
469
564
|
broadcast();
|
|
470
|
-
console.log(`
|
|
565
|
+
console.log(` [recv] Complete: ${savePath} (${formatSize(received)})`);
|
|
471
566
|
resolve();
|
|
472
567
|
});
|
|
473
568
|
socket.on('error', err => {
|
|
@@ -475,6 +570,7 @@ async function handleIncoming(socket) {
|
|
|
475
570
|
t.status = 'failed';
|
|
476
571
|
t.error = err.message;
|
|
477
572
|
broadcast();
|
|
573
|
+
console.error(` [recv] Socket error: ${err.message}`);
|
|
478
574
|
reject(err);
|
|
479
575
|
});
|
|
480
576
|
});
|