Serve directory for file server & Fix bufio flush bug (#15)

This commit is contained in:
Kevin (Kun) "Kassimo" Qian 2018-12-11 17:56:32 -05:00 committed by Ryan Dahl
parent f1f1f39cd3
commit b78f4e9fbd
6 changed files with 240 additions and 27 deletions

View File

@ -5,4 +5,4 @@ install:
- export PATH="$HOME/.deno/bin:$PATH"
script:
- deno test.ts
- deno test.ts --allow-run --allow-net

View File

@ -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);

View File

@ -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") });
}
return;
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...
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}/`);

46
file_server_test.ts Normal file
View File

@ -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();
});
}

13
http.ts
View File

@ -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;

15
test.ts
View File

@ -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";