#!/usr/bin/env deno --allow-net // This program serves files in the current directory over HTTP. // TODO Stream responses instead of reading them into memory. // TODO Add tests like these: // https://github.com/indexzero/http-server/blob/master/test/http-server-test.js import { listenAndServe, ServerRequest, setContentLength } from "./http"; import { cwd, readFile, DenoError, ErrorKind, args, stat, readDir } from "deno"; const dirViewerTemplate = ` Deno File Server

Index of <%DIRNAME%>

<%CONTENTS%>
ModeSizeName
`; 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(); function modeToString(isDir: boolean, maybeMode: number | null) { const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; 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", "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 }; 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}/`);