summaryrefslogtreecommitdiff
path: root/http/file_server.ts
diff options
context:
space:
mode:
Diffstat (limited to 'http/file_server.ts')
-rwxr-xr-xhttp/file_server.ts241
1 files changed, 241 insertions, 0 deletions
diff --git a/http/file_server.ts b/http/file_server.ts
new file mode 100755
index 000000000..4437a44e4
--- /dev/null
+++ b/http/file_server.ts
@@ -0,0 +1,241 @@
+#!/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
+
+import {
+ listenAndServe,
+ ServerRequest,
+ setContentLength,
+ Response
+} from "./mod.ts";
+import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno";
+import { extname } from "../fs/path.ts";
+import { contentType } from "../media_types/mod.ts";
+
+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>
+`;
+
+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;
+ }
+}
+let currentDir = cwd();
+const target = serverArgs[1];
+if (target) {
+ currentDir = `${currentDir}/${target}`;
+}
+const addr = `0.0.0.0:${serverArgs[2] || 4500}`;
+const encoder = new TextEncoder();
+
+function modeToString(isDir: boolean, maybeMode: number | null) {
+ const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"];
+
+ 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;
+ }
+ base *= multipler;
+ suffixIndex++;
+ }
+
+ 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...
+ return await serveFile(req, info.path);
+ }
+ // 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(""))
+ );
+
+ const headers = new Headers();
+ headers.set("content-type", "text/html");
+
+ const res = {
+ status: 200,
+ body: page,
+ headers
+ };
+ setContentLength(res);
+ return res;
+}
+
+async function serveFile(req: ServerRequest, filename: string) {
+ const file = await open(filename);
+ const fileInfo = await stat(filename);
+ const headers = new Headers();
+ headers.set("content-length", fileInfo.len.toString());
+ headers.set("content-type", contentType(extname(filename)) || "text/plain");
+
+ const res = {
+ status: 200,
+ body: file,
+ headers
+ };
+ return res;
+}
+
+async function serveFallback(req: ServerRequest, e: Error) {
+ if (
+ e instanceof DenoError &&
+ (e as DenoError<any>).kind === ErrorKind.NotFound
+ ) {
+ return {
+ status: 404,
+ body: encoder.encode("Not found")
+ };
+ } else {
+ return {
+ status: 500,
+ body: encoder.encode("Internal server error")
+ };
+ }
+}
+
+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);
+}
+
+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"
+ );
+}
+
+listenAndServe(addr, async req => {
+ const fileName = req.url.replace(/\/$/, "");
+ const filePath = currentDir + fileName;
+
+ let response: Response;
+
+ try {
+ const fileInfo = await stat(filePath);
+ if (fileInfo.isDirectory()) {
+ // Bug with deno.stat: name and path not populated
+ // Yuck!
+ response = await serveDir(req, filePath, fileName);
+ } else {
+ response = await serveFile(req, filePath);
+ }
+ } catch (e) {
+ response = await serveFallback(req, e);
+ } finally {
+ if (CORSEnabled) {
+ setCORS(response);
+ }
+ serverLog(req, response);
+ req.respond(response);
+ }
+});
+
+console.log(`HTTP server listening on http://${addr}/`);