landrop 1.0.0
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/landrop.js +399 -0
- package/package.json +31 -0
package/landrop.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
import{join as W,basename as N,resolve as w}from"path";import{networkInterfaces as C}from"os";import{existsSync as Y,readdirSync as z,statSync as Z}from"fs";var Q=process.cwd(),V=8080;function k(E){if(E===0)return"0 B";let H=["B","KB","MB","GB"],F=Math.floor(Math.log(E)/Math.log(1024));return`${(E/1024**F).toFixed(1)} ${H[F]}`}function g(){let E=[],H=C();for(let F of Object.values(H))for(let G of F??[])if(G.family==="IPv4"&&!G.internal)E.push(G.address);return E}function B(E){let H=N(E),F=W(Q,H);if(!Y(F))return F;let G=H.includes(".")?H.slice(H.lastIndexOf(".")):"",J=G?H.slice(0,-G.length):H,K=2;while(Y(W(Q,`${J} (${K})${G}`)))K++;return W(Q,`${J} (${K})${G}`)}function j(){let E=new Map;for(let H of process.argv.slice(2)){let F=w(H);if(!Y(F)){console.warn(`\u26A0 Not found: ${F}`);continue}let G=Z(F);if(G.isDirectory())for(let J of z(F,{withFileTypes:!0})){if(!J.isFile())continue;let K=W(F,J.name);E.set(J.name,{name:J.name,size:Z(K).size,path:K})}else{let J=N(F);E.set(J,{name:J,size:G.size,path:F})}}return E}var X=j(),v=g(),u=v[0]??"localhost",$=`http://${u}:${V}`,A=`<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title>LANDROP</title>
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
|
11
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" onerror="window.QRCode=null"></script>
|
|
12
|
+
<style>
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #08090b;
|
|
15
|
+
--surface: #0e1014;
|
|
16
|
+
--border: #1a1d24;
|
|
17
|
+
--text: #8a909e;
|
|
18
|
+
--text-hi: #dde2ec;
|
|
19
|
+
--accent: #c8ff00;
|
|
20
|
+
--success: #00cc88;
|
|
21
|
+
--error: #ff3d4a;
|
|
22
|
+
--warn: #ffbb00;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
background: var(--bg)
|
|
29
|
+
radial-gradient(rgba(200,255,0,0.025) 1px, transparent 1px);
|
|
30
|
+
background-size: auto, 28px 28px;
|
|
31
|
+
color: var(--text);
|
|
32
|
+
font-family: 'Share Tech Mono', 'Courier New', monospace;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
padding: 40px 24px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.shell { width: 100%; max-width: 740px; }
|
|
41
|
+
|
|
42
|
+
/* \u2500\u2500 Header \u2500\u2500 */
|
|
43
|
+
.header {
|
|
44
|
+
display: grid;
|
|
45
|
+
grid-template-columns: 1fr auto;
|
|
46
|
+
gap: 24px;
|
|
47
|
+
align-items: start;
|
|
48
|
+
margin-bottom: 32px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.wordmark {
|
|
52
|
+
font-family: 'Bebas Neue', Impact, sans-serif;
|
|
53
|
+
font-size: 3.5rem;
|
|
54
|
+
line-height: 1;
|
|
55
|
+
color: var(--text-hi);
|
|
56
|
+
letter-spacing: 0.04em;
|
|
57
|
+
margin-bottom: 10px;
|
|
58
|
+
}
|
|
59
|
+
.wordmark em { color: var(--accent); font-style: normal; }
|
|
60
|
+
.meta { font-size: 0.65rem; line-height: 2.2; opacity: 0.65; }
|
|
61
|
+
|
|
62
|
+
/* \u2500\u2500 QR \u2500\u2500 */
|
|
63
|
+
.qr-wrap { display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
64
|
+
#qr img, #qr canvas { border: 4px solid var(--surface); display: block; }
|
|
65
|
+
.qr-label { font-size: 0.56rem; color: var(--accent); opacity: 0.6; text-align: center; }
|
|
66
|
+
.qr-missing {
|
|
67
|
+
width: 128px; height: 128px;
|
|
68
|
+
border: 1px dashed var(--border);
|
|
69
|
+
display: flex; align-items: center; justify-content: center;
|
|
70
|
+
font-size: 0.58rem; text-align: center; opacity: 0.3; padding: 8px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* \u2500\u2500 Divider \u2500\u2500 */
|
|
74
|
+
.sep { height: 1px; background: var(--border); margin-bottom: 32px; position: relative; }
|
|
75
|
+
.sep::after { content: ''; position: absolute; inset: 0 auto 0 0; width: 80px; background: var(--accent); }
|
|
76
|
+
|
|
77
|
+
/* \u2500\u2500 Drop zone \u2500\u2500 */
|
|
78
|
+
#drop-zone {
|
|
79
|
+
position: relative;
|
|
80
|
+
border: 1px solid var(--border);
|
|
81
|
+
background: var(--surface);
|
|
82
|
+
padding: 56px 40px;
|
|
83
|
+
text-align: center;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
transition: border-color 0.2s, background 0.2s;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#drop-zone::before, #drop-zone::after,
|
|
89
|
+
.dz-corners::before, .dz-corners::after {
|
|
90
|
+
content: '';
|
|
91
|
+
position: absolute;
|
|
92
|
+
width: 22px; height: 22px;
|
|
93
|
+
border-color: var(--accent);
|
|
94
|
+
border-style: solid;
|
|
95
|
+
transition: width 0.2s, height 0.2s;
|
|
96
|
+
}
|
|
97
|
+
#drop-zone::before { top: -1px; left: -1px; border-width: 2px 0 0 2px; }
|
|
98
|
+
#drop-zone::after { top: -1px; right: -1px; border-width: 2px 2px 0 0; }
|
|
99
|
+
.dz-corners { position: absolute; inset: 0; pointer-events: none; }
|
|
100
|
+
.dz-corners::before { bottom: -1px; left: -1px; border-width: 0 0 2px 2px; }
|
|
101
|
+
.dz-corners::after { bottom: -1px; right: -1px; border-width: 0 2px 2px 0; }
|
|
102
|
+
|
|
103
|
+
#drop-zone:hover::before, #drop-zone:hover::after,
|
|
104
|
+
#drop-zone:hover .dz-corners::before, #drop-zone:hover .dz-corners::after,
|
|
105
|
+
#drop-zone.drag-over::before, #drop-zone.drag-over::after,
|
|
106
|
+
#drop-zone.drag-over .dz-corners::before, #drop-zone.drag-over .dz-corners::after
|
|
107
|
+
{ width: 32px; height: 32px; }
|
|
108
|
+
|
|
109
|
+
#drop-zone:hover, #drop-zone.drag-over {
|
|
110
|
+
border-color: rgba(200,255,0,0.25);
|
|
111
|
+
background: rgba(200,255,0,0.02);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.dz-tag { font-size: 0.6rem; letter-spacing: 0.2em; color: var(--accent); opacity: 0.7; margin-bottom: 14px; }
|
|
115
|
+
.dz-main { font-size: 1.5rem; color: var(--text-hi); margin-bottom: 8px; }
|
|
116
|
+
.dz-sub { font-size: 0.7rem; opacity: 0.55; }
|
|
117
|
+
|
|
118
|
+
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
119
|
+
.cursor {
|
|
120
|
+
display: inline-block;
|
|
121
|
+
width: 9px; height: 1.4rem;
|
|
122
|
+
background: var(--accent);
|
|
123
|
+
vertical-align: middle;
|
|
124
|
+
animation: blink 1s step-end infinite;
|
|
125
|
+
margin-left: 6px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#file-input { display: none; }
|
|
129
|
+
|
|
130
|
+
/* \u2500\u2500 Section headers \u2500\u2500 */
|
|
131
|
+
.section-head {
|
|
132
|
+
display: flex;
|
|
133
|
+
justify-content: space-between;
|
|
134
|
+
padding: 20px 0 8px;
|
|
135
|
+
font-size: 0.6rem;
|
|
136
|
+
letter-spacing: 0.15em;
|
|
137
|
+
text-transform: uppercase;
|
|
138
|
+
opacity: 0.55;
|
|
139
|
+
border-bottom: 1px solid var(--border);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* \u2500\u2500 Upload items \u2500\u2500 */
|
|
143
|
+
.file-item {
|
|
144
|
+
display: grid;
|
|
145
|
+
grid-template-columns: 1fr 80px 90px;
|
|
146
|
+
gap: 0 16px;
|
|
147
|
+
align-items: start;
|
|
148
|
+
padding: 12px 0;
|
|
149
|
+
border-bottom: 1px solid var(--border);
|
|
150
|
+
animation: appear 0.2s ease;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@keyframes appear {
|
|
154
|
+
from { opacity: 0; transform: translateY(-5px); }
|
|
155
|
+
to { opacity: 1; transform: translateY(0); }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.f-info { min-width: 0; }
|
|
159
|
+
.f-name {
|
|
160
|
+
font-size: 0.82rem; color: var(--text-hi);
|
|
161
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
162
|
+
margin-bottom: 5px;
|
|
163
|
+
}
|
|
164
|
+
.progress-bar { height: 2px; background: var(--border); overflow: hidden; margin-bottom: 4px; }
|
|
165
|
+
.progress-fill { height: 100%; background: var(--accent); transition: width 0.25s linear; }
|
|
166
|
+
.f-speed { font-size: 0.6rem; opacity: 0.65; min-height: 0.9rem; }
|
|
167
|
+
.f-size { font-size: 0.7rem; text-align: right; opacity: 0.65; padding-top: 2px; }
|
|
168
|
+
.f-status { font-size: 0.65rem; letter-spacing: 0.08em; text-transform: uppercase; text-align: right; padding-top: 2px; }
|
|
169
|
+
.f-status.uploading { color: var(--warn); }
|
|
170
|
+
.f-status.success { color: var(--success); }
|
|
171
|
+
.f-status.error { color: var(--error); }
|
|
172
|
+
|
|
173
|
+
/* \u2500\u2500 Download items \u2500\u2500 */
|
|
174
|
+
.dl-item {
|
|
175
|
+
display: grid;
|
|
176
|
+
grid-template-columns: 1fr 80px auto;
|
|
177
|
+
gap: 0 16px;
|
|
178
|
+
align-items: center;
|
|
179
|
+
padding: 10px 0;
|
|
180
|
+
border-bottom: 1px solid var(--border);
|
|
181
|
+
}
|
|
182
|
+
.dl-name { font-size: 0.78rem; color: var(--text-hi); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
183
|
+
.dl-size { font-size: 0.68rem; text-align: right; opacity: 0.6; }
|
|
184
|
+
.dl-btn {
|
|
185
|
+
font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase;
|
|
186
|
+
color: var(--accent); text-decoration: none; opacity: 0.65;
|
|
187
|
+
transition: opacity 0.15s;
|
|
188
|
+
}
|
|
189
|
+
.dl-btn:hover { opacity: 1; }
|
|
190
|
+
|
|
191
|
+
/* \u2500\u2500 Footer \u2500\u2500 */
|
|
192
|
+
.footer {
|
|
193
|
+
margin-top: 32px;
|
|
194
|
+
display: flex;
|
|
195
|
+
justify-content: space-between;
|
|
196
|
+
font-size: 0.6rem;
|
|
197
|
+
letter-spacing: 0.12em;
|
|
198
|
+
text-transform: uppercase;
|
|
199
|
+
opacity: 0.4;
|
|
200
|
+
}
|
|
201
|
+
</style>
|
|
202
|
+
</head>
|
|
203
|
+
<body>
|
|
204
|
+
<div class="shell">
|
|
205
|
+
<header class="header">
|
|
206
|
+
<div>
|
|
207
|
+
<div class="wordmark">LAN<em>DROP</em></div>
|
|
208
|
+
<div class="meta">
|
|
209
|
+
TARGET: ${Q}<br>
|
|
210
|
+
PORT: ${V}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="qr-wrap">
|
|
214
|
+
<div id="qr"></div>
|
|
215
|
+
<div class="qr-label">${$}</div>
|
|
216
|
+
</div>
|
|
217
|
+
</header>
|
|
218
|
+
|
|
219
|
+
<div class="sep"></div>
|
|
220
|
+
|
|
221
|
+
<div id="drop-zone">
|
|
222
|
+
<div class="dz-corners"></div>
|
|
223
|
+
<div class="dz-tag">[ TRANSFER READY ]</div>
|
|
224
|
+
<div class="dz-main">Drop files here<span class="cursor"></span></div>
|
|
225
|
+
<div class="dz-sub">click to browse \xB7 cmd+v to paste clipboard</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<input type="file" id="file-input" multiple>
|
|
229
|
+
|
|
230
|
+
<div class="section-head">
|
|
231
|
+
<span>Upload log</span>
|
|
232
|
+
<span id="upload-count"></span>
|
|
233
|
+
</div>
|
|
234
|
+
<div id="file-list"></div>
|
|
235
|
+
|
|
236
|
+
<div class="section-head" style="margin-top:24px">
|
|
237
|
+
<span>Downloads</span>
|
|
238
|
+
<span id="dl-count"></span>
|
|
239
|
+
</div>
|
|
240
|
+
<div id="dl-list"></div>
|
|
241
|
+
|
|
242
|
+
<footer class="footer">
|
|
243
|
+
<span>Local File Relay</span>
|
|
244
|
+
<span>Bun Runtime</span>
|
|
245
|
+
</footer>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<script>
|
|
249
|
+
// QR code
|
|
250
|
+
if (window.QRCode) {
|
|
251
|
+
new QRCode(document.getElementById('qr'), {
|
|
252
|
+
text: '${$}',
|
|
253
|
+
width: 128, height: 128,
|
|
254
|
+
colorDark: '#000000', colorLight: '#ffffff',
|
|
255
|
+
correctLevel: QRCode.CorrectLevel.M
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
document.getElementById('qr').innerHTML =
|
|
259
|
+
'<div class="qr-missing">QR unavailable<br>(no internet)</div>';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const dropZone = document.getElementById('drop-zone');
|
|
263
|
+
const fileInput = document.getElementById('file-input');
|
|
264
|
+
const fileList = document.getElementById('file-list');
|
|
265
|
+
const countEl = document.getElementById('upload-count');
|
|
266
|
+
let uploads = 0;
|
|
267
|
+
|
|
268
|
+
function esc(s) {
|
|
269
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function fmtBytes(b) {
|
|
273
|
+
if (b === 0) return '0 B';
|
|
274
|
+
const u = ['B','KB','MB','GB'], i = Math.floor(Math.log(b) / Math.log(1024));
|
|
275
|
+
return (b / Math.pow(1024, i)).toFixed(1) + ' ' + u[i];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function fmtTime(s) {
|
|
279
|
+
if (s < 60) return Math.round(s) + 's';
|
|
280
|
+
return Math.floor(s / 60) + 'm ' + Math.round(s % 60) + 's';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Drag & drop
|
|
284
|
+
dropZone.addEventListener('click', () => fileInput.click());
|
|
285
|
+
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
|
286
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
|
287
|
+
dropZone.addEventListener('drop', e => {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
dropZone.classList.remove('drag-over');
|
|
290
|
+
Array.from(e.dataTransfer.files).forEach(uploadFile);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// File picker
|
|
294
|
+
fileInput.addEventListener('change', e => {
|
|
295
|
+
Array.from(e.target.files).forEach(uploadFile);
|
|
296
|
+
fileInput.value = '';
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Clipboard paste
|
|
300
|
+
document.addEventListener('paste', e => {
|
|
301
|
+
if (e.clipboardData.files.length > 0) {
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
Array.from(e.clipboardData.files).forEach(uploadFile);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
function uploadFile(file) {
|
|
308
|
+
countEl.textContent = ++uploads + ' transfer' + (uploads !== 1 ? 's' : '');
|
|
309
|
+
|
|
310
|
+
const item = document.createElement('div');
|
|
311
|
+
item.className = 'file-item';
|
|
312
|
+
item.innerHTML = \`
|
|
313
|
+
<div class="f-info">
|
|
314
|
+
<div class="f-name">\${esc(file.name)}</div>
|
|
315
|
+
<div class="progress-bar"><div class="progress-fill" style="width:0%"></div></div>
|
|
316
|
+
<div class="f-speed"></div>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="f-size">\${fmtBytes(file.size)}</div>
|
|
319
|
+
<div class="f-status uploading">sending</div>
|
|
320
|
+
\`;
|
|
321
|
+
fileList.insertBefore(item, fileList.firstChild);
|
|
322
|
+
|
|
323
|
+
const fill = item.querySelector('.progress-fill');
|
|
324
|
+
const status = item.querySelector('.f-status');
|
|
325
|
+
const speed = item.querySelector('.f-speed');
|
|
326
|
+
|
|
327
|
+
const xhr = new XMLHttpRequest();
|
|
328
|
+
let lastLoaded = 0, lastTime = Date.now();
|
|
329
|
+
|
|
330
|
+
xhr.upload.addEventListener('progress', e => {
|
|
331
|
+
if (!e.lengthComputable) return;
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
const dt = (now - lastTime) / 1000;
|
|
334
|
+
const bps = dt > 0 ? (e.loaded - lastLoaded) / dt : 0;
|
|
335
|
+
const eta = bps > 0 ? (e.total - e.loaded) / bps : Infinity;
|
|
336
|
+
lastLoaded = e.loaded;
|
|
337
|
+
lastTime = now;
|
|
338
|
+
fill.style.width = (e.loaded / e.total * 100) + '%';
|
|
339
|
+
speed.textContent = bps > 0
|
|
340
|
+
? fmtBytes(bps) + '/s' + (isFinite(eta) ? ' ' + fmtTime(eta) + ' left' : '')
|
|
341
|
+
: '';
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
xhr.addEventListener('load', () => {
|
|
345
|
+
fill.style.width = '100%';
|
|
346
|
+
speed.textContent = '';
|
|
347
|
+
if (xhr.status === 200) {
|
|
348
|
+
status.textContent = 'done';
|
|
349
|
+
status.className = 'f-status success';
|
|
350
|
+
loadDownloads();
|
|
351
|
+
} else {
|
|
352
|
+
status.textContent = 'failed';
|
|
353
|
+
status.className = 'f-status error';
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
xhr.addEventListener('error', () => {
|
|
358
|
+
status.textContent = 'error';
|
|
359
|
+
status.className = 'f-status error';
|
|
360
|
+
speed.textContent = '';
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const fd = new FormData();
|
|
364
|
+
fd.append('file', file);
|
|
365
|
+
xhr.open('POST', '/upload');
|
|
366
|
+
xhr.send(fd);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Downloads section
|
|
370
|
+
const dlList = document.getElementById('dl-list');
|
|
371
|
+
const dlCount = document.getElementById('dl-count');
|
|
372
|
+
|
|
373
|
+
async function loadDownloads() {
|
|
374
|
+
try {
|
|
375
|
+
const files = await fetch('/files').then(r => r.json());
|
|
376
|
+
dlCount.textContent = files.length ? files.length + ' file' + (files.length !== 1 ? 's' : '') : '';
|
|
377
|
+
dlList.innerHTML = files.length === 0
|
|
378
|
+
? '<div style="padding:14px 0;font-size:0.7rem;opacity:0.3">pass file/dir paths as args to share downloads</div>'
|
|
379
|
+
: files.map(f => \`
|
|
380
|
+
<div class="dl-item">
|
|
381
|
+
<div class="dl-name">\${esc(f.name)}</div>
|
|
382
|
+
<div class="dl-size">\${fmtBytes(f.size)}</div>
|
|
383
|
+
<a class="dl-btn" href="/download/\${encodeURIComponent(f.name)}" download="\${esc(f.name)}">\u2193 get</a>
|
|
384
|
+
</div>
|
|
385
|
+
\`).join('');
|
|
386
|
+
} catch {}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
loadDownloads();
|
|
390
|
+
</script>
|
|
391
|
+
</body>
|
|
392
|
+
</html>
|
|
393
|
+
`;Bun.serve({port:V,hostname:"0.0.0.0",async fetch(E){let{pathname:H}=new URL(E.url);if(H==="/"&&E.method==="GET")return new Response(A,{headers:{"Content-Type":"text/html"}});if(H==="/upload"&&E.method==="POST"){let G=(await E.formData()).get("file");if(!(G instanceof File))return new Response("No file",{status:400});let J=B(G.name);return await Bun.write(J,G),console.log(`\u2713 ${N(J)} (${k(G.size)}) \u2192 ${J}`),Response.json({success:!0,filename:N(J)})}if(H==="/files"&&E.method==="GET")return Response.json([...X.values()].map(({name:F,size:G})=>({name:F,size:G})));if(H.startsWith("/download/")&&E.method==="GET"){let F=N(decodeURIComponent(H.slice(10))),G=X.get(F);if(!G)return new Response("Not found",{status:404});return new Response(Bun.file(G.path),{headers:{"Content-Disposition":`attachment; filename="${F}"`}})}return new Response("Not found",{status:404})},error(E){return console.error("Server error:",E),new Response("Internal Server Error",{status:500})}});var M=X.size>0?[...X.values()].map((E)=>` \u2193 ${E.name} (${k(E.size)})`):[" (no files shared \u2014 pass paths as args to enable downloads)"];console.log([`
|
|
394
|
+
\u25B2 LANDROP \u2014 File Transfer Server`,` uploads \u2192 ${Q}
|
|
395
|
+
`,` http://localhost:${V}`,...v.map((E)=>` http://${E}:${V}`),`
|
|
396
|
+
Sharing:`,...M,`
|
|
397
|
+
Ctrl+C to stop
|
|
398
|
+
`].join(`
|
|
399
|
+
`));
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "landrop",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-config LAN file transfer — drop files from any browser on your network",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Sean",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"lan",
|
|
9
|
+
"file-transfer",
|
|
10
|
+
"local",
|
|
11
|
+
"drop",
|
|
12
|
+
"share",
|
|
13
|
+
"network"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/sean/landrop"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"landrop": "./landrop.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"landrop.js"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "bun build landrop.ts --outfile landrop.js --target bun --minify"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"bun": ">=1.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|