...
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 http from "http";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import querystring from "querystring";
|
import querystring from "querystring";
|
||||||
|
@ -10,90 +8,26 @@ import Tpl from "./template.mjs";
|
||||||
export { Router, Tpl };
|
export { Router, Tpl };
|
||||||
|
|
||||||
export default class flummpress {
|
export default class flummpress {
|
||||||
#mimes;
|
|
||||||
#server;
|
#server;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.router = new Router();
|
this.router = new Router();
|
||||||
this.tpl = new Tpl();
|
this.tpl = new Tpl();
|
||||||
|
this.middleware = new Set();
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
use(obj) {
|
use(obj) {
|
||||||
if(obj instanceof Router) {
|
if(obj instanceof Router) {
|
||||||
this.router.routes = new Map([ ...this.router.routes, ...obj.routes ]);
|
this.router.use(obj);
|
||||||
this.router.sortRoutes();
|
|
||||||
}
|
}
|
||||||
else if(obj instanceof Tpl) {
|
else if(obj instanceof Tpl) {
|
||||||
this.tpl = obj;
|
this.tpl = obj;
|
||||||
}
|
}
|
||||||
return this;
|
else {
|
||||||
};
|
if(!this.middleware.has(obj))
|
||||||
|
this.middleware.add(obj);
|
||||||
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])));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,6 +43,16 @@ export default class flummpress {
|
||||||
qs: {...querystring.parse(_url.search.substring(1))} // legacy
|
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 method = req.method.toLowerCase();
|
||||||
const route = this.router.getRoute(req.url.pathname, req.method);
|
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.writeHead(code, { "Content-Type": `${type}; charset=utf-8` }).end(body);
|
||||||
res.json = msg => res.reply({ type: "application/json", body: msg });
|
res.json = msg => res.reply({ type: "application/json", body: msg });
|
||||||
res.html = msg => res.reply({ type: "text/html", 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;
|
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 {
|
export default class Router {
|
||||||
|
#mimes;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.routes = new Map();
|
this.routes = new Map();
|
||||||
return this;
|
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) {
|
group(path, cb) {
|
||||||
const methods = {
|
const methods = {
|
||||||
get: this.get.bind(this),
|
get: this.get.bind(this),
|
||||||
|
@ -25,6 +43,16 @@ export default class Router {
|
||||||
return this;
|
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) {
|
get(path, ...args) {
|
||||||
if(args.length === 1)
|
if(args.length === 1)
|
||||||
this.registerRoute(path, args[0], "get");
|
this.registerRoute(path, args[0], "get");
|
||||||
|
@ -53,7 +81,7 @@ export default class Router {
|
||||||
this.sortRoutes();
|
this.sortRoutes();
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
getRoute(path, method) {
|
getRoute(path, method) {
|
||||||
method = method.toLowerCase();
|
method = method.toLowerCase();
|
||||||
return [...this.routes.entries()].filter(r => {
|
return [...this.routes.entries()].filter(r => {
|
||||||
|
@ -65,4 +93,75 @@ export default class Router {
|
||||||
this.routes = new Map([...this.routes.entries()].sort().reverse());
|
this.routes = new Map([...this.routes.entries()].sort().reverse());
|
||||||
return this;
|
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";
|
import path from "path";
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
|
#views;
|
||||||
|
#globals;
|
||||||
|
#cache;
|
||||||
|
#templates;
|
||||||
|
#debug;
|
||||||
|
|
||||||
constructor() {
|
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 {
|
try {
|
||||||
const code = 'with(_data){const __html = [];' +
|
const code = 'with(_data){const __html = [];' +
|
||||||
'__html.push(\`' +
|
'__html.push(\`' +
|
||||||
tpl.replace(/[\t]/g, ' ')
|
this.getTemplate(file)
|
||||||
.replace(/'(?=[^\{]*}})/, '\t')
|
.replace(/[\t]/g, ' ')
|
||||||
.split('\`').join('\\\`')
|
.split('\`').join('\\\`')
|
||||||
.split('\t').join('\`')
|
|
||||||
.replace(/{{=(.+?)}}/g, '\`,$1,\`')
|
.replace(/{{--(.*?)--}}/g, "") // comments
|
||||||
.replace(/{{-(.+?)}}/g, '\`,this.escape($1),\`')
|
.replace(/{{(.+?)}}/g, '\`,$1,\`') // yield variable
|
||||||
.replace(/{{include (.*?)}}/g, (_, inc) => this.render(this.getTemplate(inc), data))
|
.replace(/{!!(.+?)!!}/g, '\`,this.escape($1),\`') // yield escaped variable
|
||||||
.replace(/{{each (?<key>.*) as (?<val>.*)}}/g, (_, key, val) => `\`);this.forEach(${key},(${val},key)=>{__html.push(\``)
|
|
||||||
.replace(/{{\/each}}/g, "\`);});__html.push(`")
|
.replace(/@js/g, "`);") // inject bare javascript
|
||||||
.split('{{').join('\`);')
|
.replace(/@endjs/g, ";__html.push(`")
|
||||||
.split('}}').join('__html.push(\`')
|
|
||||||
+ '\`);return __html.join(\'\').replace(/\\n\\s*\\n/g, "\\n");}';
|
.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({
|
return (new Function("_data", code)).bind({
|
||||||
escape: this.escape,
|
escape: this.escape,
|
||||||
forEach: this.forEach
|
forEach: this.forEach
|
||||||
})(data);
|
})(data);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.log(err);
|
console.log(file, err.message);
|
||||||
return err.message;
|
return (this.#debug ? `${err.message} in ${file}` : '');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
escape(str) {
|
escape(str) {
|
||||||
return (str + '')
|
return (str + '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
|
@ -45,6 +115,7 @@ export default class {
|
||||||
.replace(/}/g, '}')
|
.replace(/}/g, '}')
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
forEach(o, f) {
|
forEach(o, f) {
|
||||||
if(Array.isArray(o))
|
if(Array.isArray(o))
|
||||||
o.forEach(f);
|
o.forEach(f);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user