import { CONTENT_TYPE } from "https://js.sabae.cc/CONTENT_TYPE.js"; import { UUID } from "https://js.sabae.cc/UUID.js"; import { getExtension } from "https://js.sabae.cc/getExtension.js"; import { parseURL } from "https://js.sabae.cc/parseURL.js"; import { serve } from "https://deno.land/std@0.114.0/http/server.ts"; import { DateTime } from "https://js.sabae.cc/DateTime.js"; const getFileNameFromDate = () => { const d = new Date(); const fix0 = (n) => n < 10 ? "0" + n : n.toString(); const ymd = d.getFullYear() + fix0(d.getMonth() + 1) + fix0(d.getDate()); return ymd + "/" + UUID.generate(); }; const DENO_BUF_SIZE = 32 * 1024; const readFilePartial = async (fn, offset, len) => { const f = await Deno.open(fn); //console.log(fn, offset, len); await Deno.seek(f.rid, offset, Deno.SeekMode.Start); const buf = new Uint8Array(len); const rbuf = new Uint8Array(DENO_BUF_SIZE); let off = 0; for (;;) { const rlen = await Deno.read(f.rid, rbuf); for (let i = 0; i < rlen; i++) { buf[off + i] = rbuf[i]; } //console.log(off, rlen); off += rlen; len -= rlen; if (len == 0) { break; } } await Deno.close(f.rid); return buf; }; const RANGE_LEN = 1024 * 1024 * 10; const readFileRange = async (fn, range) => { let gzip = true; let data = null; let range0 = 0; let range1 = RANGE_LEN - 1; if (range) { range0 = parseInt(range[0]); if (range[1] != "") { range1 = parseInt(range[1]); } else { range1 += range0; } } let flen = 0; try { /* // unsupported gzip & range request //data = Deno.readFileSync(fn + ".gz"); flen = (await Deno.stat(fn + ".gz")).size; if (range1 >= flen) { range1 = flen - 1; } data = await readFilePartial(fn + ".gz", range0, range1 - range0 + 1); */ flen = (await Deno.stat(fn + ".gz")).size; if (flen < RANGE_LEN) { data = await Deno.readFile(fn + ".gz"); return [data, data.length, gzip]; } } catch (e) { } gzip = false; flen = (await Deno.stat(fn)).size; if (range1 >= flen) { range1 = flen - 1; } data = await readFilePartial(fn, range0, range1 - range0 + 1); return [data, flen, gzip]; }; let logpath = null; { logpath = Deno.env.get("SERVER_LOG_PATH"); if (logpath && !logpath.endsWith("/")) { logpath += "/"; } } class Server { constructor(port) { this.start(port); } async handleApi(req) { //const url = req.url; const path = req.path; if (req.method === "OPTIONS") { const res = "ok"; return new Response(JSON.stringify(res), { status: 200, headers: new Headers({ "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type, Accept", // "Access-Control-Allow-Methods": "PUT, DELETE, PATCH", }) }); } try { const json = await (async () => { if (req.method === "POST") { return await req.json(); } else if (req.method == "DELETE") { return null; // no requets } else if (req.method === "GET") { const n = req.url.indexOf("?"); const sjson = decodeURIComponent(req.url.substring(n + 1)); try { return JSON.parse(sjson); } catch (e) { return sjson; } } return null; })(); //console.log("[req api]", json); const res = await this.api(path, json, req.remoteAddr, req); if (res instanceof Response) { return res; } //console.log("[res api]", res); return new Response(JSON.stringify(res), { status: 200, headers: new Headers({ "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type, Accept", // must //"Access-Control-Allow-Methods": "PUT, DELETE, PATCH", }) }); } catch (e) { this.err(e); } return null; }; async handleData(req) { //const url = req.url; const path = req.path; try { if (req.method === "POST") { const ext = getExtension(path, ".jpg"); const bin = new Uint8Array(await req.arrayBuffer()); //console.log("[req data]", bin.length); const fn = getFileNameFromDate(); const name = fn + ext; try { Deno.mkdirSync("data"); } catch (e) { } try { const dir = name.substring(0, name.lastIndexOf("/")); Deno.mkdirSync("data/" + dir); } catch (e) { } Deno.writeFileSync("data/" + name, bin); const res = { name }; //console.log("[data res]", res); return new Response(JSON.stringify(res), { status: 200, headers: new Headers({ "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type, Accept", // must //"Access-Control-Allow-Methods": "PUT, DELETE, PATCH", }) }); } else if (req.method === "GET" || req.method === "HEAD") { const fn = path; if (fn.indexOf("..") >= 0) { throw new Error("illegal filename"); } const n = fn.lastIndexOf("."); const ext = n < 0 ? "html" : fn.substring(n + 1); const data = Deno.readFileSync("." + fn); const ctype = CONTENT_TYPE[ext] || "text/plain"; return new Response(req.method === "HEAD" ? null : data, { status: 200, headers: new Headers({ "Content-Type": ctype, "Access-Control-Allow-Origin": "*", "Content-Length": data.length, }), }); } } catch (e) { //console.log("err", e.stack); } } async handleWeb(req) { //const url = req.url; const path = req.path; try { //console.log(path, req.headers); const getRange = (req) => { const range = req.headers.get("Range"); if (!range || !range.startsWith("bytes=")) { return null; } const res = range.substring(6).split("-"); if (res.length === 0) { return null; } return res; }; let range = getRange(req); const calcPath = (path) => { if (path === "/" || path.indexOf("..") >= 0) { return "/index.html"; } if (path.endsWith("/")) { return path + "index.html"; } return path; }; const fn = calcPath(path); const n = fn.lastIndexOf("."); const ext = n < 0 ? "html" : fn.substring(n + 1); const [data, totallen, gzip] = await readFileRange("static" + fn, range); if (!range) { if (data.length != totallen) { range = [0, data.length - 1]; } } else if (range[1] == "") { range[1] = parseInt(range[0]) + data.length - 1; } const ctype = CONTENT_TYPE[ext] || "text/plain"; const headers = { "Content-Type": ctype, "Accept-Ranges": "bytes", "Content-Length": data.length, }; if (gzip) { headers["Content-Encoding"] = "gzip"; } if (totallen == data.length) { range = null; } if (range) { headers["Content-Range"] = "bytes " + range[0] + "-" + range[1] + "/" + totallen; } return new Response(data, { status: range ? 206 : 200, headers: new Headers(headers), }); } catch (e) { if (path !== "/favicon.ico") { //console.log("err", path, e.stack); } } } async start(port) { console.log(`http://localhost:${port}/`); const hostname = "[::]"; // for IPv6 const addr = hostname + ":" + port; serve(async (req, conn) => { const remoteAddr = conn.remoteAddr.hostname; //console.log("remoteAddr", remoteAddr); try { const url = req.url; const purl = parseURL(url); if (purl) { req.path = purl.path; req.query = purl.query; req.host = purl.host; req.port = purl.port; req.remoteAddr = remoteAddr; const path = req.path; await this.log(req); if (path.startsWith("/api/")) { const resd = await this.handleApi(req); if (resd) { return resd; } } else if (path.startsWith("/data/")) { const resd = await this.handleData(req); if (resd) { return resd; } } const resd = await this.handleWeb(req); if (resd) { return resd; } } return await this.handleNotFound(req); } catch (e) { this.err(e); } }, { addr }); } // log async log(req) { // /* req.path = purl.path; req.query = purl.query; req.host = purl.host; req.port = purl.port; req.remoteAddr = remoteAddr; */ if (logpath) { const dt = new DateTime(); const fn = logpath + dt.day.toStringYMD() + ".log"; const s = dt.toString() + "\t" + req.path + "\t" + req.remoteAddr + "\n"; await Deno.writeTextFile(fn, s, { append: true }); } } // err err(e) { console.log("err", e.stack); } // not found async handleNotFound(req) { // to override const err = new TextEncoder().encode("not found"); return new Response(err); } // Web API async api(path, req, remoteAddr) { // to override return req; } } export { Server }; /* if (Deno. run script name? .endsWith("/Server.js")) { const port = parseInt(Deno.args[0]); new Server(port); } */