@chenpu17/serve-here 1.0.2 → 2.0.1
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/bin/serve-here.js +32 -423
- package/package.json +12 -17
- package/README.md +0 -64
- package/src/server.js +0 -403
package/bin/serve-here.js
CHANGED
|
@@ -1,434 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!fs.existsSync(LOG_DIR)) {
|
|
22
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getPidFile(port) {
|
|
27
|
-
return path.join(PID_DIR, `serve-here-${port}.pid`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function getLogFile(port) {
|
|
31
|
-
return path.join(LOG_DIR, `serve-here-${port}.log`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function readPidFile(port) {
|
|
35
|
-
const pidFile = getPidFile(port);
|
|
36
|
-
if (fs.existsSync(pidFile)) {
|
|
37
|
-
try {
|
|
38
|
-
const content = fs.readFileSync(pidFile, 'utf8').trim();
|
|
39
|
-
const lines = content.split('\n');
|
|
40
|
-
const pid = parseInt(lines[0], 10);
|
|
41
|
-
const rootDir = lines[1] || '';
|
|
42
|
-
return { pid, rootDir };
|
|
43
|
-
} catch (error) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function writePidFile(port, pid, rootDir) {
|
|
51
|
-
const pidFile = getPidFile(port);
|
|
52
|
-
fs.writeFileSync(pidFile, `${pid}\n${rootDir}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function removePidFile(port) {
|
|
56
|
-
const pidFile = getPidFile(port);
|
|
57
|
-
if (fs.existsSync(pidFile)) {
|
|
58
|
-
fs.unlinkSync(pidFile);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function isProcessRunning(pid) {
|
|
63
|
-
try {
|
|
64
|
-
process.kill(pid, 0);
|
|
65
|
-
return true;
|
|
66
|
-
} catch (error) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function startDaemon(options) {
|
|
72
|
-
ensureDirectories();
|
|
73
|
-
|
|
74
|
-
const rootDir = path.resolve(options.directory || process.cwd());
|
|
75
|
-
const port = options.port || DEFAULT_PORT;
|
|
76
|
-
const host = options.host || DEFAULT_HOST;
|
|
77
|
-
|
|
78
|
-
// Check if already running
|
|
79
|
-
const pidInfo = readPidFile(port);
|
|
80
|
-
if (pidInfo && isProcessRunning(pidInfo.pid)) {
|
|
81
|
-
console.error(`Error: Server already running on port ${port} (PID: ${pidInfo.pid})`);
|
|
82
|
-
console.error(`Serving: ${pidInfo.rootDir}`);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Validate directory
|
|
87
|
-
let stats;
|
|
88
|
-
try {
|
|
89
|
-
stats = fs.statSync(rootDir);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
console.error(`Error: directory "${rootDir}" does not exist or is not accessible.`);
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
if (!stats.isDirectory()) {
|
|
95
|
-
console.error(`Error: path "${rootDir}" is not a directory.`);
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const logFile = getLogFile(port);
|
|
100
|
-
const out = fs.openSync(logFile, 'a');
|
|
101
|
-
const err = fs.openSync(logFile, 'a');
|
|
102
|
-
|
|
103
|
-
const child = spawn(process.execPath, [__filename, '--daemon-child', '-d', rootDir, '-p', String(port), '-H', host], {
|
|
104
|
-
detached: true,
|
|
105
|
-
stdio: ['ignore', out, err],
|
|
106
|
-
env: process.env
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
child.unref();
|
|
110
|
-
|
|
111
|
-
writePidFile(port, child.pid, rootDir);
|
|
112
|
-
|
|
113
|
-
console.log(`Server started in background (PID: ${child.pid})`);
|
|
114
|
-
console.log(`Serving: ${rootDir}`);
|
|
115
|
-
console.log(`Listening on port: ${port}`);
|
|
116
|
-
console.log(`Log file: ${logFile}`);
|
|
117
|
-
console.log(`\nTo stop: serve-here --stop -p ${port}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function stopDaemon(port) {
|
|
121
|
-
const pidInfo = readPidFile(port);
|
|
122
|
-
if (!pidInfo) {
|
|
123
|
-
console.error(`No server running on port ${port}`);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!isProcessRunning(pidInfo.pid)) {
|
|
128
|
-
console.log(`Server (PID: ${pidInfo.pid}) is not running, cleaning up...`);
|
|
129
|
-
removePidFile(port);
|
|
130
|
-
process.exit(0);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
process.kill(pidInfo.pid, 'SIGTERM');
|
|
135
|
-
console.log(`Stopping server (PID: ${pidInfo.pid})...`);
|
|
136
|
-
|
|
137
|
-
// Wait for process to stop
|
|
138
|
-
let attempts = 0;
|
|
139
|
-
const checkInterval = setInterval(() => {
|
|
140
|
-
attempts++;
|
|
141
|
-
if (!isProcessRunning(pidInfo.pid)) {
|
|
142
|
-
clearInterval(checkInterval);
|
|
143
|
-
removePidFile(port);
|
|
144
|
-
console.log('Server stopped.');
|
|
145
|
-
process.exit(0);
|
|
146
|
-
} else if (attempts >= 10) {
|
|
147
|
-
clearInterval(checkInterval);
|
|
148
|
-
console.error('Server did not stop gracefully, force killing...');
|
|
149
|
-
try {
|
|
150
|
-
process.kill(pidInfo.pid, 'SIGKILL');
|
|
151
|
-
} catch (e) {}
|
|
152
|
-
removePidFile(port);
|
|
153
|
-
process.exit(0);
|
|
154
|
-
}
|
|
155
|
-
}, 500);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.error(`Failed to stop server: ${error.message}`);
|
|
158
|
-
removePidFile(port);
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function showStatus(port) {
|
|
164
|
-
if (port) {
|
|
165
|
-
const pidInfo = readPidFile(port);
|
|
166
|
-
if (!pidInfo) {
|
|
167
|
-
console.log(`No server running on port ${port}`);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const running = isProcessRunning(pidInfo.pid);
|
|
172
|
-
console.log(`Port ${port}:`);
|
|
173
|
-
console.log(` PID: ${pidInfo.pid}`);
|
|
174
|
-
console.log(` Status: ${running ? 'running' : 'stopped'}`);
|
|
175
|
-
console.log(` Directory: ${pidInfo.rootDir}`);
|
|
176
|
-
console.log(` Log: ${getLogFile(port)}`);
|
|
177
|
-
|
|
178
|
-
if (!running) {
|
|
179
|
-
removePidFile(port);
|
|
180
|
-
}
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
function getBinaryPath() {
|
|
7
|
+
const platform = process.platform;
|
|
8
|
+
const arch = process.arch;
|
|
9
|
+
|
|
10
|
+
let pkgName;
|
|
11
|
+
if (platform === "darwin" && arch === "x64") {
|
|
12
|
+
pkgName = "@chenpu17/serve-here-darwin-x64";
|
|
13
|
+
} else if (platform === "darwin" && arch === "arm64") {
|
|
14
|
+
pkgName = "@chenpu17/serve-here-darwin-arm64";
|
|
15
|
+
} else if (platform === "linux" && arch === "x64") {
|
|
16
|
+
pkgName = "@chenpu17/serve-here-linux-x64";
|
|
17
|
+
} else if (platform === "linux" && arch === "arm64") {
|
|
18
|
+
pkgName = "@chenpu17/serve-here-linux-arm64";
|
|
19
|
+
} else if (platform === "win32" && arch === "x64") {
|
|
20
|
+
pkgName = "@chenpu17/serve-here-windows-x64";
|
|
181
21
|
} else {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const files = fs.readdirSync(PID_DIR).filter(f => f.endsWith('.pid'));
|
|
185
|
-
|
|
186
|
-
if (files.length === 0) {
|
|
187
|
-
console.log('No servers running.');
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
console.log('Running servers:\n');
|
|
192
|
-
for (const file of files) {
|
|
193
|
-
const match = file.match(/serve-here-(\d+)\.pid/);
|
|
194
|
-
if (match) {
|
|
195
|
-
const p = parseInt(match[1], 10);
|
|
196
|
-
const pidInfo = readPidFile(p);
|
|
197
|
-
if (pidInfo) {
|
|
198
|
-
const running = isProcessRunning(pidInfo.pid);
|
|
199
|
-
console.log(`Port ${p}:`);
|
|
200
|
-
console.log(` PID: ${pidInfo.pid}`);
|
|
201
|
-
console.log(` Status: ${running ? 'running' : 'stopped'}`);
|
|
202
|
-
console.log(` Directory: ${pidInfo.rootDir}`);
|
|
203
|
-
console.log('');
|
|
204
|
-
|
|
205
|
-
if (!running) {
|
|
206
|
-
removePidFile(p);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
22
|
+
console.error("Unsupported platform: " + platform + "-" + arch);
|
|
23
|
+
process.exit(1);
|
|
211
24
|
}
|
|
212
|
-
}
|
|
213
25
|
|
|
214
|
-
|
|
26
|
+
const binaryName = platform === "win32" ? "serve-here.exe" : "serve-here";
|
|
215
27
|
try {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
process.exit(0);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (options.version) {
|
|
224
|
-
console.log(pkg.version);
|
|
225
|
-
process.exit(0);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Handle daemon commands
|
|
229
|
-
if (options.stop) {
|
|
230
|
-
stopDaemon(options.port || DEFAULT_PORT);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (options.status) {
|
|
235
|
-
showStatus(options.port);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (options.daemon && !options.daemonChild) {
|
|
240
|
-
startDaemon(options);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const rootDir = path.resolve(options.directory || process.cwd());
|
|
245
|
-
|
|
246
|
-
let stats;
|
|
247
|
-
try {
|
|
248
|
-
stats = fs.statSync(rootDir);
|
|
249
|
-
} catch (error) {
|
|
250
|
-
console.error(`Error: directory "${rootDir}" does not exist or is not accessible.`);
|
|
251
|
-
process.exit(1);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (!stats.isDirectory()) {
|
|
255
|
-
console.error(`Error: path "${rootDir}" is not a directory.`);
|
|
256
|
-
process.exit(1);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const server = createStaticServer({ rootDir });
|
|
260
|
-
const port = options.port || DEFAULT_PORT;
|
|
261
|
-
const host = options.host || DEFAULT_HOST;
|
|
262
|
-
|
|
263
|
-
if (options.daemonChild) {
|
|
264
|
-
ensureDirectories();
|
|
265
|
-
process.on('exit', () => removePidFile(port));
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
server.on('error', error => {
|
|
269
|
-
console.error('Failed to start server:', error.message);
|
|
270
|
-
if (options.daemonChild) {
|
|
271
|
-
removePidFile(port);
|
|
272
|
-
}
|
|
273
|
-
process.exit(1);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
server.listen(port, host, () => {
|
|
277
|
-
const messageLines = [
|
|
278
|
-
`Serving ${rootDir}`,
|
|
279
|
-
`Listening on:`,
|
|
280
|
-
...formatListeningAddresses(host, port)
|
|
281
|
-
];
|
|
282
|
-
console.log(messageLines.join('\n '));
|
|
283
|
-
|
|
284
|
-
// Write PID file when running as daemon child
|
|
285
|
-
if (options.daemonChild) {
|
|
286
|
-
writePidFile(port, process.pid, rootDir);
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const shutdown = () => {
|
|
291
|
-
console.log('\nShutting down...');
|
|
292
|
-
if (options.daemonChild) {
|
|
293
|
-
removePidFile(port);
|
|
294
|
-
}
|
|
295
|
-
server.close(() => process.exit(0));
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
process.on('SIGINT', shutdown);
|
|
299
|
-
process.on('SIGTERM', shutdown);
|
|
300
|
-
} catch (error) {
|
|
301
|
-
console.error(error.message);
|
|
28
|
+
return require.resolve(pkgName + "/bin/" + binaryName);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error("Could not find binary for " + pkgName + ".");
|
|
31
|
+
console.error("Try reinstalling the package.");
|
|
302
32
|
process.exit(1);
|
|
303
33
|
}
|
|
304
34
|
}
|
|
305
35
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
version: false,
|
|
313
|
-
daemon: false,
|
|
314
|
-
daemonChild: false,
|
|
315
|
-
stop: false,
|
|
316
|
-
status: false
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
320
|
-
const arg = args[i];
|
|
321
|
-
|
|
322
|
-
switch (arg) {
|
|
323
|
-
case '-h':
|
|
324
|
-
case '--help':
|
|
325
|
-
options.help = true;
|
|
326
|
-
break;
|
|
327
|
-
case '-V':
|
|
328
|
-
case '--version':
|
|
329
|
-
options.version = true;
|
|
330
|
-
break;
|
|
331
|
-
case '-d':
|
|
332
|
-
case '--dir':
|
|
333
|
-
case '--directory':
|
|
334
|
-
options.directory = requireValue(args, ++i, '--dir');
|
|
335
|
-
break;
|
|
336
|
-
case '-p':
|
|
337
|
-
case '--port':
|
|
338
|
-
options.port = parsePort(requireValue(args, ++i, '--port'));
|
|
339
|
-
break;
|
|
340
|
-
case '-H':
|
|
341
|
-
case '--host':
|
|
342
|
-
options.host = requireValue(args, ++i, '--host');
|
|
343
|
-
break;
|
|
344
|
-
case '-D':
|
|
345
|
-
case '--daemon':
|
|
346
|
-
options.daemon = true;
|
|
347
|
-
break;
|
|
348
|
-
case '--daemon-child':
|
|
349
|
-
options.daemonChild = true;
|
|
350
|
-
break;
|
|
351
|
-
case '--stop':
|
|
352
|
-
options.stop = true;
|
|
353
|
-
break;
|
|
354
|
-
case '--status':
|
|
355
|
-
options.status = true;
|
|
356
|
-
break;
|
|
357
|
-
default:
|
|
358
|
-
if (arg.startsWith('-')) {
|
|
359
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
360
|
-
}
|
|
361
|
-
if (options.directory) {
|
|
362
|
-
throw new Error('Multiple directories specified. Use --dir <path> to set the directory.');
|
|
363
|
-
}
|
|
364
|
-
options.directory = arg;
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return options;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function requireValue(args, index, flagName) {
|
|
373
|
-
const value = args[index];
|
|
374
|
-
if (value === undefined) {
|
|
375
|
-
throw new Error(`Missing value for ${flagName}`);
|
|
376
|
-
}
|
|
377
|
-
return value;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function parsePort(value) {
|
|
381
|
-
const port = Number.parseInt(value, 10);
|
|
382
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
383
|
-
throw new Error(`Invalid port: ${value}`);
|
|
384
|
-
}
|
|
385
|
-
return port;
|
|
36
|
+
const binaryPath = getBinaryPath();
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
const result = spawnSync(binaryPath, args, { stdio: "inherit" });
|
|
39
|
+
if (result.error) {
|
|
40
|
+
console.error(result.error.message);
|
|
41
|
+
process.exit(1);
|
|
386
42
|
}
|
|
387
|
-
|
|
388
|
-
function formatListeningAddresses(host, port) {
|
|
389
|
-
const addresses = [];
|
|
390
|
-
|
|
391
|
-
if (host === '0.0.0.0' || host === '::') {
|
|
392
|
-
addresses.push(`http://localhost:${port}/`);
|
|
393
|
-
const interfaces = os.networkInterfaces();
|
|
394
|
-
for (const iface of Object.values(interfaces)) {
|
|
395
|
-
if (!iface) continue;
|
|
396
|
-
for (const addrInfo of iface) {
|
|
397
|
-
if (addrInfo.internal) continue;
|
|
398
|
-
if (addrInfo.family !== 'IPv4' && addrInfo.family !== 4) continue;
|
|
399
|
-
addresses.push(`http://${addrInfo.address}:${port}/`);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
} else {
|
|
403
|
-
addresses.push(`http://${host}:${port}/`);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return [...new Set(addresses)];
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function printHelp() {
|
|
410
|
-
console.log(`serve-here v${pkg.version}
|
|
411
|
-
|
|
412
|
-
Usage:
|
|
413
|
-
serve-here [options] [directory]
|
|
414
|
-
|
|
415
|
-
Options:
|
|
416
|
-
-d, --dir <path> Directory to serve (defaults to current working directory)
|
|
417
|
-
-p, --port <number> Port to listen on (default: ${DEFAULT_PORT})
|
|
418
|
-
-H, --host <address> Hostname or IP to bind (default: ${DEFAULT_HOST})
|
|
419
|
-
-D, --daemon Run as a background daemon (does not occupy terminal)
|
|
420
|
-
--stop Stop a running daemon (use with -p to specify port)
|
|
421
|
-
--status Show status of running daemon(s)
|
|
422
|
-
-h, --help Show this help message
|
|
423
|
-
-V, --version Show version
|
|
424
|
-
|
|
425
|
-
Examples:
|
|
426
|
-
serve-here Start server in foreground on port 8080
|
|
427
|
-
serve-here -D Start server as daemon on port 8080
|
|
428
|
-
serve-here -D -p 3000 Start daemon on port 3000
|
|
429
|
-
serve-here --stop Stop daemon on port 8080
|
|
430
|
-
serve-here --stop -p 3000 Stop daemon on port 3000
|
|
431
|
-
serve-here --status Show all running daemons`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
main();
|
|
43
|
+
process.exit(result.status ?? 1);
|
package/package.json
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chenpu17/serve-here",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "A minimal CLI to serve static files from a directory over HTTP.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"serve-here": "bin/serve-here.js"
|
|
7
7
|
},
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"optionalDependencies": {
|
|
13
|
+
"@chenpu17/serve-here-darwin-x64": "2.0.1",
|
|
14
|
+
"@chenpu17/serve-here-darwin-arm64": "2.0.1",
|
|
15
|
+
"@chenpu17/serve-here-linux-x64": "2.0.1",
|
|
16
|
+
"@chenpu17/serve-here-linux-arm64": "2.0.1",
|
|
17
|
+
"@chenpu17/serve-here-windows-x64": "2.0.1"
|
|
11
18
|
},
|
|
12
19
|
"keywords": [
|
|
13
20
|
"cli",
|
|
@@ -26,17 +33,5 @@
|
|
|
26
33
|
"bugs": {
|
|
27
34
|
"url": "https://github.com/chenpu17/serve-here/issues"
|
|
28
35
|
},
|
|
29
|
-
"homepage": "https://github.com/chenpu17/serve-here#readme"
|
|
30
|
-
"files": [
|
|
31
|
-
"bin",
|
|
32
|
-
"src",
|
|
33
|
-
"README.md",
|
|
34
|
-
"LICENSE"
|
|
35
|
-
],
|
|
36
|
-
"engines": {
|
|
37
|
-
"node": ">=14"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"mime-types": "^2.1.35"
|
|
41
|
-
}
|
|
36
|
+
"homepage": "https://github.com/chenpu17/serve-here#readme"
|
|
42
37
|
}
|
package/README.md
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# serve-here
|
|
2
|
-
|
|
3
|
-
Serve any local directory over HTTP with a single command.
|
|
4
|
-
用一条命令把任意本地目录通过 HTTP 暴露出去。
|
|
5
|
-
|
|
6
|
-
## Features | 功能特点
|
|
7
|
-
- Instant static hosting for the current or specified directory.
|
|
8
|
-
立即托管当前目录或指定目录。
|
|
9
|
-
- Automatic `index.html` support plus clean directory listings.
|
|
10
|
-
自动识别 `index.html`,无首页时提供整洁的目录列表。
|
|
11
|
-
- Safe redirects for non-ASCII directory names (e.g. 中文路径).
|
|
12
|
-
支持中文等非 ASCII 路径的安全重定向。
|
|
13
|
-
- Directory rows include file size and last modified time.
|
|
14
|
-
列表项展示文件大小与最近修改时间。
|
|
15
|
-
- Works as a global install or `npx` one-off.
|
|
16
|
-
支持全局安装或通过 `npx` 临时使用。
|
|
17
|
-
|
|
18
|
-
## Installation | 安装
|
|
19
|
-
|
|
20
|
-
```sh
|
|
21
|
-
npm install -g @chenpu17/serve-here
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Or run it ad‑hoc without installing globally:
|
|
25
|
-
或者临时使用:
|
|
26
|
-
|
|
27
|
-
```sh
|
|
28
|
-
npx @chenpu17/serve-here
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Usage | 使用方式
|
|
32
|
-
|
|
33
|
-
```sh
|
|
34
|
-
serve-here [options] [directory]
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
- Installed globally, invoke via `serve-here`; with `npx`, run `npx @chenpu17/serve-here`.
|
|
38
|
-
全局安装后直接使用 `serve-here`;临时使用时运行 `npx @chenpu17/serve-here`。
|
|
39
|
-
|
|
40
|
-
- `directory`: Directory to share; defaults to the current working directory.
|
|
41
|
-
`directory`:要共享的目录,默认使用当前工作目录。
|
|
42
|
-
- `-d, --dir <path>`: Explicit directory override.
|
|
43
|
-
`-d, --dir <path>`:显式指定要共享的目录。
|
|
44
|
-
- `-p, --port <number>`: Port to listen on (default `8080`).
|
|
45
|
-
`-p, --port <number>`:设置监听端口(默认 `8080`)。
|
|
46
|
-
- `-H, --host <address>`: Host/IP to bind (default `0.0.0.0`).
|
|
47
|
-
`-H, --host <address>`:指定绑定的主机或 IP(默认 `0.0.0.0`)。
|
|
48
|
-
- `-h, --help`: Show help.
|
|
49
|
-
`-h, --help`:查看帮助信息。
|
|
50
|
-
- `-V, --version`: Print version.
|
|
51
|
-
`-V, --version`:查看版本号。
|
|
52
|
-
|
|
53
|
-
After startup you’ll see the bound addresses in the terminal. Opening them in a browser displays your static files or a table view of the directory contents.
|
|
54
|
-
启动后终端会打印可访问的地址,浏览器打开即可查看静态文件;若目录中没有 `index.html`,将显示带有文件大小和修改时间的目录表格。
|
|
55
|
-
|
|
56
|
-
## Development | 开发
|
|
57
|
-
|
|
58
|
-
```sh
|
|
59
|
-
npm install
|
|
60
|
-
npm start
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
`npm start` serves the current project directory so you can test quickly with a browser or `curl`.
|
|
64
|
-
`npm start` 会把当前项目目录作为静态资源根目录,方便通过浏览器或 `curl` 快速验证。
|
package/src/server.js
DELETED
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
const http = require('http');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { lookup } = require('mime-types');
|
|
5
|
-
|
|
6
|
-
const { readdir, stat } = fs.promises;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Creates an HTTP server that serves static files rooted at the provided directory.
|
|
10
|
-
*
|
|
11
|
-
* @param {object} options
|
|
12
|
-
* @param {string} options.rootDir Absolute or relative directory to serve.
|
|
13
|
-
* @param {string[]} [options.indexFiles] Index files to try when a directory is requested.
|
|
14
|
-
* @param {boolean} [options.enableDirectoryListing] Whether to render a simple directory listing when no index file exists.
|
|
15
|
-
* @param {Console} [options.logger] Logger implementation, defaults to console.
|
|
16
|
-
* @returns {http.Server}
|
|
17
|
-
*/
|
|
18
|
-
function createStaticServer({
|
|
19
|
-
rootDir,
|
|
20
|
-
indexFiles = ['index.html', 'index.htm'],
|
|
21
|
-
enableDirectoryListing = true,
|
|
22
|
-
logger = console
|
|
23
|
-
}) {
|
|
24
|
-
if (!rootDir) {
|
|
25
|
-
throw new Error('rootDir is required to create a static server');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const resolvedRoot = path.resolve(rootDir);
|
|
29
|
-
const rootWithSep = resolvedRoot.endsWith(path.sep)
|
|
30
|
-
? resolvedRoot
|
|
31
|
-
: `${resolvedRoot}${path.sep}`;
|
|
32
|
-
|
|
33
|
-
const server = http.createServer((req, res) => {
|
|
34
|
-
const startTime = Date.now();
|
|
35
|
-
|
|
36
|
-
void (async () => {
|
|
37
|
-
if (!req.url) {
|
|
38
|
-
sendError(res, 400, 'Bad Request', req.method === 'HEAD');
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
43
|
-
res.setHeader('Allow', 'GET, HEAD');
|
|
44
|
-
sendError(res, 405, 'Method Not Allowed', false);
|
|
45
|
-
logRequest(logger, req, res.statusCode, startTime);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let decodedPath;
|
|
50
|
-
let requestPathname;
|
|
51
|
-
let requestSearch;
|
|
52
|
-
try {
|
|
53
|
-
const url = new URL(req.url, 'http://localhost');
|
|
54
|
-
requestPathname = url.pathname;
|
|
55
|
-
requestSearch = url.search;
|
|
56
|
-
decodedPath = decodeURIComponent(requestPathname);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
sendError(res, 400, 'Bad Request', req.method === 'HEAD');
|
|
59
|
-
logRequest(logger, req, res.statusCode, startTime, error);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const candidateSegments = decodedPath
|
|
64
|
-
.split('/')
|
|
65
|
-
.filter(segment => segment && segment !== '.');
|
|
66
|
-
const resolvedPath = path.resolve(resolvedRoot, ...candidateSegments);
|
|
67
|
-
|
|
68
|
-
if (!resolvedPath.startsWith(rootWithSep) && resolvedPath !== resolvedRoot) {
|
|
69
|
-
sendError(res, 403, 'Forbidden', req.method === 'HEAD');
|
|
70
|
-
logRequest(logger, req, res.statusCode, startTime);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
let stats;
|
|
75
|
-
try {
|
|
76
|
-
stats = await stat(resolvedPath);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
if (error.code === 'ENOENT') {
|
|
79
|
-
sendError(res, 404, 'Not Found', req.method === 'HEAD');
|
|
80
|
-
} else {
|
|
81
|
-
sendError(res, 500, 'Internal Server Error', req.method === 'HEAD');
|
|
82
|
-
logger.error('Error reading path', resolvedPath, error);
|
|
83
|
-
}
|
|
84
|
-
logRequest(logger, req, res.statusCode, startTime, error);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (stats.isDirectory()) {
|
|
89
|
-
if (!requestPathname.endsWith('/')) {
|
|
90
|
-
// Align with browser expectations for relative asset loading.
|
|
91
|
-
const location = `${requestPathname}/${requestSearch}`;
|
|
92
|
-
try {
|
|
93
|
-
// Use the encoded pathname for headers (Node rejects non-ASCII in Location).
|
|
94
|
-
res.statusCode = 301;
|
|
95
|
-
res.setHeader('Location', location);
|
|
96
|
-
res.end();
|
|
97
|
-
logRequest(logger, req, res.statusCode, startTime);
|
|
98
|
-
return;
|
|
99
|
-
} catch (error) {
|
|
100
|
-
sendError(res, 400, 'Bad Request', req.method === 'HEAD');
|
|
101
|
-
logRequest(logger, req, res.statusCode, startTime, error);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
for (const indexFile of indexFiles) {
|
|
107
|
-
const candidate = path.join(resolvedPath, indexFile);
|
|
108
|
-
try {
|
|
109
|
-
const indexStats = await stat(candidate);
|
|
110
|
-
if (indexStats.isFile()) {
|
|
111
|
-
await serveFile(res, candidate, indexStats, req.method === 'HEAD');
|
|
112
|
-
logRequest(logger, req, res.statusCode, startTime);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (error.code !== 'ENOENT') {
|
|
117
|
-
logger.error('Error reading index file', candidate, error);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!enableDirectoryListing) {
|
|
123
|
-
sendError(res, 403, 'Directory listing disabled', req.method === 'HEAD');
|
|
124
|
-
logRequest(logger, req, res.statusCode, startTime);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
await serveDirectoryListing(
|
|
130
|
-
res,
|
|
131
|
-
decodedPath,
|
|
132
|
-
resolvedPath,
|
|
133
|
-
req.method === 'HEAD'
|
|
134
|
-
);
|
|
135
|
-
logRequest(logger, req, res.statusCode, startTime);
|
|
136
|
-
return;
|
|
137
|
-
} catch (error) {
|
|
138
|
-
sendError(res, 500, 'Internal Server Error', req.method === 'HEAD');
|
|
139
|
-
logger.error('Error generating directory listing', resolvedPath, error);
|
|
140
|
-
logRequest(logger, req, res.statusCode, startTime, error);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (stats.isFile()) {
|
|
146
|
-
try {
|
|
147
|
-
await serveFile(res, resolvedPath, stats, req.method === 'HEAD');
|
|
148
|
-
} catch (error) {
|
|
149
|
-
logger.error('Error serving file', resolvedPath, error);
|
|
150
|
-
// Stream errors may happen after headers were sent.
|
|
151
|
-
}
|
|
152
|
-
logRequest(logger, req, res.statusCode, startTime);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
sendError(res, 403, 'Forbidden', req.method === 'HEAD');
|
|
157
|
-
logRequest(logger, req, res.statusCode, startTime);
|
|
158
|
-
})().catch(error => {
|
|
159
|
-
if (!res.headersSent) {
|
|
160
|
-
sendError(res, 500, 'Internal Server Error', req.method === 'HEAD');
|
|
161
|
-
} else {
|
|
162
|
-
res.destroy();
|
|
163
|
-
}
|
|
164
|
-
logger.error('Unhandled request error', error);
|
|
165
|
-
logRequest(logger, req, res.statusCode || 500, startTime, error);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
return server;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function sendError(res, statusCode, message, headOnly = false) {
|
|
173
|
-
if (res.headersSent) {
|
|
174
|
-
if (!res.writableEnded) {
|
|
175
|
-
res.end();
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
res.statusCode = statusCode;
|
|
181
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
182
|
-
res.setHeader('Cache-Control', 'no-store');
|
|
183
|
-
|
|
184
|
-
if (headOnly) {
|
|
185
|
-
res.end();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
res.end(`${statusCode} ${message}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function serveFile(res, filePath, stats, headOnly) {
|
|
193
|
-
const mimeType = lookup(filePath) || 'application/octet-stream';
|
|
194
|
-
res.statusCode = 200;
|
|
195
|
-
res.setHeader('Content-Type', mimeType);
|
|
196
|
-
res.setHeader('Content-Length', stats.size);
|
|
197
|
-
res.setHeader('Last-Modified', stats.mtime.toUTCString());
|
|
198
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
199
|
-
|
|
200
|
-
if (headOnly) {
|
|
201
|
-
res.end();
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
await streamFile(res, filePath);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function streamFile(res, filePath) {
|
|
209
|
-
return new Promise((resolve, reject) => {
|
|
210
|
-
const stream = fs.createReadStream(filePath);
|
|
211
|
-
let settled = false;
|
|
212
|
-
|
|
213
|
-
const settle = (fn, arg) => {
|
|
214
|
-
if (settled) return;
|
|
215
|
-
settled = true;
|
|
216
|
-
cleanup();
|
|
217
|
-
fn(arg);
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const onResponseClose = () => {
|
|
221
|
-
if (!stream.destroyed) {
|
|
222
|
-
stream.destroy();
|
|
223
|
-
}
|
|
224
|
-
settle(resolve);
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const onResponseError = error => {
|
|
228
|
-
if (!stream.destroyed) {
|
|
229
|
-
stream.destroy(error);
|
|
230
|
-
}
|
|
231
|
-
settle(reject, error);
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const onStreamError = error => {
|
|
235
|
-
if (!res.headersSent) {
|
|
236
|
-
sendError(res, 500, 'Internal Server Error');
|
|
237
|
-
} else {
|
|
238
|
-
res.destroy(error);
|
|
239
|
-
}
|
|
240
|
-
settle(reject, error);
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const onStreamEnd = () => settle(resolve);
|
|
244
|
-
|
|
245
|
-
const cleanup = () => {
|
|
246
|
-
res.removeListener('close', onResponseClose);
|
|
247
|
-
res.removeListener('error', onResponseError);
|
|
248
|
-
stream.removeListener('error', onStreamError);
|
|
249
|
-
stream.removeListener('end', onStreamEnd);
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
res.on('close', onResponseClose);
|
|
253
|
-
res.on('error', onResponseError);
|
|
254
|
-
stream.on('error', onStreamError);
|
|
255
|
-
stream.on('end', onStreamEnd);
|
|
256
|
-
stream.pipe(res);
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function serveDirectoryListing(res, requestPath, directoryPath, headOnly) {
|
|
261
|
-
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
262
|
-
const items = entries
|
|
263
|
-
.map(entry => ({
|
|
264
|
-
name: entry.name,
|
|
265
|
-
isDirectory: entry.isDirectory()
|
|
266
|
-
}))
|
|
267
|
-
.sort((a, b) => {
|
|
268
|
-
if (a.isDirectory && !b.isDirectory) return -1;
|
|
269
|
-
if (!a.isDirectory && b.isDirectory) return 1;
|
|
270
|
-
return a.name.localeCompare(b.name);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const detailedItems = [];
|
|
274
|
-
for (const item of items) {
|
|
275
|
-
const fullPath = path.join(directoryPath, item.name);
|
|
276
|
-
try {
|
|
277
|
-
const stats = await stat(fullPath);
|
|
278
|
-
detailedItems.push({
|
|
279
|
-
...item,
|
|
280
|
-
size: stats.isFile() ? stats.size : null,
|
|
281
|
-
mtime: stats.mtime
|
|
282
|
-
});
|
|
283
|
-
} catch (error) {
|
|
284
|
-
detailedItems.push({
|
|
285
|
-
...item,
|
|
286
|
-
size: null,
|
|
287
|
-
mtime: null
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const tableRows = detailedItems
|
|
293
|
-
.map(item => {
|
|
294
|
-
const trailingSlash = item.isDirectory ? '/' : '';
|
|
295
|
-
const href = encodeURIComponent(item.name) + trailingSlash;
|
|
296
|
-
const displayName = `${escapeHtml(item.name)}${trailingSlash}`;
|
|
297
|
-
const size = item.isDirectory ? '-' : formatBytes(item.size);
|
|
298
|
-
const modified = item.mtime ? formatDate(item.mtime) : '-';
|
|
299
|
-
return `<tr><td><a href="${href}">${displayName}</a></td><td>${size}</td><td>${modified}</td></tr>`;
|
|
300
|
-
})
|
|
301
|
-
.join('\n');
|
|
302
|
-
|
|
303
|
-
const body = `<!doctype html>
|
|
304
|
-
<html>
|
|
305
|
-
<head>
|
|
306
|
-
<meta charset="utf-8">
|
|
307
|
-
<title>Index of ${escapeHtml(requestPath)}</title>
|
|
308
|
-
<style>
|
|
309
|
-
body { font-family: sans-serif; padding: 1rem 2rem; }
|
|
310
|
-
h1 { font-size: 1.5rem; margin-bottom: 1rem; }
|
|
311
|
-
table { border-collapse: collapse; width: 100%; max-width: 60rem; }
|
|
312
|
-
th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid #e1e4e8; }
|
|
313
|
-
th { font-weight: 600; background: #f6f8fa; }
|
|
314
|
-
td:nth-child(2), th:nth-child(2) { width: 8rem; }
|
|
315
|
-
td:nth-child(3), th:nth-child(3) { width: 16rem; }
|
|
316
|
-
a { text-decoration: none; color: #0366d6; }
|
|
317
|
-
a:hover { text-decoration: underline; }
|
|
318
|
-
@media (max-width: 600px) {
|
|
319
|
-
table { font-size: 0.9rem; }
|
|
320
|
-
td:nth-child(3), th:nth-child(3) { width: auto; }
|
|
321
|
-
}
|
|
322
|
-
</style>
|
|
323
|
-
</head>
|
|
324
|
-
<body>
|
|
325
|
-
<h1>Index of ${escapeHtml(requestPath)}</h1>
|
|
326
|
-
<table>
|
|
327
|
-
<thead>
|
|
328
|
-
<tr><th>Name</th><th>Size</th><th>Last Modified</th></tr>
|
|
329
|
-
</thead>
|
|
330
|
-
<tbody>
|
|
331
|
-
${requestPath !== '/' ? '<tr><td><a href="../">../</a></td><td>-</td><td>-</td></tr>' : ''}
|
|
332
|
-
${tableRows}
|
|
333
|
-
</tbody>
|
|
334
|
-
</table>
|
|
335
|
-
<footer>
|
|
336
|
-
<p>
|
|
337
|
-
Served by <a href="https://github.com/chenpu17/serve-here" target="_blank" rel="noreferrer">serve-here</a>
|
|
338
|
-
</p>
|
|
339
|
-
</footer>
|
|
340
|
-
</body>
|
|
341
|
-
</html>`;
|
|
342
|
-
|
|
343
|
-
res.statusCode = 200;
|
|
344
|
-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
345
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
346
|
-
|
|
347
|
-
if (headOnly) {
|
|
348
|
-
res.end();
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
res.end(body);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function escapeHtml(value) {
|
|
356
|
-
return value.replace(/[&<>"']/g, character => {
|
|
357
|
-
const replacements = {
|
|
358
|
-
'&': '&',
|
|
359
|
-
'<': '<',
|
|
360
|
-
'>': '>',
|
|
361
|
-
'"': '"',
|
|
362
|
-
"'": '''
|
|
363
|
-
};
|
|
364
|
-
return replacements[character] || character;
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function formatBytes(bytes) {
|
|
369
|
-
if (bytes === null || bytes === undefined) {
|
|
370
|
-
return '-';
|
|
371
|
-
}
|
|
372
|
-
if (bytes === 0) {
|
|
373
|
-
return '0 B';
|
|
374
|
-
}
|
|
375
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
376
|
-
const exponent = Math.min(
|
|
377
|
-
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
378
|
-
units.length - 1
|
|
379
|
-
);
|
|
380
|
-
const size = bytes / 1024 ** exponent;
|
|
381
|
-
return `${size >= 10 ? size.toFixed(0) : size.toFixed(1)} ${units[exponent]}`;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function formatDate(date) {
|
|
385
|
-
if (!date) return '-';
|
|
386
|
-
return date.toLocaleString();
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function logRequest(logger, req, statusCode, startTime, error) {
|
|
390
|
-
const duration = Date.now() - startTime;
|
|
391
|
-
const method = req.method;
|
|
392
|
-
const url = req.url;
|
|
393
|
-
const message = `${method} ${url} ${statusCode} ${duration}ms`;
|
|
394
|
-
if (error) {
|
|
395
|
-
logger.error(message);
|
|
396
|
-
} else {
|
|
397
|
-
logger.info ? logger.info(message) : logger.log(message);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
module.exports = {
|
|
402
|
-
createStaticServer
|
|
403
|
-
};
|