This commit is contained in:
Flummi 2021-06-09 07:24:32 +02:00
parent a595aea282
commit d40fb2c5cf
3 changed files with 208 additions and 90 deletions

View File

@ -1,5 +1,3 @@
import path from "path";
import fs from "fs";
import http from "http";
import { URL } from "url";
import querystring from "querystring";
@ -10,90 +8,26 @@ import Tpl from "./template.mjs";
export { Router, Tpl };
export default class flummpress {
#mimes;
#server;
constructor() {
this.router = new Router();
this.tpl = new Tpl();
this.middleware = new Set();
return this;
};
use(obj) {
if(obj instanceof Router) {
this.router.routes = new Map([ ...this.router.routes, ...obj.routes ]);
this.router.sortRoutes();
this.router.use(obj);
}
else if(obj instanceof Tpl) {
this.tpl = obj;
}
return this;
};
static({ dir = path.resolve() + "/public", route = /^\/public/ }) {
if(!this.#mimes) {
this.#mimes = new Map();
(fs.readFileSync("./mime.types", "utf-8"))
.split("\n")
.filter(e => !e.startsWith("#") && e)
.map(e => e.split(/\s{2,}/))
.filter(e => e.length > 1)
.forEach(m => m[1].split(" ").forEach(ext => this.#mimes.set(ext, m[0])));
else {
if(!this.middleware.has(obj))
this.middleware.add(obj);
}
this.router.get(route, (req, res) => {
try {
const filename = req.url.pathname.replace(route, "") || "index.html";
const mime = this.#mimes.get(filename.split(".").pop());
const file = path.join(dir, filename);
let stat;
try {
stat = fs.statSync(file);
} catch {
res.reply({
code: 404,
body: "404 - file not found."
});
return this;
}
if(!mime.startsWith("video") && !mime.startsWith("audio")) {
res.reply({
type: this.#mimes.get(filename.split(".").pop()).toLowerCase(),
body: fs.readFileSync(path.join(dir, filename))
});
return this;
}
if(req.headers.range) {
const parts = req.headers.range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
res.writeHead(206, {
"Content-Range": `bytes ${start}-${end}/${stat.size}`,
"Accept-Ranges": "bytes",
"Content-Length": (end - start) + 1,
"Content-Type": mime,
});
const stream = fs.createReadStream(file, { start: start, end: end })
.on("open", () => stream.pipe(res))
.on("error", err => res.end(err));
}
else {
res.writeHead(200, {
"Content-Length": stat.size,
"Content-Type": mime,
});
fs.createReadStream(file).pipe(res);
}
} catch(err) {
console.error(err);
res.reply({
code: 500,
body: "500 - f0ck hat keinen b0ck"
});
}
});
return this;
};
@ -109,6 +43,16 @@ export default class flummpress {
qs: {...querystring.parse(_url.search.substring(1))} // legacy
};
req.cookies = {};
if(req.headers.cookie) {
req.headers.cookie.split("; ").forEach(c => {
const parts = c.split('=');
req.cookies[parts.shift().trim()] = decodeURI(parts.join('='));
});
}
await Promise.all([...this.middleware].map(m => m(req, res)));
const method = req.method.toLowerCase();
const route = this.router.getRoute(req.url.pathname, req.method);
@ -166,6 +110,10 @@ export default class flummpress {
}) => res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` }).end(body);
res.json = msg => res.reply({ type: "application/json", body: msg });
res.html = msg => res.reply({ type: "text/html", body: msg });
res.redirect = (target, code = 307) => res.writeHead(code, {
"Cache-Control": "no-cache, public",
"Location": target
}).end();
return res;
};

View File

@ -1,9 +1,27 @@
import fs from "fs";
import path from "path";
import Tpl from "./template.mjs";
export default class Router {
#mimes;
constructor() {
this.routes = new Map();
return this;
};
async importRoutesFromPath(p, tpl = false) {
await Promise.all(
fs.readdirSync(path.resolve() + "/" + p)
.filter(r => r.endsWith(".mjs"))
.map(async r => {
const tmp = (await import(`${path.resolve()}/${p}/${r}`)).default(this, tpl);
this.use(tmp);
})
);
return this;
};
group(path, cb) {
const methods = {
get: this.get.bind(this),
@ -25,6 +43,16 @@ export default class Router {
return this;
};
use(obj) {
if(obj instanceof Router) {
this.routes = new Map([ ...this.routes, ...obj.routes ]);
this.sortRoutes();
}
if(obj instanceof Tpl) {
this.tpl = obj;
}
}
get(path, ...args) {
if(args.length === 1)
this.registerRoute(path, args[0], "get");
@ -53,7 +81,7 @@ export default class Router {
this.sortRoutes();
return this;
};
getRoute(path, method) {
method = method.toLowerCase();
return [...this.routes.entries()].filter(r => {
@ -65,4 +93,75 @@ export default class Router {
this.routes = new Map([...this.routes.entries()].sort().reverse());
return this;
};
readMimes(file = "/etc/mime.types") {
this.#mimes = new Map();
(fs.readFileSync(file, "utf-8"))
.split("\n")
.filter(e => !e.startsWith("#") && e)
.map(e => e.split(/\s{2,}/))
.filter(e => e.length > 1)
.forEach(m => m[1].split(" ").forEach(ext => this.#mimes.set(ext, m[0])));
};
static({ dir = path.resolve() + "/public", route = /^\/public/ }) {
if(!this.#mimes)
this.readMimes();
this.get(route, (req, res) => {
try {
const filename = req.url.pathname.replace(route, "") || "index.html";
const mime = this.#mimes.get(filename.split(".").pop());
const file = path.join(dir, filename);
let stat;
try {
stat = fs.statSync(file);
} catch(err) {
console.log(err);
res.reply({
code: 404,
body: "404 - file not found."
});
return this;
}
if(!mime.startsWith("video") && !mime.startsWith("audio")) {
res.reply({
type: this.#mimes.get(filename.split(".").pop()).toLowerCase(),
body: fs.readFileSync(path.join(dir, filename))
});
return this;
}
if(req.headers.range) {
const parts = req.headers.range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
res.writeHead(206, {
"Content-Range": `bytes ${start}-${end}/${stat.size}`,
"Accept-Ranges": "bytes",
"Content-Length": (end - start) + 1,
"Content-Type": mime,
});
const stream = fs.createReadStream(file, { start: start, end: end })
.on("open", () => stream.pipe(res))
.on("error", err => res.end(err));
}
else {
res.writeHead(200, {
"Content-Length": stat.size,
"Content-Type": mime,
});
fs.createReadStream(file).pipe(res);
}
} catch(err) {
console.error(err);
res.reply({
code: 500,
body: "500 - internal server error"
});
}
});
return this;
};
};

View File

@ -2,38 +2,108 @@ import fs from "fs";
import path from "path";
export default class {
#views;
#globals;
#cache;
#templates;
#debug;
constructor() {
this.views = path.resolve() + "/views";
this.#views = "./views";
this.#globals = {};
this.#debug = false;
this.#cache = true;
this.#templates = new Map();
};
getTemplate(tpl) {
return fs.readFileSync(`${this.views}/${tpl}.html`, "utf-8");
set debug(d) {
this.#debug = !!d;
};
set views(v) {
this.#views = v;
this.readdir(v);
};
set globals(g) {
this.#globals = g;
};
set cache(c) {
this.#cache = !!c;
};
readdir(dir, root = dir) {
for(const d of fs.readdirSync(`${path.resolve()}/${dir}`)) {
if(d.endsWith(".html")) { // template
const file = path.parse(`${dir.replace(this.#views, '')}/${d}`);
const t_dir = file.dir.substring(1);
const t_file = file.name;
this.getTemplate(!t_dir ? t_file : `${t_dir}/${t_file}`);
}
else { // directory
this.readdir(`${dir}/${d}`, root);
}
}
}
render(tpl, data = {}) {
getTemplate(tpl) {
let template = {};
let cache = false;
if(this.#cache && this.#templates.has(tpl)) {
template = this.#templates.get(tpl);
cache = template.cached;
}
else {
template = {
code: fs.readFileSync(`${this.#views}/${tpl}.html`, "utf-8"),
cached: new Date()
};
this.#templates.set(tpl, template);
}
return [
template.code,
(this.#debug ? `<!-- ${tpl}.html ` + (cache ? `cached ${template.cached}` : `not cached`) + " -->" : "")
].join("");
};
render(file, data = {}, locals) {
data = { ...data, ...this.#globals, ...locals };
try {
const code = 'with(_data){const __html = [];' +
'__html.push(\`' +
tpl.replace(/[\t]/g, ' ')
.replace(/'(?=[^\{]*}})/, '\t')
this.getTemplate(file)
.replace(/[\t]/g, ' ')
.split('\`').join('\\\`')
.split('\t').join('\`')
.replace(/{{=(.+?)}}/g, '\`,$1,\`')
.replace(/{{-(.+?)}}/g, '\`,this.escape($1),\`')
.replace(/{{include (.*?)}}/g, (_, inc) => this.render(this.getTemplate(inc), data))
.replace(/{{each (?<key>.*) as (?<val>.*)}}/g, (_, key, val) => `\`);this.forEach(${key},(${val},key)=>{__html.push(\``)
.replace(/{{\/each}}/g, "\`);});__html.push(`")
.split('{{').join('\`);')
.split('}}').join('__html.push(\`')
+ '\`);return __html.join(\'\').replace(/\\n\\s*\\n/g, "\\n");}';
.replace(/{{--(.*?)--}}/g, "") // comments
.replace(/{{(.+?)}}/g, '\`,$1,\`') // yield variable
.replace(/{!!(.+?)!!}/g, '\`,this.escape($1),\`') // yield escaped variable
.replace(/@js/g, "`);") // inject bare javascript
.replace(/@endjs/g, ";__html.push(`")
.replace(/@include\((.*?)\)/g, (_, inc) => this.render(inc, data)) // include template
.replace(/@for\((.*?)\)$/gm, `\`);for($1){__html.push(\``)
.replace(/@endfor/g, `\`);}__html.push(\``)
.replace(/@each\((.*?) as (.*?)\)/g, `\`);this.forEach($1,($2,key)=>{__html.push(\``) // foreach for the poor
.replace(/@endeach/g, "\`);});__html.push(`")
.replace(/@elseif\((.*?)\)(\)?)/g, `\`);}else if($1$2){__html.push(\``) // if lol
.replace(/@if\((.*?)\)(\)?)/g, `\`);if($1$2){__html.push(\``)
.replace(/@else/g, "`);}else{__html.push(`")
.replace(/@endif/g, "`);}__html.push(`") +
'\`);return __html.join(\'\').replace(/\\n\\s*\\n/g, "\\n");}';
return (new Function("_data", code)).bind({
escape: this.escape,
forEach: this.forEach
})(data);
} catch(err) {
console.log(err);
return err.message;
console.log(file, err.message);
return (this.#debug ? `${err.message} in ${file}` : '');
}
};
escape(str) {
return (str + '')
.replace(/&/g, '&amp;')
@ -45,6 +115,7 @@ export default class {
.replace(/}/g, '&#125;')
;
};
forEach(o, f) {
if(Array.isArray(o))
o.forEach(f);