2018-12-09 20:35:26 +00:00
|
|
|
#!/usr/bin/env deno --allow-net
|
|
|
|
|
|
|
|
// This program serves files in the current directory over HTTP.
|
|
|
|
// TODO Stream responses instead of reading them into memory.
|
|
|
|
// TODO Add tests like these:
|
|
|
|
// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js
|
|
|
|
|
2018-12-17 16:49:10 +00:00
|
|
|
import {
|
|
|
|
listenAndServe,
|
|
|
|
ServerRequest,
|
|
|
|
setContentLength,
|
|
|
|
Response
|
2018-12-23 23:49:46 +00:00
|
|
|
} from "./http.ts";
|
2018-12-17 16:49:10 +00:00
|
|
|
import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno";
|
2019-01-10 22:11:44 +00:00
|
|
|
import { extname } from "../fs/path.ts";
|
2018-12-31 09:06:06 +00:00
|
|
|
import * as extensionsMap from "./extension_map.json";
|
2018-12-11 22:56:32 +00:00
|
|
|
|
|
|
|
const dirViewerTemplate = `
|
|
|
|
<!DOCTYPE html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
|
|
<title>Deno File Server</title>
|
|
|
|
<style>
|
|
|
|
td {
|
|
|
|
padding: 0 1rem;
|
|
|
|
}
|
|
|
|
td.mode {
|
|
|
|
font-family: Courier;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Index of <%DIRNAME%></h1>
|
|
|
|
<table>
|
|
|
|
<tr><th>Mode</th><th>Size</th><th>Name</th></tr>
|
|
|
|
<%CONTENTS%>
|
|
|
|
</table>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
`;
|
2018-11-07 18:16:07 +00:00
|
|
|
|
2018-12-24 03:50:49 +00:00
|
|
|
const serverArgs = args.slice();
|
|
|
|
let CORSEnabled = false;
|
|
|
|
// TODO: switch to flags if we later want to add more options
|
|
|
|
for (let i = 0; i < serverArgs.length; i++) {
|
|
|
|
if (serverArgs[i] === "--cors") {
|
|
|
|
CORSEnabled = true;
|
|
|
|
serverArgs.splice(i, 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2018-12-10 01:17:55 +00:00
|
|
|
let currentDir = cwd();
|
2018-12-24 03:50:49 +00:00
|
|
|
const target = serverArgs[1];
|
2018-12-10 01:17:55 +00:00
|
|
|
if (target) {
|
|
|
|
currentDir = `${currentDir}/${target}`;
|
|
|
|
}
|
2018-12-24 03:50:49 +00:00
|
|
|
const addr = `0.0.0.0:${serverArgs[2] || 4500}`;
|
2018-12-09 20:35:26 +00:00
|
|
|
const encoder = new TextEncoder();
|
|
|
|
|
2018-12-11 22:56:32 +00:00
|
|
|
function modeToString(isDir: boolean, maybeMode: number | null) {
|
|
|
|
const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"];
|
2018-11-07 18:16:07 +00:00
|
|
|
|
2018-12-11 22:56:32 +00:00
|
|
|
if (maybeMode === null) {
|
|
|
|
return "(unknown mode)";
|
|
|
|
}
|
|
|
|
const mode = maybeMode!.toString(8);
|
|
|
|
if (mode.length < 3) {
|
|
|
|
return "(unknown mode)";
|
|
|
|
}
|
|
|
|
let output = "";
|
|
|
|
mode
|
|
|
|
.split("")
|
|
|
|
.reverse()
|
|
|
|
.slice(0, 3)
|
|
|
|
.forEach(v => {
|
|
|
|
output = modeMap[+v] + output;
|
|
|
|
});
|
|
|
|
output = `(${isDir ? "d" : "-"}${output})`;
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
function fileLenToString(len: number) {
|
|
|
|
const multipler = 1024;
|
|
|
|
let base = 1;
|
|
|
|
const suffix = ["B", "K", "M", "G", "T"];
|
|
|
|
let suffixIndex = 0;
|
|
|
|
|
|
|
|
while (base * multipler < len) {
|
|
|
|
if (suffixIndex >= suffix.length - 1) {
|
|
|
|
break;
|
2018-12-09 20:35:26 +00:00
|
|
|
}
|
2018-12-11 22:56:32 +00:00
|
|
|
base *= multipler;
|
|
|
|
suffixIndex++;
|
2018-12-09 20:35:26 +00:00
|
|
|
}
|
2018-12-11 22:56:32 +00:00
|
|
|
|
|
|
|
return `${(len / base).toFixed(2)}${suffix[suffixIndex]}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
function createDirEntryDisplay(
|
|
|
|
name: string,
|
|
|
|
path: string,
|
|
|
|
size: number | null,
|
|
|
|
mode: number | null,
|
|
|
|
isDir: boolean
|
|
|
|
) {
|
|
|
|
const sizeStr = size === null ? "" : "" + fileLenToString(size!);
|
|
|
|
return `
|
|
|
|
<tr><td class="mode">${modeToString(
|
|
|
|
isDir,
|
|
|
|
mode
|
|
|
|
)}</td><td>${sizeStr}</td><td><a href="${path}">${name}${
|
|
|
|
isDir ? "/" : ""
|
|
|
|
}</a></td>
|
|
|
|
</tr>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: simplify this after deno.stat and deno.readDir are fixed
|
|
|
|
async function serveDir(req: ServerRequest, dirPath: string, dirName: string) {
|
|
|
|
// dirname has no prefix
|
|
|
|
const listEntry: string[] = [];
|
|
|
|
const fileInfos = await readDir(dirPath);
|
|
|
|
for (const info of fileInfos) {
|
|
|
|
if (info.name === "index.html" && info.isFile()) {
|
|
|
|
// in case index.html as dir...
|
2018-12-12 17:47:58 +00:00
|
|
|
return await serveFile(req, info.path);
|
2018-12-11 22:56:32 +00:00
|
|
|
}
|
|
|
|
// Yuck!
|
|
|
|
let mode = null;
|
|
|
|
try {
|
|
|
|
mode = (await stat(info.path)).mode;
|
|
|
|
} catch (e) {}
|
|
|
|
listEntry.push(
|
|
|
|
createDirEntryDisplay(
|
|
|
|
info.name,
|
|
|
|
dirName + "/" + info.name,
|
|
|
|
info.isFile() ? info.len : null,
|
|
|
|
mode,
|
|
|
|
info.isDirectory()
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const page = new TextEncoder().encode(
|
|
|
|
dirViewerTemplate
|
|
|
|
.replace("<%DIRNAME%>", dirName + "/")
|
|
|
|
.replace("<%CONTENTS%>", listEntry.join(""))
|
|
|
|
);
|
|
|
|
|
2018-12-09 20:35:26 +00:00
|
|
|
const headers = new Headers();
|
2018-12-11 22:56:32 +00:00
|
|
|
headers.set("content-type", "text/html");
|
|
|
|
|
|
|
|
const res = {
|
|
|
|
status: 200,
|
|
|
|
body: page,
|
|
|
|
headers
|
|
|
|
};
|
|
|
|
setContentLength(res);
|
2018-12-12 09:38:46 +00:00
|
|
|
return res;
|
2018-12-11 22:56:32 +00:00
|
|
|
}
|
|
|
|
|
2018-12-31 09:06:06 +00:00
|
|
|
function guessContentType(filename: string): string {
|
|
|
|
let extension = extname(filename);
|
|
|
|
let contentType = extensionsMap[extension];
|
|
|
|
|
|
|
|
if (contentType) {
|
|
|
|
return contentType;
|
|
|
|
}
|
|
|
|
|
|
|
|
extension = extension.toLowerCase();
|
|
|
|
contentType = extensionsMap[extension];
|
|
|
|
|
|
|
|
if (contentType) {
|
|
|
|
return contentType;
|
|
|
|
}
|
|
|
|
|
2019-01-01 23:45:41 +00:00
|
|
|
return extensionsMap[""];
|
2018-12-31 09:06:06 +00:00
|
|
|
}
|
|
|
|
|
2018-12-11 22:56:32 +00:00
|
|
|
async function serveFile(req: ServerRequest, filename: string) {
|
2018-12-17 16:49:10 +00:00
|
|
|
const file = await open(filename);
|
|
|
|
const fileInfo = await stat(filename);
|
2018-12-11 22:56:32 +00:00
|
|
|
const headers = new Headers();
|
2018-12-17 16:49:10 +00:00
|
|
|
headers.set("content-length", fileInfo.len.toString());
|
2018-12-31 09:06:06 +00:00
|
|
|
headers.set("content-type", guessContentType(filename));
|
2018-12-09 20:35:26 +00:00
|
|
|
|
|
|
|
const res = {
|
|
|
|
status: 200,
|
|
|
|
body: file,
|
2018-12-11 22:56:32 +00:00
|
|
|
headers
|
|
|
|
};
|
2018-12-12 09:38:46 +00:00
|
|
|
return res;
|
2018-12-11 22:56:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function serveFallback(req: ServerRequest, e: Error) {
|
|
|
|
if (
|
|
|
|
e instanceof DenoError &&
|
|
|
|
(e as DenoError<any>).kind === ErrorKind.NotFound
|
|
|
|
) {
|
2018-12-17 16:49:10 +00:00
|
|
|
return {
|
|
|
|
status: 404,
|
|
|
|
body: encoder.encode("Not found")
|
2018-12-12 09:38:46 +00:00
|
|
|
};
|
2018-12-11 22:56:32 +00:00
|
|
|
} else {
|
2018-12-12 09:38:46 +00:00
|
|
|
return {
|
2018-12-11 22:56:32 +00:00
|
|
|
status: 500,
|
|
|
|
body: encoder.encode("Internal server error")
|
2018-12-12 09:38:46 +00:00
|
|
|
};
|
2018-12-11 22:56:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-12 09:38:46 +00:00
|
|
|
function serverLog(req: ServerRequest, res: Response) {
|
|
|
|
const d = new Date().toISOString();
|
|
|
|
const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`;
|
|
|
|
const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`;
|
|
|
|
console.log(s);
|
|
|
|
}
|
|
|
|
|
2018-12-24 03:50:49 +00:00
|
|
|
function setCORS(res: Response) {
|
|
|
|
if (!res.headers) {
|
|
|
|
res.headers = new Headers();
|
|
|
|
}
|
|
|
|
res.headers!.append("access-control-allow-origin", "*");
|
|
|
|
res.headers!.append(
|
|
|
|
"access-control-allow-headers",
|
|
|
|
"Origin, X-Requested-With, Content-Type, Accept, Range"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-12-11 22:56:32 +00:00
|
|
|
listenAndServe(addr, async req => {
|
|
|
|
const fileName = req.url.replace(/\/$/, "");
|
|
|
|
const filePath = currentDir + fileName;
|
|
|
|
|
2018-12-12 09:38:46 +00:00
|
|
|
let response: Response;
|
|
|
|
|
2018-12-11 22:56:32 +00:00
|
|
|
try {
|
|
|
|
const fileInfo = await stat(filePath);
|
|
|
|
if (fileInfo.isDirectory()) {
|
|
|
|
// Bug with deno.stat: name and path not populated
|
|
|
|
// Yuck!
|
2018-12-12 09:38:46 +00:00
|
|
|
response = await serveDir(req, filePath, fileName);
|
2018-12-11 22:56:32 +00:00
|
|
|
} else {
|
2018-12-12 09:38:46 +00:00
|
|
|
response = await serveFile(req, filePath);
|
2018-12-11 22:56:32 +00:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2018-12-12 09:38:46 +00:00
|
|
|
response = await serveFallback(req, e);
|
|
|
|
} finally {
|
2018-12-24 03:50:49 +00:00
|
|
|
if (CORSEnabled) {
|
|
|
|
setCORS(response);
|
|
|
|
}
|
2018-12-12 09:38:46 +00:00
|
|
|
serverLog(req, response);
|
|
|
|
req.respond(response);
|
2018-12-11 22:56:32 +00:00
|
|
|
}
|
2018-11-07 18:16:07 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
console.log(`HTTP server listening on http://${addr}/`);
|