mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 23:34:47 -05:00
better html for file_server (#3423)
This commit is contained in:
parent
136b5e3da2
commit
cfa4f540ba
3 changed files with 129 additions and 82 deletions
|
@ -7,8 +7,7 @@
|
|||
// 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,
|
||||
|
@ -16,33 +15,14 @@ import {
|
|||
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
|
||||
|
@ -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"];
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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> => {
|
||||
|
|
|
@ -36,10 +36,6 @@ test(async function serveFile(): Promise<void> {
|
|||
const res = await fetch("http://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")
|
||||
|
@ -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="/README.md">README.md</a></td>`));
|
||||
assert(/<td class="mode">(\s)*\(unknown mode\)(\s)*<\/td>/.test(page));
|
||||
assert(page.includes(`<a href="/README.md">README.md</a>`));
|
||||
} finally {
|
||||
killFileServer();
|
||||
}
|
||||
|
|
6
std/http/testdata/simple_https_server.ts
vendored
6
std/http/testdata/simple_https_server.ts
vendored
|
@ -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 });
|
||||
|
|
Loading…
Reference in a new issue