Skip to content

Commit

Permalink
better html for file_server (denoland#3423)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhmushan authored and ry committed Dec 3, 2019
1 parent 136b5e3 commit cfa4f54
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 82 deletions.
195 changes: 122 additions & 73 deletions std/http/file_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,22 @@
// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js

const { ErrorKind, cwd, args, stat, readDir, open } = Deno;
import { contentType } from "../media_types/mod.ts";
import { extname, posix } from "../path/mod.ts";
import { posix } from "../path/mod.ts";
import {
listenAndServe,
ServerRequest,
setContentLength,
Response
} from "./server.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>
`;
interface EntryInfo {
mode: string;
size: string;
url: string;
name: string;
}

const encoder = new TextEncoder();
const serverArgs = args.slice();
let CORSEnabled = false;
// TODO: switch to flags if we later want to add more options
Expand All @@ -58,7 +38,6 @@ const target = posix.isAbsolute(targetArg)
? posix.normalize(targetArg)
: posix.join(cwd(), targetArg);
const addr = `0.0.0.0:${serverArgs[2] || 4500}`;
const encoder = new TextEncoder();

function modeToString(isDir: boolean, maybeMode: number | null): string {
const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"];
Expand Down Expand Up @@ -99,25 +78,6 @@ function fileLenToString(len: number): string {
return `${(len / base).toFixed(2)}${suffix[suffixIndex]}`;
}

function createDirEntryDisplay(
name: string,
url: string,
size: number | null,
mode: number | null,
isDir: boolean
): string {
const sizeStr = size === null ? "" : "" + fileLenToString(size!);
return `
<tr><td class="mode">${modeToString(
isDir,
mode
)}</td><td>${sizeStr}</td><td><a href="${url}">${name}${
isDir ? "/" : ""
}</a></td>
</tr>
`;
}

async function serveFile(
req: ServerRequest,
filePath: string
Expand All @@ -126,7 +86,7 @@ async function serveFile(
const fileInfo = await stat(filePath);
const headers = new Headers();
headers.set("content-length", fileInfo.len.toString());
headers.set("content-type", contentType(extname(filePath)) || "text/plain");
headers.set("content-type", "text/plain");

const res = {
status: 200,
Expand All @@ -141,12 +101,8 @@ async function serveDir(
req: ServerRequest,
dirPath: string
): Promise<Response> {
interface ListItem {
name: string;
template: string;
}
const dirUrl = `/${posix.relative(target, dirPath)}`;
const listEntry: ListItem[] = [];
const listEntry: EntryInfo[] = [];
const fileInfos = await readDir(dirPath);
for (const fileInfo of fileInfos) {
const filePath = posix.join(dirPath, fileInfo.name);
Expand All @@ -161,29 +117,17 @@ async function serveDir(
mode = (await stat(filePath)).mode;
} catch (e) {}
listEntry.push({
mode: modeToString(fileInfo.isDirectory(), mode),
size: fileInfo.isFile() ? fileLenToString(fileInfo.len) : "",
name: fileInfo.name,
template: createDirEntryDisplay(
fileInfo.name,
fileUrl,
fileInfo.isFile() ? fileInfo.len : null,
mode,
fileInfo.isDirectory()
)
url: fileUrl
});
}

const formattedDirUrl = `${dirUrl.replace(/\/$/, "")}/`;
const page = new TextEncoder().encode(
dirViewerTemplate.replace("<%DIRNAME%>", formattedDirUrl).replace(
"<%CONTENTS%>",
listEntry
.sort((a, b): number =>
a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
)
.map((v): string => v.template)
.join("")
)
listEntry.sort((a, b) =>
a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
);
const formattedDirUrl = `${dirUrl.replace(/\/$/, "")}/`;
const page = encoder.encode(dirViewerTemplate(formattedDirUrl, listEntry));

const headers = new Headers();
headers.set("content-type", "text/html");
Expand Down Expand Up @@ -232,6 +176,111 @@ function setCORS(res: Response): void {
);
}

function dirViewerTemplate(dirname: string, entries: EntryInfo[]): string {
return html`
<!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>
:root {
--background-color: #fafafa;
--color: rgba(0, 0, 0, 0.87);
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #303030;
--color: #fff;
}
}
@media (min-width: 960px) {
main {
max-width: 960px;
}
body {
padding-left: 32px;
padding-right: 32px;
}
}
@media (min-width: 600px) {
main {
padding-left: 24px;
padding-right: 24px;
}
}
body {
background: var(--background-color);
color: var(--color);
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
font-weight: 400;
line-height: 1.43;
font-size: 0.875rem;
}
a {
color: #2196f3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
table th {
text-align: left;
}
table td {
padding: 12px 24px 0 0;
}
</style>
</head>
<body>
<main>
<h1>Index of ${dirname}</h1>
<table>
<tr>
<th>Mode</th>
<th>Size</th>
<th>Name</th>
</tr>
${entries.map(
entry => html`
<tr>
<td class="mode">
${entry.mode}
</td>
<td>
${entry.size}
</td>
<td>
<a href="${entry.url}">${entry.name}</a>
</td>
</tr>
`
)}
</table>
</main>
</body>
</html>
`;
}

function html(strings: TemplateStringsArray, ...values: unknown[]): string {
const l = strings.length - 1;
let html = "";

for (let i = 0; i < l; i++) {
let v = values[i];
if (v instanceof Array) {
v = v.join("");
}
const s = strings[i] + v;
html += s;
}
html += strings[l];
return html;
}

listenAndServe(
addr,
async (req): Promise<void> => {
Expand Down
10 changes: 3 additions & 7 deletions std/http/file_server_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ test(async function serveFile(): Promise<void> {
const res = await fetch("http:https://localhost:4500/README.md");
assert(res.headers.has("access-control-allow-origin"));
assert(res.headers.has("access-control-allow-headers"));
assertEquals(
res.headers.get("content-type"),
"text/markdown; charset=utf-8"
);
const downloadedFile = await res.text();
const localFile = new TextDecoder().decode(
await Deno.readFile("README.md")
Expand All @@ -63,10 +59,10 @@ test(async function serveDirectory(): Promise<void> {
// TODO: `mode` should work correctly in the future.
// Correct this test case accordingly.
Deno.build.os !== "win" &&
assert(/<td class="mode">\([a-zA-Z-]{10}\)<\/td>/.test(page));
assert(/<td class="mode">(\s)*\([a-zA-Z-]{10}\)(\s)*<\/td>/.test(page));
Deno.build.os === "win" &&
assert(/<td class="mode">\(unknown mode\)<\/td>/.test(page));
assert(page.includes(`<td><a href="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/README.md">README.md</a></td>`));
assert(/<td class="mode">(\s)*\(unknown mode\)(\s)*<\/td>/.test(page));
assert(page.includes(`<a href="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/README.md">README.md</a>`));
} finally {
killFileServer();
}
Expand Down
6 changes: 4 additions & 2 deletions std/http/testdata/simple_https_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ const tlsOptions = {
hostname: "localhost",
port: 4503,
certFile: "./http/testdata/tls/localhost.crt",
keyFile: "./http/testdata/tls/localhost.key",
keyFile: "./http/testdata/tls/localhost.key"
};
const s = serveTLS(tlsOptions);
console.log(`Simple HTTPS server listening on ${tlsOptions.hostname}:${tlsOptions.port}`);
console.log(
`Simple HTTPS server listening on ${tlsOptions.hostname}:${tlsOptions.port}`
);
const body = new TextEncoder().encode("Hello HTTPS");
for await (const req of s) {
req.respond({ body });
Expand Down

0 comments on commit cfa4f54

Please sign in to comment.