diff options
| author | Kevin (Kun) "Kassimo" Qian <kevinkassimo@gmail.com> | 2018-12-11 17:56:32 -0500 |
|---|---|---|
| committer | Ryan Dahl <ry@tinyclouds.org> | 2018-12-11 17:56:32 -0500 |
| commit | b94530332910c0b85553dc91fa1912bce308df3c (patch) | |
| tree | 86034754d12b04aa8ee5afe541c4b98b78ca5ed0 | |
| parent | a691d9257b89cc210de6371138508c76aa288aba (diff) | |
Serve directory for file server & Fix bufio flush bug (denoland/deno_std#15)
Original: https://github.com/denoland/deno_std/commit/b78f4e9fbd477b026f1a309c64f4f8f75cd4d5d6
| -rw-r--r-- | .travis.yml | 2 | ||||
| -rw-r--r-- | bufio.ts | 2 | ||||
| -rwxr-xr-x | file_server.ts | 187 | ||||
| -rw-r--r-- | file_server_test.ts | 46 | ||||
| -rw-r--r-- | http.ts | 13 | ||||
| -rw-r--r-- | test.ts | 15 |
6 files changed, 239 insertions, 26 deletions
diff --git a/.travis.yml b/.travis.yml index b3b2391af..8c4abc16f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,4 @@ install: - export PATH="$HOME/.deno/bin:$PATH" script: -- deno test.ts +- deno test.ts --allow-run --allow-net @@ -425,7 +425,7 @@ export class BufWriter implements Writer { } else { n = copyBytes(this.buf, p, this.n); this.n += n; - this.flush(); + await this.flush(); } nn += n; p = p.subarray(n); diff --git a/file_server.ts b/file_server.ts index 9dcac8704..d2b9fe0b0 100755 --- a/file_server.ts +++ b/file_server.ts @@ -5,42 +5,191 @@ // TODO Add tests like these: // https://github.com/indexzero/http-server/blob/master/test/http-server-test.js -import { listenAndServe } from "./http"; -import { cwd, readFile, DenoError, ErrorKind, args } from "deno"; +import { listenAndServe, ServerRequest, setContentLength } from "./http"; +import { cwd, readFile, DenoError, ErrorKind, args, stat, readDir } from "deno"; + +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 addr = "0.0.0.0:4500"; let currentDir = cwd(); const target = args[1]; if (target) { currentDir = `${currentDir}/${target}`; } +const addr = `0.0.0.0:${args[2] || 4500}`; const encoder = new TextEncoder(); -listenAndServe(addr, async req => { - const fileName = req.url.replace(/\/$/, '/index.html'); - const filePath = currentDir + fileName; - let file; +function modeToString(isDir: boolean, maybeMode: number | null) { + const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; - try { - file = await readFile(filePath); - } catch (e) { - if (e instanceof DenoError && e.kind === ErrorKind.NotFound) { - await req.respond({ status: 404, body: encoder.encode("Not found") }); - } else { - await req.respond({ status: 500, body: encoder.encode("Internal server error") }); + 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; } - return; + 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... + await serveFile(req, info.path); + return; + } + // 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', 'octet-stream'); + headers.set("content-type", "text/html"); + + const res = { + status: 200, + body: page, + headers + }; + setContentLength(res); + await req.respond(res); +} + +async function serveFile(req: ServerRequest, filename: string) { + let file = await readFile(filename); + const headers = new Headers(); + headers.set("content-type", "octet-stream"); const res = { status: 200, body: file, - headers, - } + headers + }; await req.respond(res); +} + +async function serveFallback(req: ServerRequest, e: Error) { + if ( + e instanceof DenoError && + (e as DenoError<any>).kind === ErrorKind.NotFound + ) { + await req.respond({ status: 404, body: encoder.encode("Not found") }); + } else { + await req.respond({ + status: 500, + body: encoder.encode("Internal server error") + }); + } +} + +listenAndServe(addr, async req => { + const fileName = req.url.replace(/\/$/, ""); + const filePath = currentDir + fileName; + + try { + const fileInfo = await stat(filePath); + if (fileInfo.isDirectory()) { + // Bug with deno.stat: name and path not populated + // Yuck! + await serveDir(req, filePath, fileName); + } else { + await serveFile(req, filePath); + } + } catch (e) { + await serveFallback(req, e); + return; + } }); console.log(`HTTP server listening on http://${addr}/`); diff --git a/file_server_test.ts b/file_server_test.ts new file mode 100644 index 000000000..a04ced7e5 --- /dev/null +++ b/file_server_test.ts @@ -0,0 +1,46 @@ +import { readFile } from "deno"; + +import { + test, + assert, + assertEqual +} from "https://deno.land/x/testing/testing.ts"; + +// Promise to completeResolve when all tests completes +let completeResolve; +export const completePromise = new Promise(res => (completeResolve = res)); +let completedTestCount = 0; + +function maybeCompleteTests() { + completedTestCount++; + // Change this when adding more tests + if (completedTestCount === 3) { + completeResolve(); + } +} + +export function runTests(serverReadyPromise: Promise<any>) { + test(async function serveFile() { + await serverReadyPromise; + const res = await fetch("http://localhost:4500/.travis.yml"); + const downloadedFile = await res.text(); + const localFile = new TextDecoder().decode(await readFile("./.travis.yml")); + assertEqual(downloadedFile, localFile); + maybeCompleteTests(); + }); + + test(async function serveDirectory() { + await serverReadyPromise; + const res = await fetch("http://localhost:4500/"); + const page = await res.text(); + assert(page.includes(".travis.yml")); + maybeCompleteTests(); + }); + + test(async function serveFallback() { + await serverReadyPromise; + const res = await fetch("http://localhost:4500/badfile.txt"); + assertEqual(res.status, 404); + maybeCompleteTests(); + }); +} @@ -82,7 +82,10 @@ export async function* serve(addr: string) { listener.close(); } -export async function listenAndServe(addr: string, handler: (req: ServerRequest) => void) { +export async function listenAndServe( + addr: string, + handler: (req: ServerRequest) => void +) { const server = serve(addr); for await (const request of server) { @@ -90,23 +93,23 @@ export async function listenAndServe(addr: string, handler: (req: ServerRequest) } } -interface Response { +export interface Response { status?: number; headers?: Headers; body?: Uint8Array; } -function setContentLength(r: Response): void { +export function setContentLength(r: Response): void { if (!r.headers) { r.headers = new Headers(); } if (!r.headers.has("content-length")) { - const bodyLength = r.body ? r.body.byteLength : 0 + const bodyLength = r.body ? r.body.byteLength : 0; r.headers.append("Content-Length", bodyLength.toString()); } } -class ServerRequest { +export class ServerRequest { url: string; method: string; proto: string; @@ -1,4 +1,19 @@ +import { run } from "deno"; + import "./buffer_test.ts"; import "./bufio_test.ts"; import "./textproto_test.ts"; +import { runTests, completePromise } from "./file_server_test.ts"; + +// file server test +const fileServer = run({ + args: ["deno", "--allow-net", "file_server.ts", "."] +}); +// I am also too lazy to do this properly LOL +runTests(new Promise(res => setTimeout(res, 1000))); +(async () => { + await completePromise; + fileServer.close(); +})(); + // TODO import "./http_test.ts"; |
