@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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/public/index.html +73 -14
  3. package/server.js +124 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balaji003/lantransfer",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "LAN File Transfer — peer-to-peer file sharing over local network. Zero dependencies.",
5
5
  "main": "server.js",
6
6
  "bin": {
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
- async function doBrowse() {
357
- if (browsing) return;
358
- browsing = true;
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(requestId, accepted) {
372
- api('respond', { requestId: requestId, accepted: accepted });
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.incomingRequests && state.incomingRequests.length > 0) {
491
- const req = state.incomingRequests[0];
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(\'' + esc(req.id) + '\', true)">Accept</button>'
500
- + '<button class="btn-reject" onclick="respondTransfer(\'' + esc(req.id) + '\', false)">Reject</button>'
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
- const pendingRequests = new Map(); // id → { filename, size, senderName, resolve }
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
- incomingRequests: [...pendingRequests.entries()].map(([id, r]) => ({
115
- id, filename: r.filename, size: r.size, senderName: r.senderName,
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
- req.on('close', () => sseClients.delete(res));
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
- const fp = await openFileDialog();
239
- browseInProgress = false;
240
- if (fp) {
241
- try {
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
- } catch (e) {
247
- console.error(` [browse] Error reading file: ${e.message}`);
258
+ } else {
259
+ console.log(' [browse] Cancelled by user');
248
260
  }
249
- } else {
250
- console.log(' [browse] Cancelled');
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
- const pending = pendingRequests.get(data.requestId);
282
- if (pending) {
283
- pending.resolve(!!data.accepted);
284
- pendingRequests.delete(data.requestId);
285
- console.log(` [respond] ${data.accepted ? 'Accepted' : 'Rejected'} transfer ${data.requestId}`);
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
- const reqId = String(idCounter++);
492
+ console.log(` [recv] Waiting for user decision...`);
493
+
405
494
  const accepted = await new Promise(resolve => {
406
- pendingRequests.set(reqId, { filename, size, senderName: sender_name, resolve });
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 (pendingRequests.has(reqId)) {
412
- pendingRequests.delete(reqId);
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 Downloads
434
- const dl = path.join(os.homedir(), 'Downloads');
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(` Saved: ${savePath}`);
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
  });