@balaji003/lantransfer 1.0.5 → 1.0.6
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 +70 -0
- package/package.json +3 -2
- package/public/index.html +109 -20
- package/server.js +79 -17
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# lantransfer
|
|
2
|
+
|
|
3
|
+
Peer-to-peer file sharing over your local network. Zero dependencies. Works on Windows, macOS, and Linux.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install -g @balaji003/lantransfer
|
|
7
|
+
lantransfer
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
A browser window opens automatically — select a file, pick a nearby device, and send.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Zero dependencies** — uses only Node.js built-in modules
|
|
15
|
+
- **Auto-discovery** — finds devices on your LAN automatically via HTTP subnet scanning (no firewall rules needed) + UDP broadcast fallback
|
|
16
|
+
- **Browser UI** — clean dark-themed interface, no extra apps to install
|
|
17
|
+
- **Cross-platform** — Windows, macOS, Linux
|
|
18
|
+
- **Accept/Reject prompt** — incoming files must be approved before transfer
|
|
19
|
+
- **Progress tracking** — real-time speed and progress for all transfers
|
|
20
|
+
- **XOR encrypted** — file data is obfuscated during transfer
|
|
21
|
+
- **Native file picker** — uses your OS file dialog (PowerShell on Windows, osascript on macOS, zenity/kdialog on Linux)
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Install globally and run
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g @balaji003/lantransfer
|
|
29
|
+
lantransfer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Or run directly with npx
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @balaji003/lantransfer
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Or clone and run
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/user/lantransfer.git
|
|
42
|
+
cd lantransfer
|
|
43
|
+
node server.js
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
1. Run `lantransfer` on two (or more) devices connected to the same WiFi/LAN
|
|
49
|
+
2. Each device scans the local subnet to find other instances — no firewall configuration needed
|
|
50
|
+
3. Devices appear in the sidebar under "Nearby Devices"
|
|
51
|
+
4. Click **Browse** to select a file, select a device, and click **Send**
|
|
52
|
+
5. The receiver sees a popup to **Accept** or **Reject**
|
|
53
|
+
6. Accepted files are saved to the `Downloads` folder
|
|
54
|
+
|
|
55
|
+
## Ports
|
|
56
|
+
|
|
57
|
+
| Port | Protocol | Purpose |
|
|
58
|
+
|-------|----------|----------------------|
|
|
59
|
+
| 3000 | HTTP | Web UI + discovery |
|
|
60
|
+
| 34254 | UDP | Broadcast discovery (fallback) |
|
|
61
|
+
| 34255 | TCP | File transfer |
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Node.js >= 16.0.0
|
|
66
|
+
- Both devices on the same local network
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@balaji003/lantransfer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "LAN File Transfer — peer-to-peer file sharing over local network. Zero dependencies.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"server.js",
|
|
11
11
|
"public/",
|
|
12
|
-
"bin/"
|
|
12
|
+
"bin/",
|
|
13
|
+
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"start": "node server.js"
|
package/public/index.html
CHANGED
|
@@ -47,7 +47,14 @@
|
|
|
47
47
|
}
|
|
48
48
|
.header-left { display: flex; align-items: center; gap: 16px; }
|
|
49
49
|
.logo { font-size: 20px; font-weight: 700; color: #8cc8ff; }
|
|
50
|
-
.device-info { color: var(--text-dim); font-size: 13px; }
|
|
50
|
+
.device-info { color: var(--text-dim); font-size: 13px; display: flex; align-items: center; gap: 6px; }
|
|
51
|
+
.device-name-edit {
|
|
52
|
+
background: transparent; border: 1px solid transparent; color: var(--text);
|
|
53
|
+
font-size: 13px; font-family: inherit; padding: 2px 6px; border-radius: 4px;
|
|
54
|
+
width: 160px; outline: none; transition: border-color 0.2s;
|
|
55
|
+
}
|
|
56
|
+
.device-name-edit:hover { border-color: var(--border); }
|
|
57
|
+
.device-name-edit:focus { border-color: var(--accent); background: var(--surface-hover); }
|
|
51
58
|
|
|
52
59
|
.toggle {
|
|
53
60
|
display: flex; align-items: center; gap: 8px;
|
|
@@ -135,6 +142,22 @@
|
|
|
135
142
|
.file-info { color: var(--text-dim); font-size: 13px; }
|
|
136
143
|
.file-info.has-file { color: var(--text); font-weight: 500; }
|
|
137
144
|
|
|
145
|
+
.file-list { margin-top: 12px; }
|
|
146
|
+
.file-item {
|
|
147
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
148
|
+
padding: 8px 12px; background: var(--surface); border-radius: var(--radius);
|
|
149
|
+
margin-bottom: 4px; font-size: 13px;
|
|
150
|
+
}
|
|
151
|
+
.file-item-left { display: flex; align-items: center; gap: 8px; overflow: hidden; }
|
|
152
|
+
.file-item-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
153
|
+
.file-item-size { color: var(--text-dim); white-space: nowrap; }
|
|
154
|
+
.file-item-radio { accent-color: var(--accent); cursor: pointer; }
|
|
155
|
+
.file-item-remove {
|
|
156
|
+
background: none; border: none; color: var(--text-dim); cursor: pointer;
|
|
157
|
+
font-size: 16px; padding: 0 4px; line-height: 1; transition: color 0.12s;
|
|
158
|
+
}
|
|
159
|
+
.file-item-remove:hover { color: var(--red); }
|
|
160
|
+
|
|
138
161
|
/* ── Transfers ── */
|
|
139
162
|
.transfers-section { flex: 1; overflow-y: auto; padding: 16px 24px; }
|
|
140
163
|
.transfer {
|
|
@@ -216,7 +239,11 @@
|
|
|
216
239
|
<div class="header">
|
|
217
240
|
<div class="header-left">
|
|
218
241
|
<div class="logo">⇌ LAN Transfer</div>
|
|
219
|
-
<div class="device-info"
|
|
242
|
+
<div class="device-info">
|
|
243
|
+
<input class="device-name-edit" id="device-name" type="text" maxlength="50" title="Click to edit device name" />
|
|
244
|
+
<span style="color:var(--text-dim)">·</span>
|
|
245
|
+
<span id="local-ip"></span>
|
|
246
|
+
</div>
|
|
220
247
|
</div>
|
|
221
248
|
<div class="header-right">
|
|
222
249
|
<div class="toggle" id="toggle" onclick="api('toggle')">
|
|
@@ -243,9 +270,10 @@
|
|
|
243
270
|
<div class="send-panel">
|
|
244
271
|
<div class="send-title">Send File</div>
|
|
245
272
|
<div class="send-row">
|
|
246
|
-
<button class="btn btn-browse" onclick="
|
|
247
|
-
<span class="file-info" id="file-info">No
|
|
273
|
+
<button class="btn btn-browse" id="browse-btn" onclick="doBrowse()">Browse</button>
|
|
274
|
+
<span class="file-info" id="file-info">No files added</span>
|
|
248
275
|
</div>
|
|
276
|
+
<div class="file-list" id="file-list"></div>
|
|
249
277
|
<div class="send-row" style="margin-top: 12px;">
|
|
250
278
|
<button class="btn btn-send" id="send-btn" onclick="doSend()" disabled>Send</button>
|
|
251
279
|
</div>
|
|
@@ -273,6 +301,8 @@
|
|
|
273
301
|
// ──────────────────────────────────────────────────────────────────────────
|
|
274
302
|
let state = {};
|
|
275
303
|
let selectedPeerId = null;
|
|
304
|
+
let selectedFileId = null;
|
|
305
|
+
let browsing = false;
|
|
276
306
|
|
|
277
307
|
// ──────────────────────────────────────────────────────────────────────────
|
|
278
308
|
// SSE connection
|
|
@@ -282,6 +312,10 @@
|
|
|
282
312
|
evtSource = new EventSource('/events');
|
|
283
313
|
evtSource.onmessage = e => {
|
|
284
314
|
state = JSON.parse(e.data);
|
|
315
|
+
// Auto-select the latest file if none selected
|
|
316
|
+
if (!selectedFileId && state.sharedFiles && state.sharedFiles.length > 0) {
|
|
317
|
+
selectedFileId = state.sharedFiles[state.sharedFiles.length - 1].id;
|
|
318
|
+
}
|
|
285
319
|
render();
|
|
286
320
|
};
|
|
287
321
|
evtSource.onopen = () => {
|
|
@@ -297,7 +331,7 @@
|
|
|
297
331
|
// API
|
|
298
332
|
// ──────────────────────────────────────────────────────────────────────────
|
|
299
333
|
function api(endpoint, data) {
|
|
300
|
-
fetch('/api/' + endpoint, {
|
|
334
|
+
return fetch('/api/' + endpoint, {
|
|
301
335
|
method: 'POST',
|
|
302
336
|
headers: { 'Content-Type': 'application/json' },
|
|
303
337
|
body: JSON.stringify(data || {}),
|
|
@@ -309,9 +343,28 @@
|
|
|
309
343
|
render();
|
|
310
344
|
}
|
|
311
345
|
|
|
346
|
+
function selectFile(id) {
|
|
347
|
+
selectedFileId = id;
|
|
348
|
+
render();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function removeFile(id) {
|
|
352
|
+
api('remove-file', { fileId: id });
|
|
353
|
+
if (selectedFileId === id) selectedFileId = null;
|
|
354
|
+
}
|
|
355
|
+
|
|
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';
|
|
363
|
+
}
|
|
364
|
+
|
|
312
365
|
function doSend() {
|
|
313
|
-
if (selectedPeerId &&
|
|
314
|
-
api('send', { peerId: selectedPeerId });
|
|
366
|
+
if (selectedPeerId && selectedFileId) {
|
|
367
|
+
api('send', { peerId: selectedPeerId, fileId: selectedFileId });
|
|
315
368
|
}
|
|
316
369
|
}
|
|
317
370
|
|
|
@@ -326,13 +379,29 @@
|
|
|
326
379
|
}
|
|
327
380
|
}
|
|
328
381
|
|
|
382
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
383
|
+
// Device name editing
|
|
384
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
385
|
+
const nameInput = document.getElementById('device-name');
|
|
386
|
+
let nameLoaded = false;
|
|
387
|
+
nameInput.addEventListener('change', () => {
|
|
388
|
+
const val = nameInput.value.trim();
|
|
389
|
+
if (val) api('rename', { name: val });
|
|
390
|
+
});
|
|
391
|
+
nameInput.addEventListener('keydown', e => {
|
|
392
|
+
if (e.key === 'Enter') nameInput.blur();
|
|
393
|
+
});
|
|
394
|
+
|
|
329
395
|
// ──────────────────────────────────────────────────────────────────────────
|
|
330
396
|
// Render
|
|
331
397
|
// ──────────────────────────────────────────────────────────────────────────
|
|
332
398
|
function render() {
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
399
|
+
// Device name (only set once to avoid clobbering user edits)
|
|
400
|
+
if (!nameLoaded && state.deviceName) {
|
|
401
|
+
nameInput.value = state.deviceName;
|
|
402
|
+
nameLoaded = true;
|
|
403
|
+
}
|
|
404
|
+
document.getElementById('local-ip').textContent = state.localIP || '';
|
|
336
405
|
|
|
337
406
|
// Toggle
|
|
338
407
|
const toggle = document.getElementById('toggle');
|
|
@@ -350,7 +419,6 @@
|
|
|
350
419
|
if (!state.peers || state.peers.length === 0) {
|
|
351
420
|
peersEl.innerHTML = '<div class="empty-msg">Searching for nearby devices…<br><br><small>Make sure both devices are on the same WiFi</small></div>';
|
|
352
421
|
} else {
|
|
353
|
-
// Validate selection still exists
|
|
354
422
|
if (selectedPeerId && !state.peers.some(p => p.id === selectedPeerId)) {
|
|
355
423
|
selectedPeerId = null;
|
|
356
424
|
}
|
|
@@ -362,26 +430,47 @@
|
|
|
362
430
|
).join('');
|
|
363
431
|
}
|
|
364
432
|
|
|
365
|
-
//
|
|
433
|
+
// Shared files
|
|
434
|
+
const files = state.sharedFiles || [];
|
|
366
435
|
const fileInfoEl = document.getElementById('file-info');
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
} else {
|
|
436
|
+
const fileListEl = document.getElementById('file-list');
|
|
437
|
+
|
|
438
|
+
if (files.length === 0) {
|
|
371
439
|
fileInfoEl.className = 'file-info';
|
|
372
|
-
fileInfoEl.textContent = 'No
|
|
440
|
+
fileInfoEl.textContent = 'No files added';
|
|
441
|
+
fileListEl.innerHTML = '';
|
|
442
|
+
selectedFileId = null;
|
|
443
|
+
} else {
|
|
444
|
+
fileInfoEl.className = 'file-info has-file';
|
|
445
|
+
fileInfoEl.textContent = files.length + ' file' + (files.length > 1 ? 's' : '') + ' ready to send';
|
|
446
|
+
// Validate selection
|
|
447
|
+
if (selectedFileId && !files.some(f => f.id === selectedFileId)) {
|
|
448
|
+
selectedFileId = files[files.length - 1].id;
|
|
449
|
+
}
|
|
450
|
+
fileListEl.innerHTML = files.map(f =>
|
|
451
|
+
'<div class="file-item">'
|
|
452
|
+
+ '<div class="file-item-left">'
|
|
453
|
+
+ '<input type="radio" name="sel-file" class="file-item-radio" '
|
|
454
|
+
+ (selectedFileId === f.id ? 'checked ' : '')
|
|
455
|
+
+ 'onchange="selectFile(\'' + esc(f.id) + '\')" />'
|
|
456
|
+
+ '<span class="file-item-name">' + esc(f.name) + '</span>'
|
|
457
|
+
+ '<span class="file-item-size">' + formatSize(f.size) + '</span>'
|
|
458
|
+
+ '</div>'
|
|
459
|
+
+ '<button class="file-item-remove" onclick="removeFile(\'' + esc(f.id) + '\')" title="Remove">×</button>'
|
|
460
|
+
+ '</div>'
|
|
461
|
+
).join('');
|
|
373
462
|
}
|
|
374
463
|
|
|
375
464
|
// Send button
|
|
376
465
|
const sendBtn = document.getElementById('send-btn');
|
|
377
|
-
const canSend = !!(selectedPeerId &&
|
|
466
|
+
const canSend = !!(selectedPeerId && selectedFileId && state.peers && state.peers.some(p => p.id === selectedPeerId));
|
|
378
467
|
sendBtn.disabled = !canSend;
|
|
379
468
|
if (canSend) {
|
|
380
469
|
const peer = state.peers.find(p => p.id === selectedPeerId);
|
|
381
470
|
sendBtn.textContent = 'Send to ' + (peer ? peer.name : 'peer');
|
|
382
|
-
} else if (selectedPeerId && !
|
|
471
|
+
} else if (selectedPeerId && !selectedFileId) {
|
|
383
472
|
sendBtn.textContent = 'Select a file first';
|
|
384
|
-
} else if (!selectedPeerId &&
|
|
473
|
+
} else if (!selectedPeerId && selectedFileId) {
|
|
385
474
|
sendBtn.textContent = 'Select a device first';
|
|
386
475
|
} else {
|
|
387
476
|
sendBtn.textContent = 'Send';
|
package/server.js
CHANGED
|
@@ -31,16 +31,39 @@ const XOR_KEY = Buffer.from('LAN-XFER-KEY-2024');
|
|
|
31
31
|
const PEER_TIMEOUT = 10_000; // ms
|
|
32
32
|
const BROADCAST_INTERVAL = 3_000; // ms
|
|
33
33
|
|
|
34
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Persistent config (device name)
|
|
36
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
const CONFIG_PATH = path.join(os.homedir(), '.lantransfer.json');
|
|
38
|
+
|
|
39
|
+
function loadConfig() {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
42
|
+
} catch { return {}; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function saveConfig(cfg) {
|
|
46
|
+
try {
|
|
47
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
48
|
+
console.log(` [config] Saved to ${CONFIG_PATH}`);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(` [config] Failed to save: ${e.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const config = loadConfig();
|
|
55
|
+
|
|
34
56
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
35
57
|
// Application state
|
|
36
58
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
37
59
|
let isDiscoverable = true;
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
let deviceName = config.deviceName || os.hostname();
|
|
61
|
+
const sharedFiles = []; // [ { id, path, name, size } ]
|
|
40
62
|
const peers = new Map(); // id → { device_name, ip, tcp_port, last_seen }
|
|
41
63
|
const transfers = []; // [ TransferEntry ]
|
|
42
64
|
const pendingRequests = new Map(); // id → { filename, size, senderName, resolve }
|
|
43
65
|
let idCounter = 0;
|
|
66
|
+
let browseInProgress = false;
|
|
44
67
|
|
|
45
68
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
46
69
|
// SSE (Server-Sent Events) clients
|
|
@@ -80,9 +103,8 @@ function serializeState() {
|
|
|
80
103
|
peers: [...peers.entries()].map(([id, p]) => ({
|
|
81
104
|
id, name: p.device_name, ip: p.ip,
|
|
82
105
|
})),
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
: null,
|
|
106
|
+
sharedFiles: sharedFiles.map(f => ({ id: f.id, name: f.name, size: f.size })),
|
|
107
|
+
selectedFileId: sharedFiles.length > 0 ? sharedFiles[sharedFiles.length - 1].id : null,
|
|
86
108
|
transfers: transfers.map(t => ({
|
|
87
109
|
id: t.id, filename: t.filename, size: t.size,
|
|
88
110
|
direction: t.direction, peerName: t.peerName,
|
|
@@ -167,6 +189,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
167
189
|
|
|
168
190
|
// ── Ping endpoint for HTTP-based discovery ──
|
|
169
191
|
if (req.method === 'GET' && req.url === '/api/ping') {
|
|
192
|
+
if (!isDiscoverable) {
|
|
193
|
+
res.writeHead(403);
|
|
194
|
+
res.end('Hidden');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
170
197
|
res.writeHead(200, {
|
|
171
198
|
'Content-Type': 'application/json',
|
|
172
199
|
'Access-Control-Allow-Origin': '*',
|
|
@@ -200,20 +227,54 @@ const server = http.createServer(async (req, res) => {
|
|
|
200
227
|
|
|
201
228
|
if (req.url === '/api/toggle') {
|
|
202
229
|
isDiscoverable = !isDiscoverable;
|
|
230
|
+
console.log(` [toggle] Discoverable: ${isDiscoverable}`);
|
|
203
231
|
}
|
|
204
232
|
else if (req.url === '/api/browse') {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
233
|
+
if (browseInProgress) {
|
|
234
|
+
console.log(' [browse] Dialog already open, ignoring');
|
|
235
|
+
} else {
|
|
236
|
+
browseInProgress = true;
|
|
237
|
+
console.log(' [browse] Opening file dialog...');
|
|
238
|
+
const fp = await openFileDialog();
|
|
239
|
+
browseInProgress = false;
|
|
240
|
+
if (fp) {
|
|
241
|
+
try {
|
|
242
|
+
const s = fs.statSync(fp);
|
|
243
|
+
const file = { id: String(idCounter++), path: fp, name: path.basename(fp), size: s.size };
|
|
244
|
+
sharedFiles.push(file);
|
|
245
|
+
console.log(` [browse] Added: ${file.name} (${formatSize(file.size)})`);
|
|
246
|
+
} catch (e) {
|
|
247
|
+
console.error(` [browse] Error reading file: ${e.message}`);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
console.log(' [browse] Cancelled');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else if (req.url === '/api/remove-file') {
|
|
255
|
+
const idx = sharedFiles.findIndex(f => f.id === data.fileId);
|
|
256
|
+
if (idx !== -1) {
|
|
257
|
+
console.log(` [files] Removed: ${sharedFiles[idx].name}`);
|
|
258
|
+
sharedFiles.splice(idx, 1);
|
|
211
259
|
}
|
|
212
260
|
}
|
|
213
261
|
else if (req.url === '/api/send') {
|
|
214
262
|
const peer = peers.get(data.peerId);
|
|
215
|
-
|
|
216
|
-
|
|
263
|
+
const file = sharedFiles.find(f => f.id === data.fileId);
|
|
264
|
+
if (peer && file) {
|
|
265
|
+
console.log(` [send] ${file.name} -> ${peer.device_name} (${peer.ip})`);
|
|
266
|
+
sendFile(peer, { ...file });
|
|
267
|
+
} else {
|
|
268
|
+
console.log(` [send] Failed — peer: ${!!peer}, file: ${!!file}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (req.url === '/api/rename') {
|
|
272
|
+
const newName = (data.name || '').trim();
|
|
273
|
+
if (newName && newName.length <= 50) {
|
|
274
|
+
deviceName = newName;
|
|
275
|
+
config.deviceName = newName;
|
|
276
|
+
saveConfig(config);
|
|
277
|
+
console.log(` [rename] Device name set to: ${deviceName}`);
|
|
217
278
|
}
|
|
218
279
|
}
|
|
219
280
|
else if (req.url === '/api/respond') {
|
|
@@ -221,6 +282,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
221
282
|
if (pending) {
|
|
222
283
|
pending.resolve(!!data.accepted);
|
|
223
284
|
pendingRequests.delete(data.requestId);
|
|
285
|
+
console.log(` [respond] ${data.accepted ? 'Accepted' : 'Rejected'} transfer ${data.requestId}`);
|
|
224
286
|
}
|
|
225
287
|
}
|
|
226
288
|
else if (req.url === '/api/shutdown') {
|
|
@@ -267,7 +329,6 @@ const udp = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
|
267
329
|
|
|
268
330
|
udp.on('listening', () => {
|
|
269
331
|
udp.setBroadcast(true);
|
|
270
|
-
console.log(` Discovery UDP :${DISCOVERY_PORT}`);
|
|
271
332
|
});
|
|
272
333
|
|
|
273
334
|
udp.on('message', (msg, rinfo) => {
|
|
@@ -325,9 +386,7 @@ tcpServer.on('error', err => {
|
|
|
325
386
|
console.error(' Is another instance running? (port conflict on TCP ' + TRANSFER_PORT + ')');
|
|
326
387
|
});
|
|
327
388
|
|
|
328
|
-
tcpServer.listen(TRANSFER_PORT
|
|
329
|
-
console.log(` Transfer TCP :${TRANSFER_PORT}`);
|
|
330
|
-
});
|
|
389
|
+
tcpServer.listen(TRANSFER_PORT);
|
|
331
390
|
|
|
332
391
|
async function handleIncoming(socket) {
|
|
333
392
|
// Read header length (8 bytes, little-endian u64)
|
|
@@ -612,6 +671,9 @@ server.listen(HTTP_PORT, () => {
|
|
|
612
671
|
console.log(' ⇄ LAN File Transfer');
|
|
613
672
|
console.log(` Device: ${deviceName}`);
|
|
614
673
|
console.log(` Local IP: ${ip}`);
|
|
674
|
+
console.log(` Discovery HTTP scan (no firewall needed)`);
|
|
675
|
+
console.log(` Discovery UDP :${DISCOVERY_PORT} (fallback)`);
|
|
676
|
+
console.log(` Transfer TCP :${TRANSFER_PORT}`);
|
|
615
677
|
console.log(` UI: http://localhost:${HTTP_PORT}`);
|
|
616
678
|
console.log('');
|
|
617
679
|
|