mirror of
https://github.com/denoland/deno.git
synced 2024-11-30 16:40:57 -05:00
Serve directory for file server & Fix bufio flush bug (#15)
This commit is contained in:
parent
f1f1f39cd3
commit
b78f4e9fbd
6 changed files with 240 additions and 27 deletions
|
@ -5,4 +5,4 @@ install:
|
||||||
- export PATH="$HOME/.deno/bin:$PATH"
|
- export PATH="$HOME/.deno/bin:$PATH"
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- deno test.ts
|
- deno test.ts --allow-run --allow-net
|
||||||
|
|
2
bufio.ts
2
bufio.ts
|
@ -425,7 +425,7 @@ export class BufWriter implements Writer {
|
||||||
} else {
|
} else {
|
||||||
n = copyBytes(this.buf, p, this.n);
|
n = copyBytes(this.buf, p, this.n);
|
||||||
this.n += n;
|
this.n += n;
|
||||||
this.flush();
|
await this.flush();
|
||||||
}
|
}
|
||||||
nn += n;
|
nn += n;
|
||||||
p = p.subarray(n);
|
p = p.subarray(n);
|
||||||
|
|
189
file_server.ts
189
file_server.ts
|
@ -5,42 +5,191 @@
|
||||||
// TODO Add tests like these:
|
// TODO Add tests like these:
|
||||||
// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js
|
// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js
|
||||||
|
|
||||||
import { listenAndServe } from "./http";
|
import { listenAndServe, ServerRequest, setContentLength } from "./http";
|
||||||
import { cwd, readFile, DenoError, ErrorKind, args } from "deno";
|
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();
|
let currentDir = cwd();
|
||||||
const target = args[1];
|
const target = args[1];
|
||||||
if (target) {
|
if (target) {
|
||||||
currentDir = `${currentDir}/${target}`;
|
currentDir = `${currentDir}/${target}`;
|
||||||
}
|
}
|
||||||
|
const addr = `0.0.0.0:${args[2] || 4500}`;
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
listenAndServe(addr, async req => {
|
function modeToString(isDir: boolean, maybeMode: number | null) {
|
||||||
const fileName = req.url.replace(/\/$/, '/index.html');
|
const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"];
|
||||||
const filePath = currentDir + fileName;
|
|
||||||
let file;
|
|
||||||
|
|
||||||
try {
|
if (maybeMode === null) {
|
||||||
file = await readFile(filePath);
|
return "(unknown mode)";
|
||||||
} 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;
|
|
||||||
}
|
}
|
||||||
|
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();
|
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 = {
|
const res = {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: file,
|
body: file,
|
||||||
headers,
|
headers
|
||||||
}
|
};
|
||||||
await req.respond(res);
|
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}/`);
|
console.log(`HTTP server listening on http://${addr}/`);
|
||||||
|
|
46
file_server_test.ts
Normal file
46
file_server_test.ts
Normal 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
13
http.ts
|
@ -82,7 +82,10 @@ export async function* serve(addr: string) {
|
||||||
listener.close();
|
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);
|
const server = serve(addr);
|
||||||
|
|
||||||
for await (const request of server) {
|
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;
|
status?: number;
|
||||||
headers?: Headers;
|
headers?: Headers;
|
||||||
body?: Uint8Array;
|
body?: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setContentLength(r: Response): void {
|
export function setContentLength(r: Response): void {
|
||||||
if (!r.headers) {
|
if (!r.headers) {
|
||||||
r.headers = new Headers();
|
r.headers = new Headers();
|
||||||
}
|
}
|
||||||
if (!r.headers.has("content-length")) {
|
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());
|
r.headers.append("Content-Length", bodyLength.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerRequest {
|
export class ServerRequest {
|
||||||
url: string;
|
url: string;
|
||||||
method: string;
|
method: string;
|
||||||
proto: string;
|
proto: string;
|
||||||
|
|
15
test.ts
15
test.ts
|
@ -1,4 +1,19 @@
|
||||||
|
import { run } from "deno";
|
||||||
|
|
||||||
import "./buffer_test.ts";
|
import "./buffer_test.ts";
|
||||||
import "./bufio_test.ts";
|
import "./bufio_test.ts";
|
||||||
import "./textproto_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";
|
// TODO import "./http_test.ts";
|
||||||
|
|
Loading…
Reference in a new issue