@lexho111/plainblog 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Article.js +3 -6
- package/Auth.js +2 -2
- package/Blog.js +91 -77
- package/debug-loader.js +16 -1
- package/model/DataModel.js +11 -9
- package/model/SqliteAdapter.js +14 -6
- package/model/datastructures/BinarySearchTree.js +1 -1
- package/package.json +1 -1
- package/router.js +60 -64
- package/server.js +190 -155
- package/workers/compiler-worker.js +12 -15
package/Article.js
CHANGED
|
@@ -45,12 +45,9 @@ export default class Article {
|
|
|
45
45
|
*/
|
|
46
46
|
toJSON() {
|
|
47
47
|
// Return a plain object with the desired properties
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} catch (err) {
|
|
52
|
-
console.error(err);
|
|
53
|
-
}
|
|
48
|
+
const date = !isNaN(this.createdAt)
|
|
49
|
+
? new Date(this.createdAt).toISOString()
|
|
50
|
+
: new Date().toISOString();
|
|
54
51
|
return {
|
|
55
52
|
id: this.id,
|
|
56
53
|
title: this.title,
|
package/Auth.js
CHANGED
|
@@ -16,14 +16,14 @@ export default class Auth {
|
|
|
16
16
|
if (!req.headers.cookie) {
|
|
17
17
|
const time_end = performance.now();
|
|
18
18
|
const duration = time_end - time_start;
|
|
19
|
-
debug_perf("isAuthenticated
|
|
19
|
+
debug_perf("isAuthenticated", duration);
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
22
|
const match = req.headers.cookie.match(/(?:^|;\s*)session=([^;]*)/);
|
|
23
23
|
const result = match ? this.sessions.has(match[1]) : false;
|
|
24
24
|
const time_end = performance.now();
|
|
25
25
|
const duration = time_end - time_start;
|
|
26
|
-
debug_perf("isAuthenticated
|
|
26
|
+
debug_perf("isAuthenticated", duration);
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
29
|
|
package/Blog.js
CHANGED
|
@@ -163,74 +163,88 @@ export default class Blog {
|
|
|
163
163
|
if (this.#initPromise) return this.#initPromise;
|
|
164
164
|
|
|
165
165
|
this.#initPromise = (async () => {
|
|
166
|
-
|
|
166
|
+
try {
|
|
167
|
+
await this.assetManager.init();
|
|
168
|
+
|
|
169
|
+
if (this.#isExternalAPI) {
|
|
170
|
+
console.log("external API");
|
|
171
|
+
await this.#loadFromAPI();
|
|
172
|
+
} else {
|
|
173
|
+
debug(`database: ${this.database.type}`);
|
|
174
|
+
if (!this.#databaseModel) {
|
|
175
|
+
if (this.database.type === "file") {
|
|
176
|
+
this.#databaseModel = new DatabaseModel(
|
|
177
|
+
new WorkerAdapter(this.database),
|
|
178
|
+
);
|
|
179
|
+
} else if (this.database.type === "sqlite") {
|
|
180
|
+
this.#databaseModel = new DatabaseModel(
|
|
181
|
+
new SqliteAdapter(this.database),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Timeout nach 60 Sekunden, um Hängenbleiben zu verhindern
|
|
186
|
+
let timeoutHandle;
|
|
187
|
+
const timeout = new Promise((_, reject) => {
|
|
188
|
+
timeoutHandle = setTimeout(
|
|
189
|
+
() => reject(new Error("Database initialization timed out")),
|
|
190
|
+
60000,
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
await Promise.race([this.#databaseModel.initialize(), timeout]);
|
|
195
|
+
} finally {
|
|
196
|
+
clearTimeout(timeoutHandle); // Timer aufräumen, sobald fertig
|
|
197
|
+
}
|
|
167
198
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
debug(`database: ${this.database.type}`);
|
|
173
|
-
if (!this.#databaseModel) {
|
|
174
|
-
if (this.database.type === "file") {
|
|
175
|
-
this.#databaseModel = new DatabaseModel(
|
|
176
|
-
new WorkerAdapter(this.database),
|
|
199
|
+
if (this.#databaseModel.isReady()) {
|
|
200
|
+
console.log(`connected to database`);
|
|
201
|
+
console.log(
|
|
202
|
+
`using ${this.#databaseModel.getType()} database '${this.#databaseModel.getDBName()}'`,
|
|
177
203
|
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
}
|
|
205
|
+
this.#articles.setDatabase(this.#databaseModel);
|
|
206
|
+
const [dbTitle, dbArticles] = await Promise.all([
|
|
207
|
+
this.#databaseModel.getBlogTitle(),
|
|
208
|
+
this.#databaseModel.findAll(-1),
|
|
209
|
+
]);
|
|
210
|
+
debug("dbArticles.size(): %d", dbArticles.length);
|
|
211
|
+
//log(filename, "dbArticles.size(): " + dbArticles.length);
|
|
212
|
+
//log(filename, "dbArticles.size(): " + dbArticles.length, "Blog.js");
|
|
213
|
+
debug("all articles in Blog after loading from db");
|
|
214
|
+
|
|
215
|
+
// Displays a beautiful table in the console
|
|
216
|
+
//table(dbArticles)
|
|
217
|
+
|
|
218
|
+
if (dbArticles.length == 0) {
|
|
219
|
+
dbArticles.push(
|
|
220
|
+
new Article(
|
|
221
|
+
1,
|
|
222
|
+
"Sample Entry #1",
|
|
223
|
+
"Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.",
|
|
224
|
+
new Date(),
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
dbArticles.push(
|
|
228
|
+
new Article(
|
|
229
|
+
2,
|
|
230
|
+
"Sample Entry #2",
|
|
231
|
+
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.",
|
|
232
|
+
new Date(),
|
|
233
|
+
),
|
|
181
234
|
);
|
|
182
235
|
}
|
|
236
|
+
if (this.assetManager.reloadStylesOnGET)
|
|
237
|
+
console.log("reload scripts and styles on get-request");
|
|
238
|
+
let title = "";
|
|
239
|
+
if (this.#title != null && this.#title.length > 0)
|
|
240
|
+
title = this.#title; // use blog title if set
|
|
241
|
+
else title = dbTitle; // use title from the database
|
|
242
|
+
const responseData = { title: title, articles: dbArticles };
|
|
243
|
+
this.#applyBlogData(responseData);
|
|
183
244
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
if (this.#databaseModel.isReady()) {
|
|
189
|
-
console.log(`connected to database`);
|
|
190
|
-
console.log(
|
|
191
|
-
`using ${this.#databaseModel.getType()} database '${this.#databaseModel.getDBName()}'`,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
await p;
|
|
195
|
-
this.#articles.setDatabase(this.#databaseModel);
|
|
196
|
-
const [dbTitle, dbArticles] = await Promise.all([
|
|
197
|
-
this.#databaseModel.getBlogTitle(),
|
|
198
|
-
this.#databaseModel.findAll(-1),
|
|
199
|
-
]);
|
|
200
|
-
debug("dbArticles.size(): %d", dbArticles.length);
|
|
201
|
-
//log(filename, "dbArticles.size(): " + dbArticles.length);
|
|
202
|
-
//log(filename, "dbArticles.size(): " + dbArticles.length, "Blog.js");
|
|
203
|
-
debug("all articles in Blog after loading from db");
|
|
204
|
-
|
|
205
|
-
// Displays a beautiful table in the console
|
|
206
|
-
//table(dbArticles)
|
|
207
|
-
|
|
208
|
-
if (dbArticles.length == 0) {
|
|
209
|
-
dbArticles.push(
|
|
210
|
-
new Article(
|
|
211
|
-
1,
|
|
212
|
-
"Sample Entry #1",
|
|
213
|
-
"Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.",
|
|
214
|
-
new Date(),
|
|
215
|
-
),
|
|
216
|
-
);
|
|
217
|
-
dbArticles.push(
|
|
218
|
-
new Article(
|
|
219
|
-
2,
|
|
220
|
-
"Sample Entry #2",
|
|
221
|
-
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.",
|
|
222
|
-
new Date(),
|
|
223
|
-
),
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
if (this.assetManager.reloadStylesOnGET)
|
|
227
|
-
console.log("reload scripts and styles on get-request");
|
|
228
|
-
let title = "";
|
|
229
|
-
if (this.#title != null && this.#title.length > 0)
|
|
230
|
-
title = this.#title; // use blog title if set
|
|
231
|
-
else title = dbTitle; // use title from the database
|
|
232
|
-
const responseData = { title: title, articles: dbArticles };
|
|
233
|
-
this.#applyBlogData(responseData);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
this.#initPromise = null; // Reset promise on error to allow retry
|
|
247
|
+
throw err;
|
|
234
248
|
}
|
|
235
249
|
})();
|
|
236
250
|
return this.#initPromise;
|
|
@@ -267,11 +281,6 @@ export default class Blog {
|
|
|
267
281
|
/** start a http server with default port 8080 */
|
|
268
282
|
async startServer(port = 8080) {
|
|
269
283
|
await this.init();
|
|
270
|
-
// is database ready
|
|
271
|
-
while (this.#databaseModel && !this.#databaseModel.isReady()) {
|
|
272
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
273
|
-
//console.log("is not ready;");
|
|
274
|
-
}
|
|
275
284
|
|
|
276
285
|
this.#server = await createServer(
|
|
277
286
|
this.postArticle.bind(this),
|
|
@@ -285,14 +294,16 @@ export default class Blog {
|
|
|
285
294
|
this.database,
|
|
286
295
|
);
|
|
287
296
|
|
|
288
|
-
await new Promise(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
this.#server.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
297
|
+
await new Promise(
|
|
298
|
+
function f1(resolve, reject) {
|
|
299
|
+
const errorHandler = (err) => reject(err);
|
|
300
|
+
this.#server.once("error", errorHandler);
|
|
301
|
+
this.#server.listen(port, "0.0.0.0", () => {
|
|
302
|
+
this.#server.removeListener("error", errorHandler);
|
|
303
|
+
resolve();
|
|
304
|
+
});
|
|
305
|
+
}.bind(this),
|
|
306
|
+
);
|
|
296
307
|
console.log(`server running at http://localhost:${port}/`);
|
|
297
308
|
}
|
|
298
309
|
|
|
@@ -363,7 +374,10 @@ export default class Blog {
|
|
|
363
374
|
|
|
364
375
|
/** render this blog content to valid html */
|
|
365
376
|
async toHTML(loggedin) {
|
|
366
|
-
|
|
377
|
+
let articles_after = this.#articles.findAll(null, null, 50);
|
|
378
|
+
if (articles_after instanceof Promise) {
|
|
379
|
+
articles_after = await articles_after;
|
|
380
|
+
}
|
|
367
381
|
// prettier-ignore
|
|
368
382
|
debug("fetched %d articles", articles_after.length);
|
|
369
383
|
const data = {
|
package/debug-loader.js
CHANGED
|
@@ -27,14 +27,29 @@ export function createDebug(namespace) {
|
|
|
27
27
|
|
|
28
28
|
const debug_perf_ns = createDebug("plainblog:performance");
|
|
29
29
|
export function debug_perf(message, duration) {
|
|
30
|
+
let duration_rounded = duration.toFixed(2);
|
|
31
|
+
debug_perf_ns(message.padEnd(35) + " took " + duration_rounded + "ms");
|
|
30
32
|
const threshold = 25;
|
|
31
33
|
if (duration > threshold) {
|
|
32
34
|
debug_perf_ns(message + ` WARNING! threshold > ${threshold}ms reached!`);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
export async function measure_perf(message, fn) {
|
|
39
|
+
const start = performance.now();
|
|
40
|
+
try {
|
|
41
|
+
return await fn();
|
|
42
|
+
} finally {
|
|
43
|
+
const duration = performance.now() - start;
|
|
44
|
+
debug_perf(message, duration);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
export function debug_perf1(message, duration, threshold) {
|
|
37
49
|
if (duration > threshold) {
|
|
38
|
-
debug_perf_ns(
|
|
50
|
+
debug_perf_ns(
|
|
51
|
+
message +
|
|
52
|
+
` took ${duration}ms. WARNING! threshold > ${threshold}ms reached!`,
|
|
53
|
+
);
|
|
39
54
|
}
|
|
40
55
|
}
|
package/model/DataModel.js
CHANGED
|
@@ -56,7 +56,7 @@ export default class DataModel {
|
|
|
56
56
|
return this.storage.getAllArticles(order);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
findAll(start, end, limit) {
|
|
60
60
|
debug("find all %d %d %d", start, end, limit);
|
|
61
61
|
if (start == null && end == null && limit == null)
|
|
62
62
|
return this.getAllArticles();
|
|
@@ -64,15 +64,17 @@ export default class DataModel {
|
|
|
64
64
|
let articles = this.storage.getRange(start, end, limit);
|
|
65
65
|
|
|
66
66
|
if (articles.length < limit && this.db) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.storage.
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
return (async () => {
|
|
68
|
+
const dbArticles = await this.db.findAll(limit, 0, start, end);
|
|
69
|
+
for (const data of dbArticles) {
|
|
70
|
+
if (!this.storage.contains(data.id)) {
|
|
71
|
+
this.storage.insert(
|
|
72
|
+
new Article(data.id, data.title, data.content, data.createdAt),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
return this.storage.getRange(start, end, limit);
|
|
77
|
+
})();
|
|
76
78
|
}
|
|
77
79
|
return articles;
|
|
78
80
|
}
|
package/model/SqliteAdapter.js
CHANGED
|
@@ -13,6 +13,7 @@ export default class SqliteAdapter {
|
|
|
13
13
|
this.db = null;
|
|
14
14
|
this.initPromise = null;
|
|
15
15
|
this.ready = false;
|
|
16
|
+
this.stmts = {};
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
async initialize() {
|
|
@@ -86,16 +87,23 @@ export default class SqliteAdapter {
|
|
|
86
87
|
return this.dbname;
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
getStmt(sql) {
|
|
91
|
+
if (!this.stmts[sql]) {
|
|
92
|
+
this.stmts[sql] = this.db.prepare(sql);
|
|
93
|
+
}
|
|
94
|
+
return this.stmts[sql];
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
async getBlogTitle() {
|
|
90
98
|
debug("getBlogTitle dbname: %s", this.dbname);
|
|
91
99
|
if (!this.db) await this.initialize();
|
|
92
|
-
const row = this.
|
|
100
|
+
const row = this.getStmt("SELECT title FROM BlogInfos LIMIT 1").get();
|
|
93
101
|
return row ? row.title : "Blog";
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
async updateBlogTitle(newTitle) {
|
|
97
105
|
if (!this.db) await this.initialize();
|
|
98
|
-
this.
|
|
106
|
+
this.getStmt("UPDATE BlogInfos SET title = ?").run(newTitle);
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
async save(newArticle) {
|
|
@@ -106,7 +114,7 @@ export default class SqliteAdapter {
|
|
|
106
114
|
: new Date().toISOString();
|
|
107
115
|
const updatedAt = new Date().toISOString();
|
|
108
116
|
|
|
109
|
-
const stmt = this.
|
|
117
|
+
const stmt = this.getStmt(
|
|
110
118
|
`INSERT INTO Articles (id, title, content, createdAt, updatedAt)
|
|
111
119
|
VALUES (@id, @title, @content, @createdAt, @updatedAt)`,
|
|
112
120
|
);
|
|
@@ -150,13 +158,13 @@ export default class SqliteAdapter {
|
|
|
150
158
|
|
|
151
159
|
if (sets.length > 1) {
|
|
152
160
|
const sql = `UPDATE Articles SET ${sets.join(", ")} WHERE id = @id`;
|
|
153
|
-
this.
|
|
161
|
+
this.getStmt(sql).run(params);
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
async remove(id) {
|
|
158
166
|
if (!this.db) await this.initialize();
|
|
159
|
-
this.
|
|
167
|
+
this.getStmt("DELETE FROM Articles WHERE id = ?").run(id);
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
async findAll(
|
|
@@ -195,7 +203,7 @@ export default class SqliteAdapter {
|
|
|
195
203
|
params.push(offset);
|
|
196
204
|
}
|
|
197
205
|
|
|
198
|
-
const rows = this.
|
|
206
|
+
const rows = this.getStmt(query).all(...params);
|
|
199
207
|
return rows.map((row) => ({
|
|
200
208
|
...row,
|
|
201
209
|
createdAt: new Date(row.createdAt),
|
package/package.json
CHANGED
package/router.js
CHANGED
|
@@ -9,112 +9,108 @@ const debug = createDebug("plainblog:api");
|
|
|
9
9
|
const staticCache = new Map();
|
|
10
10
|
|
|
11
11
|
// API routes
|
|
12
|
-
export
|
|
12
|
+
export function login(req, res, cb, cb2) {
|
|
13
13
|
// workaround for angular frontend
|
|
14
14
|
if (
|
|
15
15
|
(req.url === "/api/login" || req.url === "/login") &&
|
|
16
16
|
req.method === "POST"
|
|
17
17
|
) {
|
|
18
18
|
debug(`${req.method} ${req.url}`);
|
|
19
|
-
|
|
20
|
-
return;
|
|
19
|
+
return cb(req, res);
|
|
21
20
|
}
|
|
22
21
|
if (req.method === "GET" && req.url === "/login") {
|
|
23
22
|
debug(`${req.method} ${req.url}`);
|
|
24
23
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
25
|
-
|
|
26
|
-
return;
|
|
24
|
+
return cb2(req, res);
|
|
27
25
|
}
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
export
|
|
28
|
+
export function logout(req, res, cb) {
|
|
31
29
|
if (req.method === "POST") {
|
|
32
30
|
if (req.url === "/logout" || req.url === "/api/logout") {
|
|
33
31
|
debug(`${req.method} ${req.url}`);
|
|
34
|
-
|
|
35
|
-
return;
|
|
32
|
+
return cb(req, res);
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
export
|
|
37
|
+
export function api(req, res, cb) {
|
|
41
38
|
if (req.url.startsWith("/api")) {
|
|
42
39
|
debug(`${req.method} ${req.url}`);
|
|
43
|
-
|
|
44
|
-
return;
|
|
40
|
+
return cb(req, res);
|
|
45
41
|
}
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
export
|
|
44
|
+
export function new1(req, res, cb) {
|
|
49
45
|
if (req.method === "POST" && req.url === "/new") {
|
|
50
46
|
const time_start = performance.now();
|
|
51
47
|
debug(`${req.method} ${req.url}`);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
return cb(req, res).then(() => {
|
|
49
|
+
const time_end = performance.now();
|
|
50
|
+
const duration = time_end - time_start;
|
|
51
|
+
debug_perf('"/new"', duration);
|
|
52
|
+
});
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
export
|
|
56
|
+
export function handleLogin(req, res, cb) {
|
|
61
57
|
if (req.method === "POST") {
|
|
62
58
|
debug(`${req.method} ${req.url}`);
|
|
63
|
-
|
|
64
|
-
return;
|
|
59
|
+
return cb(req, res);
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
export
|
|
63
|
+
export function getPages(req, res, cb, publicDir) {
|
|
69
64
|
if (req.method === "GET") {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Try to serve static files from public folder
|
|
80
|
-
// Normalize path to prevent directory traversal attacks
|
|
81
|
-
const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
|
|
82
|
-
const filePath = path.join(
|
|
83
|
-
publicDir,
|
|
84
|
-
safePath === "/" ? "index.html" : safePath,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
//debug("%s", filePath);
|
|
88
|
-
try {
|
|
89
|
-
let data;
|
|
90
|
-
if (staticCache.has(filePath)) {
|
|
91
|
-
data = staticCache.get(filePath);
|
|
92
|
-
} else {
|
|
93
|
-
data = await readFile(filePath);
|
|
94
|
-
staticCache.set(filePath, data);
|
|
65
|
+
return (async () => {
|
|
66
|
+
const getRoot_start = performance.now();
|
|
67
|
+
if (req.url === "/") {
|
|
68
|
+
debug(`${req.method} ${req.url}`);
|
|
69
|
+
await cb(req, res);
|
|
70
|
+
const getRoot_end = performance.now();
|
|
71
|
+
const duration = getRoot_end - getRoot_start;
|
|
72
|
+
debug_perf('GET "/"', duration);
|
|
73
|
+
return;
|
|
95
74
|
}
|
|
75
|
+
// Try to serve static files from public folder
|
|
76
|
+
// Normalize path to prevent directory traversal attacks
|
|
77
|
+
const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
|
|
78
|
+
const filePath = path.join(
|
|
79
|
+
publicDir,
|
|
80
|
+
safePath === "/" ? "index.html" : safePath,
|
|
81
|
+
);
|
|
96
82
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
83
|
+
//debug("%s", filePath);
|
|
84
|
+
try {
|
|
85
|
+
let data;
|
|
86
|
+
if (staticCache.has(filePath)) {
|
|
87
|
+
data = staticCache.get(filePath);
|
|
88
|
+
} else {
|
|
89
|
+
data = await readFile(filePath);
|
|
90
|
+
staticCache.set(filePath, data);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Manual MIME type detection (simplified)
|
|
94
|
+
const ext = path.extname(filePath);
|
|
95
|
+
const mimeTypes = {
|
|
96
|
+
".html": "text/html",
|
|
97
|
+
".css": "text/css",
|
|
98
|
+
".js": "text/javascript",
|
|
99
|
+
};
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
res.writeHead(200, {
|
|
102
|
+
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
103
|
+
});
|
|
104
|
+
res.end(data);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err) {
|
|
107
|
+
if (!res.headersSent) {
|
|
108
|
+
res.writeHead(404);
|
|
109
|
+
return res.end("File Not Found");
|
|
110
|
+
}
|
|
114
111
|
}
|
|
115
112
|
}
|
|
116
|
-
}
|
|
117
|
-
return;
|
|
113
|
+
})();
|
|
118
114
|
}
|
|
119
115
|
}
|
|
120
116
|
// ---------------------------------------------
|
package/server.js
CHANGED
|
@@ -45,15 +45,21 @@ async function readBody(req, timeout = 15000, maxSize = 1024 * 1024) {
|
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
req.on("end", ()
|
|
48
|
+
req.on("end", function read_body_end() {
|
|
49
49
|
if (!completed) {
|
|
50
50
|
completed = true;
|
|
51
51
|
clearTimeout(timer);
|
|
52
|
-
|
|
52
|
+
if (chunks.length === 0) {
|
|
53
|
+
resolve("");
|
|
54
|
+
} else if (chunks.length === 1) {
|
|
55
|
+
resolve(chunks[0].toString());
|
|
56
|
+
} else {
|
|
57
|
+
resolve(Buffer.concat(chunks).toString());
|
|
58
|
+
}
|
|
53
59
|
}
|
|
54
60
|
const time_end = performance.now();
|
|
55
61
|
const duration = time_end - time_start;
|
|
56
|
-
debug_perf("readBody on end
|
|
62
|
+
debug_perf("readBody on end", duration);
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
req.on("error", (err) => {
|
|
@@ -64,7 +70,7 @@ async function readBody(req, timeout = 15000, maxSize = 1024 * 1024) {
|
|
|
64
70
|
}
|
|
65
71
|
const time_end = performance.now();
|
|
66
72
|
const duration = time_end - time_start;
|
|
67
|
-
debug_perf("readBody on error
|
|
73
|
+
debug_perf("readBody on error", duration);
|
|
68
74
|
});
|
|
69
75
|
});
|
|
70
76
|
}
|
|
@@ -106,37 +112,55 @@ export async function createServer(
|
|
|
106
112
|
res.on("finish", clearTimeouts);
|
|
107
113
|
res.on("close", clearTimeouts);
|
|
108
114
|
//debug("query %s", req.url);
|
|
109
|
-
await api(req, res, async (req, res) => {
|
|
110
|
-
const time_start = performance.now();
|
|
111
|
-
await jsonAPI(
|
|
112
|
-
req,
|
|
113
|
-
res,
|
|
114
|
-
title,
|
|
115
|
-
articles,
|
|
116
|
-
databaseModel,
|
|
117
|
-
readBody,
|
|
118
|
-
auth.isAuthenticated.bind(auth),
|
|
119
|
-
);
|
|
120
|
-
const time_end = performance.now();
|
|
121
|
-
const duration = time_end - time_start;
|
|
122
|
-
debug_perf1("jsonAPI took " + duration + "ms", duration, 400);
|
|
123
|
-
});
|
|
124
|
-
if (res.headersSent) return;
|
|
125
115
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
res
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
// Helper: Führt eine Route aus und stoppt, wenn eine Antwort gesendet wurde
|
|
117
|
+
const tryRoute = async (routeCall) => {
|
|
118
|
+
if (res.headersSent) return true;
|
|
119
|
+
const p = routeCall();
|
|
120
|
+
if (p) {
|
|
121
|
+
await p;
|
|
122
|
+
return res.headersSent;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
await tryRoute(() =>
|
|
129
|
+
api(req, res, async function handleAPIRequest(req, res) {
|
|
130
|
+
const time_start = performance.now();
|
|
131
|
+
await jsonAPI(
|
|
132
|
+
req,
|
|
133
|
+
res,
|
|
134
|
+
title,
|
|
135
|
+
articles,
|
|
136
|
+
databaseModel,
|
|
137
|
+
readBody,
|
|
138
|
+
auth.isAuthenticated.bind(auth),
|
|
139
|
+
);
|
|
140
|
+
const time_end = performance.now();
|
|
141
|
+
const duration = time_end - time_start;
|
|
142
|
+
debug_perf1("jsonAPI", duration, 400);
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
return;
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
await tryRoute(() =>
|
|
150
|
+
login(
|
|
131
151
|
req,
|
|
132
152
|
res,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
async (req, res) => {
|
|
154
|
+
await auth.handleLogin(
|
|
155
|
+
req,
|
|
156
|
+
res,
|
|
157
|
+
readBody,
|
|
158
|
+
REQUEST_TIMEOUT,
|
|
159
|
+
MAX_REQUEST_SIZE,
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
async (req, res) => {
|
|
163
|
+
res.end(`${header("My Blog")}<body>
|
|
140
164
|
<form class="loginform" id="loginForm">
|
|
141
165
|
<h1>Blog</h1>
|
|
142
166
|
<!-- Message container -->
|
|
@@ -183,130 +207,144 @@ export async function createServer(
|
|
|
183
207
|
});
|
|
184
208
|
</script>
|
|
185
209
|
</body></html>`);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
auth.handleLogout(req, res);
|
|
192
|
-
});
|
|
193
|
-
if (res.headersSent) return;
|
|
194
|
-
|
|
195
|
-
await new1(req, res, async (req, res) => {
|
|
196
|
-
if (!auth.isAuthenticated(req)) {
|
|
197
|
-
debug("not authenticated");
|
|
198
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
199
|
-
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
const body = await readBody(req);
|
|
205
|
-
const params = new URLSearchParams(body);
|
|
206
|
-
const articleData = Object.fromEntries(params.entries());
|
|
207
|
-
|
|
208
|
-
debug("New Article Data:", articleData);
|
|
209
|
-
// local
|
|
210
|
-
// Write-before-exit strategy: Create in memory, save on closeServer()
|
|
211
|
-
const savedArticle = Article.createNew(
|
|
212
|
-
articleData.title,
|
|
213
|
-
articleData.content,
|
|
214
|
-
);
|
|
210
|
+
},
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
return;
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
if (
|
|
217
|
+
await tryRoute(() =>
|
|
218
|
+
logout(req, res, (req, res) => {
|
|
219
|
+
auth.handleLogout(req, res);
|
|
220
|
+
}),
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
return;
|
|
219
224
|
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
if (
|
|
226
|
+
await tryRoute(() =>
|
|
227
|
+
new1(req, res, async (req, res) => {
|
|
228
|
+
if (!auth.isAuthenticated(req)) {
|
|
229
|
+
debug("not authenticated");
|
|
230
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
231
|
+
res.end(JSON.stringify({ error: "Forbidden" }));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
222
234
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (res.headersSent) return;
|
|
235
|
-
|
|
236
|
-
await getPages(
|
|
237
|
-
req,
|
|
238
|
-
res,
|
|
239
|
-
async (req, res) => {
|
|
240
|
-
// reload styles and scripts on (every) request
|
|
241
|
-
if (assetManager.reloadStylesOnGET) {
|
|
242
|
-
// This is a development-only feature and a major performance risk.
|
|
243
|
-
if (process.env.NODE_ENV === "production") {
|
|
244
|
-
console.warn(
|
|
245
|
-
"Warning: 'reloadStylesOnGET' is enabled in a production-like environment. This is a major performance risk and should be disabled.",
|
|
235
|
+
try {
|
|
236
|
+
const body = await readBody(req);
|
|
237
|
+
const params = new URLSearchParams(body);
|
|
238
|
+
const articleData = Object.fromEntries(params.entries());
|
|
239
|
+
|
|
240
|
+
debug("New Article Data:", articleData);
|
|
241
|
+
// local
|
|
242
|
+
// Write-before-exit strategy: Create in memory, save on closeServer()
|
|
243
|
+
const savedArticle = Article.createNew(
|
|
244
|
+
articleData.title,
|
|
245
|
+
articleData.content,
|
|
246
246
|
);
|
|
247
|
-
assetManager.reloadStylesOnGET = false; // Disable it for subsequent requests
|
|
248
|
-
}
|
|
249
|
-
await assetManager.reload();
|
|
250
|
-
}
|
|
251
247
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
loggedin = false;
|
|
256
|
-
} else {
|
|
257
|
-
// logout
|
|
258
|
-
loggedin = true;
|
|
259
|
-
}
|
|
248
|
+
databaseModel
|
|
249
|
+
.save(savedArticle)
|
|
250
|
+
.catch((err) => console.error("Async save failed:", err));
|
|
260
251
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const filePath = path.join(publicDir, "index.html");
|
|
252
|
+
await postArticle(savedArticle);
|
|
253
|
+
// external
|
|
264
254
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const mimeTypes = {
|
|
271
|
-
".html": "text/html",
|
|
272
|
-
".css": "text/css",
|
|
273
|
-
".js": "text/javascript",
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
res.writeHead(200, {
|
|
277
|
-
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
278
|
-
});
|
|
279
|
-
res.end(data);
|
|
280
|
-
} catch (fileErr) {
|
|
281
|
-
debug("Error reading index file: %s", fileErr.message);
|
|
255
|
+
// Success response
|
|
256
|
+
res.writeHead(302, { Location: "/" });
|
|
257
|
+
res.end();
|
|
258
|
+
} catch (formErr) {
|
|
259
|
+
debug("Error handling form submission: %s", formErr.message);
|
|
282
260
|
if (!res.headersSent) {
|
|
283
|
-
|
|
284
|
-
res.
|
|
285
|
-
return res.end("Index-File Not Found");
|
|
261
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
262
|
+
res.end(JSON.stringify({ error: "Failed to parse form data" }));
|
|
286
263
|
}
|
|
287
264
|
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
265
|
+
}),
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
return;
|
|
269
|
+
|
|
270
|
+
if (
|
|
271
|
+
await tryRoute(() =>
|
|
272
|
+
getPages(
|
|
273
|
+
req,
|
|
274
|
+
res,
|
|
275
|
+
async (req, res) => {
|
|
276
|
+
// reload styles and scripts on (every) request
|
|
277
|
+
if (assetManager.reloadStylesOnGET) {
|
|
278
|
+
// This is a development-only feature and a major performance risk.
|
|
279
|
+
if (process.env.NODE_ENV === "production") {
|
|
280
|
+
console.warn(
|
|
281
|
+
"Warning: 'reloadStylesOnGET' is enabled in a production-like environment. This is a major performance risk and should be disabled.",
|
|
282
|
+
);
|
|
283
|
+
assetManager.reloadStylesOnGET = false; // Disable it for subsequent requests
|
|
284
|
+
}
|
|
285
|
+
await assetManager.reload();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let loggedin = false;
|
|
289
|
+
if (!auth.isAuthenticated(req)) {
|
|
290
|
+
// login
|
|
291
|
+
loggedin = false;
|
|
292
|
+
} else {
|
|
293
|
+
// logout
|
|
294
|
+
loggedin = true;
|
|
302
295
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
296
|
+
|
|
297
|
+
if (angular) {
|
|
298
|
+
// use angular frontend
|
|
299
|
+
const filePath = path.join(publicDir, "index.html");
|
|
300
|
+
|
|
301
|
+
debug("%s", filePath);
|
|
302
|
+
try {
|
|
303
|
+
const data = await readFile(filePath);
|
|
304
|
+
// Manual MIME type detection (simplified)
|
|
305
|
+
const ext = path.extname(filePath);
|
|
306
|
+
const mimeTypes = {
|
|
307
|
+
".html": "text/html",
|
|
308
|
+
".css": "text/css",
|
|
309
|
+
".js": "text/javascript",
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
res.writeHead(200, {
|
|
313
|
+
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
314
|
+
});
|
|
315
|
+
res.end(data);
|
|
316
|
+
} catch (fileErr) {
|
|
317
|
+
debug("Error reading index file: %s", fileErr.message);
|
|
318
|
+
if (!res.headersSent) {
|
|
319
|
+
debug404("unmatched route: %s %s", req.method, req.url);
|
|
320
|
+
res.writeHead(404);
|
|
321
|
+
return res.end("Index-File Not Found");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
// use built in view engine
|
|
326
|
+
try {
|
|
327
|
+
const html = await html_content(loggedin); // render this blog to HTML
|
|
328
|
+
res.writeHead(200, {
|
|
329
|
+
"Content-Type": "text/html; charset=UTF-8",
|
|
330
|
+
});
|
|
331
|
+
res.end(html);
|
|
332
|
+
return;
|
|
333
|
+
} catch (renderErr) {
|
|
334
|
+
console.error("Error rendering HTML:", renderErr);
|
|
335
|
+
if (!res.headersSent) {
|
|
336
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
337
|
+
res.end("Internal Server Error");
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
publicDir,
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
return;
|
|
310
348
|
|
|
311
349
|
// If no route was matched, send a 404
|
|
312
350
|
if (!res.headersSent) {
|
|
@@ -472,7 +510,7 @@ async function jsonAPI(
|
|
|
472
510
|
const dbArticles = await articles.findAll(start, end, limit);
|
|
473
511
|
const time_end = performance.now();
|
|
474
512
|
const duration = time_end - time_start;
|
|
475
|
-
debug_perf('GET "/api/articles"
|
|
513
|
+
debug_perf('GET "/api/articles"', duration);
|
|
476
514
|
if (!res.headersSent) {
|
|
477
515
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
478
516
|
const responseData = {
|
|
@@ -502,13 +540,10 @@ async function jsonAPI(
|
|
|
502
540
|
body = await readBody(req, REQUEST_TIMEOUT, MAX_REQUEST_SIZE);
|
|
503
541
|
const time_end1 = performance.now();
|
|
504
542
|
const duration1 = time_end1 - time_start1;
|
|
505
|
-
debug_perf(
|
|
506
|
-
'POST "/api/articles" readBody took ' + duration1 + "ms",
|
|
507
|
-
duration1,
|
|
508
|
-
);
|
|
543
|
+
debug_perf('POST "/api/articles" readBody', duration1);
|
|
509
544
|
const time_end = performance.now();
|
|
510
545
|
const duration = time_end - time_start;
|
|
511
|
-
debug_perf('POST "/api/articles"
|
|
546
|
+
debug_perf('POST "/api/articles"', duration);
|
|
512
547
|
} catch (err) {
|
|
513
548
|
debug("Error reading request body: %s", err.message);
|
|
514
549
|
if (!res.headersSent) {
|
|
@@ -541,7 +576,7 @@ async function jsonAPI(
|
|
|
541
576
|
debug("new article: %s", newArticle.title);
|
|
542
577
|
const time_end1 = performance.now();
|
|
543
578
|
const duration1 = time_end1 - time_start;
|
|
544
|
-
debug_perf("new article took "
|
|
579
|
+
debug_perf("new article took ", duration1);
|
|
545
580
|
|
|
546
581
|
// Create article with a temporary ID for immediate use
|
|
547
582
|
const articleWithTempId = Article.createNew(
|
|
@@ -563,7 +598,7 @@ async function jsonAPI(
|
|
|
563
598
|
|
|
564
599
|
const time_end1 = performance.now();
|
|
565
600
|
const duration = time_end1 - time_start;
|
|
566
|
-
debug_perf("databaseModel
|
|
601
|
+
debug_perf("databaseModel", duration);
|
|
567
602
|
} catch (dbErr) {
|
|
568
603
|
console.error("Database save error:", dbErr.message);
|
|
569
604
|
console.error("Stack:", dbErr.stack);
|
|
@@ -636,7 +671,7 @@ async function jsonAPI(
|
|
|
636
671
|
}
|
|
637
672
|
const time_end = performance.now();
|
|
638
673
|
const duration = time_end - time_start;
|
|
639
|
-
debug_perf('DELETE "/api/articles"
|
|
674
|
+
debug_perf('DELETE "/api/articles"', duration);
|
|
640
675
|
} else if (req.method === "PUT") {
|
|
641
676
|
debug("PUT an article");
|
|
642
677
|
if (!isAuthenticated(req)) {
|
|
@@ -698,7 +733,7 @@ async function jsonAPI(
|
|
|
698
733
|
}
|
|
699
734
|
const time_end = performance.now();
|
|
700
735
|
const duration = time_end - time_start;
|
|
701
|
-
debug_perf('POST "/api/articles"
|
|
736
|
+
debug_perf('POST "/api/articles"', duration);
|
|
702
737
|
} else {
|
|
703
738
|
if (!res.headersSent) {
|
|
704
739
|
debug404("unmatched route: %s %s", req.method, req.url);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parentPort, workerData } from "worker_threads";
|
|
2
2
|
import { compileStyles, mergeStyles } from "../build-styles.js";
|
|
3
3
|
import { compileScripts } from "../build-scripts.js";
|
|
4
|
-
import { createDebug,
|
|
4
|
+
import { createDebug, measure_perf } from "../debug-loader.js";
|
|
5
5
|
|
|
6
6
|
async function run() {
|
|
7
7
|
if (!parentPort) {
|
|
@@ -9,20 +9,17 @@ async function run() {
|
|
|
9
9
|
}
|
|
10
10
|
const { type, fileData, cssContents } = workerData;
|
|
11
11
|
try {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
const time_end = performance.now();
|
|
24
|
-
const duration = time_end - time_start;
|
|
25
|
-
debug_perf("compilerWorker took " + duration + "ms", duration);
|
|
12
|
+
const result = await measure_perf("compilerWorker", async () => {
|
|
13
|
+
if (type === "styles") {
|
|
14
|
+
return await compileStyles(fileData);
|
|
15
|
+
} else if (type === "scripts") {
|
|
16
|
+
return await compileScripts(fileData);
|
|
17
|
+
} else if (type === "mergeStyles") {
|
|
18
|
+
return await mergeStyles(...cssContents);
|
|
19
|
+
} else {
|
|
20
|
+
throw new Error(`Unknown compilation type: ${type}`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
26
23
|
parentPort.postMessage({ status: "success", result });
|
|
27
24
|
} catch (error) {
|
|
28
25
|
parentPort.postMessage({ status: "error", error: error.message });
|