diff --git a/.travis.yml b/.travis.yml
index b3b2391af9..8c4abc16f0 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
diff --git a/bufio.ts b/bufio.ts
index 819c610f94..a1f673653e 100644
--- a/bufio.ts
+++ b/bufio.ts
@@ -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 9dcac8704e..d2b9fe0b09 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 = `
+
+
+
+
+
+
+ Deno File Server
+
+
+
+ Index of <%DIRNAME%>
+
+ Mode | Size | Name |
+ <%CONTENTS%>
+
+
+
+`;
-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 `
+ ${modeToString(
+ isDir,
+ mode
+ )} | ${sizeStr} | ${name}${
+ isDir ? "/" : ""
+ } |
+
+ `;
+}
+
+// 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).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 0000000000..a04ced7e5c
--- /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) {
+ 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();
+ });
+}
diff --git a/http.ts b/http.ts
index b11e2b3698..6954a48ba2 100644
--- a/http.ts
+++ b/http.ts
@@ -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;
diff --git a/test.ts b/test.ts
index 2ee9a820b9..44a6920158 100644
--- a/test.ts
+++ b/test.ts
@@ -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";