...
This commit is contained in:
parent
a595aea282
commit
d40fb2c5cf
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
101
src/router.mjs
101
src/router.mjs
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
105
src/template.mjs
105
src/template.mjs
|
@ -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, '&')
|
||||
|
@ -45,6 +115,7 @@ export default class {
|
|||
.replace(/}/g, '}')
|
||||
;
|
||||
};
|
||||
|
||||
forEach(o, f) {
|
||||
if(Array.isArray(o))
|
||||
o.forEach(f);
|
||||
|
|
Loading…
Reference in New Issue
Block a user