@stamhoofd/backend-renderer 2.39.1 → 2.40.0
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/eslint.config.mjs +5 -0
- package/index.ts +37 -34
- package/package.json +3 -3
- package/src/endpoints/HtmlToPdfEndpoint.ts +94 -89
- package/src/endpoints/PdfCacheEndpoint.ts +19 -19
- package/src/helpers/FileCache.ts +26 -23
- package/stamhoofd.d.ts +7 -8
- package/.eslintrc.js +0 -61
package/index.ts
CHANGED
|
@@ -1,40 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import backendEnv from '@stamhoofd/backend-env';
|
|
2
|
+
backendEnv.load({ service: 'renderer' });
|
|
3
|
+
|
|
4
|
+
import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
|
|
5
|
+
import { I18n } from '@stamhoofd/backend-i18n';
|
|
6
|
+
import { CORSMiddleware, LogMiddleware } from '@stamhoofd/backend-middleware';
|
|
7
|
+
import { loadLogger } from '@stamhoofd/logging';
|
|
8
|
+
|
|
9
|
+
process.on('unhandledRejection', (error: Error) => {
|
|
10
|
+
console.error('unhandledRejection');
|
|
9
11
|
console.error(error.message, error.stack);
|
|
10
12
|
process.exit(1);
|
|
11
13
|
});
|
|
12
14
|
|
|
13
15
|
// Set timezone!
|
|
14
|
-
process.env.TZ =
|
|
16
|
+
process.env.TZ = 'UTC';
|
|
15
17
|
|
|
16
18
|
// Quick check
|
|
17
|
-
if (new Date().getTimezoneOffset()
|
|
18
|
-
throw new Error(
|
|
19
|
+
if (new Date().getTimezoneOffset() !== 0) {
|
|
20
|
+
throw new Error('Process should always run in UTC timezone');
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const start = async () => {
|
|
22
|
-
console.log('Started Renderer.')
|
|
24
|
+
console.log('Started Renderer.');
|
|
23
25
|
loadLogger();
|
|
24
|
-
await I18n.load()
|
|
26
|
+
await I18n.load();
|
|
25
27
|
const router = new Router();
|
|
26
|
-
await router.loadEndpoints(__dirname +
|
|
27
|
-
router.endpoints.push(new CORSPreflightEndpoint())
|
|
28
|
+
await router.loadEndpoints(__dirname + '/src/endpoints');
|
|
29
|
+
router.endpoints.push(new CORSPreflightEndpoint());
|
|
28
30
|
|
|
29
31
|
const routerServer = new RouterServer(router);
|
|
30
|
-
routerServer.verbose = false
|
|
31
|
-
|
|
32
|
+
routerServer.verbose = false;
|
|
33
|
+
|
|
32
34
|
// Send the app version along
|
|
33
|
-
routerServer.addRequestMiddleware(LogMiddleware)
|
|
34
|
-
routerServer.addResponseMiddleware(LogMiddleware)
|
|
35
|
+
routerServer.addRequestMiddleware(LogMiddleware);
|
|
36
|
+
routerServer.addResponseMiddleware(LogMiddleware);
|
|
35
37
|
|
|
36
38
|
// Add CORS headers
|
|
37
|
-
routerServer.addResponseMiddleware(CORSMiddleware)
|
|
39
|
+
routerServer.addResponseMiddleware(CORSMiddleware);
|
|
38
40
|
|
|
39
41
|
routerServer.listen(STAMHOOFD.PORT ?? 9090);
|
|
40
42
|
|
|
@@ -44,19 +46,20 @@ const start = async () => {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const shutdown = async () => {
|
|
47
|
-
console.log(
|
|
49
|
+
console.log('Shutting down...');
|
|
48
50
|
// Disable keep alive
|
|
49
|
-
routerServer.defaultHeaders = Object.assign(routerServer.defaultHeaders, {
|
|
51
|
+
routerServer.defaultHeaders = Object.assign(routerServer.defaultHeaders, { Connection: 'close' });
|
|
50
52
|
if (routerServer.server) {
|
|
51
53
|
routerServer.server.headersTimeout = 5000;
|
|
52
54
|
routerServer.server.keepAliveTimeout = 1;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
try {
|
|
56
|
-
await routerServer.close()
|
|
57
|
-
console.log(
|
|
58
|
-
}
|
|
59
|
-
|
|
58
|
+
await routerServer.close();
|
|
59
|
+
console.log('HTTP server stopped');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.error('Failed to stop HTTP server:');
|
|
60
63
|
console.error(err);
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -64,24 +67,24 @@ const start = async () => {
|
|
|
64
67
|
process.exit(0);
|
|
65
68
|
};
|
|
66
69
|
|
|
67
|
-
process.on(
|
|
68
|
-
console.info(
|
|
70
|
+
process.on('SIGTERM', () => {
|
|
71
|
+
console.info('SIGTERM signal received.');
|
|
69
72
|
shutdown().catch((e) => {
|
|
70
|
-
console.error(e)
|
|
73
|
+
console.error(e);
|
|
71
74
|
process.exit(1);
|
|
72
75
|
});
|
|
73
76
|
});
|
|
74
77
|
|
|
75
|
-
process.on(
|
|
76
|
-
console.info(
|
|
78
|
+
process.on('SIGINT', () => {
|
|
79
|
+
console.info('SIGINT signal received.');
|
|
77
80
|
shutdown().catch((e) => {
|
|
78
|
-
console.error(e)
|
|
81
|
+
console.error(e);
|
|
79
82
|
process.exit(1);
|
|
80
83
|
});
|
|
81
84
|
});
|
|
82
85
|
};
|
|
83
86
|
|
|
84
|
-
start().catch(error => {
|
|
85
|
-
console.error(
|
|
87
|
+
start().catch((error) => {
|
|
88
|
+
console.error('unhandledRejection', error);
|
|
86
89
|
process.exit(1);
|
|
87
90
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend-renderer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.40.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"build:full": "yarn clear && yarn build",
|
|
15
15
|
"clear": "rm -rf ./dist",
|
|
16
16
|
"start": "yarn build && node --enable-source-maps ./dist/index.js",
|
|
17
|
-
"lint": "eslint
|
|
17
|
+
"lint": "eslint"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/formidable": "3.4.5",
|
|
@@ -31,5 +31,5 @@
|
|
|
31
31
|
"mysql": "^2.18.1",
|
|
32
32
|
"puppeteer": "22.12.0"
|
|
33
33
|
},
|
|
34
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "6839689f56361c6ba6f34f6113a5cdc4bbd7b209"
|
|
35
35
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { DecodedRequest, Endpoint, Request, Response } from
|
|
2
|
-
import { SimpleError } from
|
|
3
|
-
import { verifyInternalSignature } from
|
|
4
|
-
import { QueueHandler } from
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { verifyInternalSignature } from '@stamhoofd/backend-env';
|
|
4
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
5
5
|
import formidable from 'formidable';
|
|
6
6
|
import { firstValues } from 'formidable/src/helpers/firstValues.js';
|
|
7
|
-
import { promises as fs } from
|
|
8
|
-
import puppeteer, { Browser } from
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import puppeteer, { Browser } from 'puppeteer';
|
|
9
9
|
|
|
10
|
-
import { FileCache } from
|
|
10
|
+
import { FileCache } from '../helpers/FileCache';
|
|
11
11
|
|
|
12
12
|
type Params = Record<string, never>;
|
|
13
13
|
type Body = undefined;
|
|
14
|
-
type Query = undefined
|
|
15
|
-
type ResponseBody = Buffer
|
|
14
|
+
type Query = undefined;
|
|
15
|
+
type ResponseBody = Buffer;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
|
|
@@ -20,11 +20,11 @@ type ResponseBody = Buffer
|
|
|
20
20
|
|
|
21
21
|
export class HtmlToPdfEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
22
22
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
23
|
-
if (request.method
|
|
23
|
+
if (request.method !== 'POST') {
|
|
24
24
|
return [false];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const params = Endpoint.parseParameters(request.url,
|
|
27
|
+
const params = Endpoint.parseParameters(request.url, '/html-to-pdf', {});
|
|
28
28
|
|
|
29
29
|
if (params) {
|
|
30
30
|
return [true, params as Params];
|
|
@@ -33,161 +33,165 @@ export class HtmlToPdfEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
36
|
-
const form = formidable({
|
|
37
|
-
maxTotalFileSize: 20 * 1024 * 1024,
|
|
36
|
+
const form = formidable({
|
|
37
|
+
maxTotalFileSize: 20 * 1024 * 1024,
|
|
38
38
|
keepExtensions: false,
|
|
39
|
-
maxFiles: 1
|
|
39
|
+
maxFiles: 1,
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
const {html, cacheId, timestamp, signature} = await new Promise<{html: string
|
|
42
|
+
const { html, cacheId, timestamp, signature } = await new Promise<{ html: string; cacheId: string; timestamp: Date; signature: string }>((resolve, reject) => {
|
|
43
43
|
if (!request.request.request) {
|
|
44
44
|
reject(new SimpleError({
|
|
45
|
-
code:
|
|
46
|
-
message:
|
|
47
|
-
statusCode: 500
|
|
45
|
+
code: 'invalid_request',
|
|
46
|
+
message: 'Invalid request',
|
|
47
|
+
statusCode: 500,
|
|
48
48
|
}));
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
53
|
-
form.parse(request.request.request, async (err, fieldsMultiple, files) => {
|
|
53
|
+
form.parse(request.request.request, async (err: Error, fieldsMultiple, files) => {
|
|
54
54
|
if (err) {
|
|
55
55
|
reject(err);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
if (!files.html || files.html.length !== 1) {
|
|
59
59
|
reject(new SimpleError({
|
|
60
|
-
code:
|
|
61
|
-
message:
|
|
62
|
-
field:
|
|
63
|
-
}))
|
|
64
|
-
return
|
|
60
|
+
code: 'missing_field',
|
|
61
|
+
message: 'Field html is required',
|
|
62
|
+
field: 'html',
|
|
63
|
+
}));
|
|
64
|
+
return;
|
|
65
65
|
}
|
|
66
66
|
const fields = firstValues(form, fieldsMultiple);
|
|
67
67
|
|
|
68
|
-
if (!fields.signature || typeof fields.signature !==
|
|
68
|
+
if (!fields.signature || typeof fields.signature !== 'string') {
|
|
69
69
|
reject(new SimpleError({
|
|
70
|
-
code:
|
|
71
|
-
message:
|
|
72
|
-
field:
|
|
73
|
-
}))
|
|
74
|
-
return
|
|
70
|
+
code: 'missing_field',
|
|
71
|
+
message: 'Field signature is required',
|
|
72
|
+
field: 'signature',
|
|
73
|
+
}));
|
|
74
|
+
return;
|
|
75
75
|
}
|
|
76
|
-
if (!fields.cacheId || typeof fields.cacheId !==
|
|
76
|
+
if (!fields.cacheId || typeof fields.cacheId !== 'string') {
|
|
77
77
|
reject(new SimpleError({
|
|
78
|
-
code:
|
|
79
|
-
message:
|
|
80
|
-
field:
|
|
81
|
-
}))
|
|
82
|
-
return
|
|
78
|
+
code: 'missing_field',
|
|
79
|
+
message: 'Field cacheId is required',
|
|
80
|
+
field: 'cacheId',
|
|
81
|
+
}));
|
|
82
|
+
return;
|
|
83
83
|
}
|
|
84
|
-
if (!fields.timestamp || typeof fields.timestamp !==
|
|
84
|
+
if (!fields.timestamp || typeof fields.timestamp !== 'string') {
|
|
85
85
|
reject(new SimpleError({
|
|
86
|
-
code:
|
|
87
|
-
message:
|
|
88
|
-
field:
|
|
89
|
-
}))
|
|
90
|
-
return
|
|
86
|
+
code: 'missing_field',
|
|
87
|
+
message: 'Field timestamp is required',
|
|
88
|
+
field: 'timestamp',
|
|
89
|
+
}));
|
|
90
|
+
return;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
let html;
|
|
94
94
|
try {
|
|
95
|
-
html = await fs.readFile(files.html[0].filepath
|
|
96
|
-
}
|
|
95
|
+
html = await fs.readFile(files.html[0].filepath, 'utf8');
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
97
98
|
reject(new SimpleError({
|
|
98
|
-
code:
|
|
99
|
-
message:
|
|
100
|
-
field:
|
|
101
|
-
}))
|
|
102
|
-
return
|
|
99
|
+
code: 'invalid_field',
|
|
100
|
+
message: 'Could not read html',
|
|
101
|
+
field: 'html',
|
|
102
|
+
}));
|
|
103
|
+
return;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
resolve({
|
|
106
107
|
html,
|
|
107
108
|
signature: fields.signature,
|
|
108
109
|
cacheId: fields.cacheId,
|
|
109
|
-
timestamp: new Date(parseInt(fields.timestamp as string))
|
|
110
|
-
})
|
|
111
|
-
return
|
|
110
|
+
timestamp: new Date(parseInt(fields.timestamp as string)),
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
112
113
|
});
|
|
113
114
|
});
|
|
114
115
|
|
|
115
116
|
// Verify signature first
|
|
116
117
|
if (!verifyInternalSignature(signature, cacheId, timestamp.getTime().toString(), html)) {
|
|
117
118
|
throw new SimpleError({
|
|
118
|
-
code:
|
|
119
|
-
message:
|
|
120
|
-
})
|
|
119
|
+
code: 'invalid_signature',
|
|
120
|
+
message: 'Invalid signature',
|
|
121
|
+
});
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
let pdf: Buffer | null = null
|
|
124
|
+
let pdf: Buffer | null = null;
|
|
124
125
|
try {
|
|
125
|
-
pdf = await this.htmlToPdf(html, {retryCount: 2, startDate: new Date()})
|
|
126
|
-
}
|
|
127
|
-
|
|
126
|
+
pdf = await this.htmlToPdf(html, { retryCount: 2, startDate: new Date() });
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
console.error(e);
|
|
128
130
|
}
|
|
129
131
|
if (!pdf) {
|
|
130
132
|
throw new SimpleError({
|
|
131
|
-
code:
|
|
132
|
-
message:
|
|
133
|
-
})
|
|
133
|
+
code: 'internal_error',
|
|
134
|
+
message: 'Could not generate pdf',
|
|
135
|
+
});
|
|
134
136
|
}
|
|
135
|
-
await FileCache.write(cacheId, timestamp, pdf)
|
|
136
|
-
const response = new Response(pdf)
|
|
137
|
-
response.headers[
|
|
138
|
-
response.headers[
|
|
137
|
+
await FileCache.write(cacheId, timestamp, pdf);
|
|
138
|
+
const response = new Response(pdf);
|
|
139
|
+
response.headers['Content-Type'] = 'application/pdf';
|
|
140
|
+
response.headers['Content-Length'] = pdf.byteLength.toString();
|
|
139
141
|
return response;
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
browsers: ({browser: Browser
|
|
143
|
-
nextBrowserIndex = 0
|
|
144
|
+
browsers: ({ browser: Browser; count: number } | null)[] = [null, null, null, null];
|
|
145
|
+
nextBrowserIndex = 0;
|
|
144
146
|
|
|
145
147
|
async useBrowser<T>(callback: (browser: Browser) => Promise<T>): Promise<T> {
|
|
146
148
|
this.nextBrowserIndex++;
|
|
147
149
|
if (this.nextBrowserIndex >= this.browsers.length) {
|
|
148
150
|
this.nextBrowserIndex = 0;
|
|
149
151
|
}
|
|
150
|
-
return await QueueHandler.schedule(
|
|
152
|
+
return await QueueHandler.schedule('getBrowser' + this.nextBrowserIndex, async () => {
|
|
151
153
|
if (!this.browsers[this.nextBrowserIndex]) {
|
|
152
|
-
this.browsers[this.nextBrowserIndex] = { browser: await puppeteer.launch({ pipe: true }), count: 0 }
|
|
154
|
+
this.browsers[this.nextBrowserIndex] = { browser: await puppeteer.launch({ pipe: true }), count: 0 };
|
|
153
155
|
}
|
|
154
|
-
const browser = this.browsers[this.nextBrowserIndex]
|
|
156
|
+
const browser = this.browsers[this.nextBrowserIndex]!;
|
|
155
157
|
if (browser.count > 50 || !browser.browser.isConnected()) {
|
|
156
158
|
try {
|
|
157
159
|
await browser.browser.close();
|
|
158
|
-
} catch (e) {
|
|
159
|
-
console.error(e)
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
catch (e) {
|
|
162
|
+
console.error(e);
|
|
163
|
+
}
|
|
164
|
+
this.browsers[this.nextBrowserIndex] = { browser: await puppeteer.launch({ pipe: true }), count: 0 };
|
|
162
165
|
}
|
|
163
|
-
|
|
164
|
-
return await callback(browser.browser)
|
|
166
|
+
|
|
167
|
+
return await callback(browser.browser);
|
|
165
168
|
});
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
async clearBrowser(browser: Browser) {
|
|
169
172
|
try {
|
|
170
173
|
await browser.close();
|
|
171
|
-
} catch (e) {
|
|
172
|
-
console.error(e)
|
|
173
174
|
}
|
|
174
|
-
|
|
175
|
+
catch (e) {
|
|
176
|
+
console.error(e);
|
|
177
|
+
}
|
|
178
|
+
const i = this.browsers.findIndex(b => b?.browser === browser);
|
|
175
179
|
if (i >= 0) {
|
|
176
|
-
this.browsers[i] = null
|
|
180
|
+
this.browsers[i] = null;
|
|
177
181
|
}
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
/**
|
|
181
185
|
* This will move to a different external service
|
|
182
186
|
*/
|
|
183
|
-
async htmlToPdf(html: string, options: {retryCount: number
|
|
187
|
+
async htmlToPdf(html: string, options: { retryCount: number; startDate: Date }): Promise<Buffer | null> {
|
|
184
188
|
const response = await this.useBrowser(async (browser) => {
|
|
185
189
|
try {
|
|
186
190
|
// Create a new page
|
|
187
191
|
const page = await browser.newPage();
|
|
188
192
|
await page.setJavaScriptEnabled(false);
|
|
189
193
|
await page.emulateMediaType('screen');
|
|
190
|
-
await page.setContent(html, { waitUntil: 'load' })
|
|
194
|
+
await page.setContent(html, { waitUntil: 'load' });
|
|
191
195
|
|
|
192
196
|
// Downlaod the PDF
|
|
193
197
|
const pdf = await page.pdf({
|
|
@@ -196,18 +200,19 @@ export class HtmlToPdfEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
196
200
|
printBackground: true,
|
|
197
201
|
format: 'A4',
|
|
198
202
|
preferCSSPageSize: true,
|
|
199
|
-
displayHeaderFooter: false
|
|
203
|
+
displayHeaderFooter: false,
|
|
200
204
|
});
|
|
201
205
|
await page.close();
|
|
202
206
|
return pdf;
|
|
203
|
-
}
|
|
204
|
-
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
console.error('Failed to render document pdf', e);
|
|
205
210
|
return null;
|
|
206
211
|
}
|
|
207
|
-
})
|
|
208
|
-
if (response
|
|
209
|
-
// Retry
|
|
210
|
-
return await this.htmlToPdf(html, {...options, retryCount: options.retryCount - 1})
|
|
212
|
+
});
|
|
213
|
+
if (response === null && options.retryCount > 0 && new Date().getTime() - options.startDate.getTime() < 15000) {
|
|
214
|
+
// Retry
|
|
215
|
+
return await this.htmlToPdf(html, { ...options, retryCount: options.retryCount - 1 });
|
|
211
216
|
}
|
|
212
217
|
return response;
|
|
213
218
|
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { AutoEncoder, DateDecoder, Decoder, field, StringDecoder } from
|
|
2
|
-
import { DecodedRequest, Endpoint, Request, Response } from
|
|
3
|
-
import { SimpleError } from
|
|
1
|
+
import { AutoEncoder, DateDecoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
4
|
|
|
5
|
-
import { FileCache } from
|
|
5
|
+
import { FileCache } from '../helpers/FileCache';
|
|
6
6
|
|
|
7
7
|
type Params = Record<string, never>;
|
|
8
8
|
class Query extends AutoEncoder {
|
|
9
9
|
@field({ decoder: StringDecoder })
|
|
10
|
-
cacheId: string
|
|
10
|
+
cacheId: string;
|
|
11
11
|
|
|
12
12
|
@field({ decoder: DateDecoder })
|
|
13
|
-
timestamp: Date
|
|
13
|
+
timestamp: Date;
|
|
14
14
|
}
|
|
15
|
-
type Body = undefined
|
|
16
|
-
type ResponseBody = Buffer
|
|
15
|
+
type Body = undefined;
|
|
16
|
+
type ResponseBody = Buffer;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
export class PdfCacheEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
23
|
-
queryDecoder = Query as Decoder<Query
|
|
23
|
+
queryDecoder = Query as Decoder<Query>;
|
|
24
24
|
|
|
25
25
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
26
|
-
if (request.method
|
|
26
|
+
if (request.method !== 'GET') {
|
|
27
27
|
return [false];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const params = Endpoint.parseParameters(request.url,
|
|
30
|
+
const params = Endpoint.parseParameters(request.url, '/pdf-cache', {});
|
|
31
31
|
|
|
32
32
|
if (params) {
|
|
33
33
|
return [true, params as Params];
|
|
@@ -36,17 +36,17 @@ export class PdfCacheEndpoint extends Endpoint<Params, Query, Body, ResponseBody
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
39
|
-
const pdf = await FileCache.read(request.query.cacheId, request.query.timestamp)
|
|
39
|
+
const pdf = await FileCache.read(request.query.cacheId, request.query.timestamp);
|
|
40
40
|
if (!pdf) {
|
|
41
41
|
throw new SimpleError({
|
|
42
|
-
code:
|
|
43
|
-
message:
|
|
44
|
-
statusCode: 404
|
|
45
|
-
})
|
|
42
|
+
code: 'cache_not_found',
|
|
43
|
+
message: 'Not cached',
|
|
44
|
+
statusCode: 404,
|
|
45
|
+
});
|
|
46
46
|
}
|
|
47
|
-
const response = new Response(pdf)
|
|
48
|
-
response.headers[
|
|
49
|
-
response.headers[
|
|
47
|
+
const response = new Response(pdf);
|
|
48
|
+
response.headers['Content-Type'] = 'application/pdf';
|
|
49
|
+
response.headers['Content-Length'] = pdf.byteLength.toString();
|
|
50
50
|
return response;
|
|
51
51
|
}
|
|
52
52
|
}
|
package/src/helpers/FileCache.ts
CHANGED
|
@@ -3,45 +3,46 @@ import { promises as fs } from 'fs';
|
|
|
3
3
|
|
|
4
4
|
export class FileCache {
|
|
5
5
|
static async write(cacheId: string, timestamp: Date, data: Buffer) {
|
|
6
|
-
if (cacheId.includes(
|
|
6
|
+
if (cacheId.includes('/')) {
|
|
7
7
|
throw new SimpleError({
|
|
8
|
-
code:
|
|
9
|
-
message:
|
|
10
|
-
field:
|
|
11
|
-
})
|
|
8
|
+
code: 'invalid_field',
|
|
9
|
+
message: 'Invalid cache id',
|
|
10
|
+
field: 'cacheId',
|
|
11
|
+
});
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
const folder = STAMHOOFD.CACHE_PATH +
|
|
15
|
-
await fs.mkdir(folder, { recursive: true })
|
|
13
|
+
|
|
14
|
+
const folder = STAMHOOFD.CACHE_PATH + '/' + cacheId;
|
|
15
|
+
await fs.mkdir(folder, { recursive: true });
|
|
16
16
|
|
|
17
17
|
// Emtpy folder
|
|
18
18
|
const files = await fs.readdir(folder);
|
|
19
19
|
for (const file of files) {
|
|
20
20
|
const fileTimestamp = parseInt(file.substring(0, file.length - 4));
|
|
21
21
|
if (fileTimestamp <= timestamp.getTime()) {
|
|
22
|
-
await fs.unlink(folder +
|
|
22
|
+
await fs.unlink(folder + '/' + file);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const path = folder +
|
|
26
|
+
const path = folder + '/' + timestamp.getTime() + '.pdf';
|
|
27
27
|
await fs.writeFile(path, data);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
static async read(cacheId: string, timestamp: Date): Promise<Buffer | null> {
|
|
31
|
-
if (cacheId.includes(
|
|
31
|
+
if (cacheId.includes('/')) {
|
|
32
32
|
throw new SimpleError({
|
|
33
|
-
code:
|
|
34
|
-
message:
|
|
35
|
-
field:
|
|
36
|
-
})
|
|
33
|
+
code: 'invalid_field',
|
|
34
|
+
message: 'Invalid cache id',
|
|
35
|
+
field: 'cacheId',
|
|
36
|
+
});
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const folder = STAMHOOFD.CACHE_PATH +
|
|
40
|
-
const path = folder +
|
|
39
|
+
const folder = STAMHOOFD.CACHE_PATH + '/' + cacheId;
|
|
40
|
+
const path = folder + '/' + timestamp.getTime() + '.pdf';
|
|
41
41
|
try {
|
|
42
|
-
const data = await fs.readFile(path)
|
|
42
|
+
const data = await fs.readFile(path);
|
|
43
43
|
return data;
|
|
44
|
-
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
45
46
|
// ignore
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -51,17 +52,19 @@ export class FileCache {
|
|
|
51
52
|
const fileTimestamp = parseInt(file.substring(0, file.length - 4));
|
|
52
53
|
if (fileTimestamp >= timestamp.getTime()) {
|
|
53
54
|
try {
|
|
54
|
-
const data = await fs.readFile(folder +
|
|
55
|
+
const data = await fs.readFile(folder + '/' + file);
|
|
55
56
|
return data;
|
|
56
|
-
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
57
59
|
// ignore
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
|
-
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
62
65
|
// ignore
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
return null;
|
|
66
69
|
}
|
|
67
|
-
}
|
|
70
|
+
}
|
package/stamhoofd.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
|
|
2
1
|
export {};
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* Stamhoofd uses a global variable to store some configurations. We don't use process.env because we can only store
|
|
6
|
-
* strings into those files. And we need objects for our localized domains (different domains for each locale).
|
|
4
|
+
* Stamhoofd uses a global variable to store some configurations. We don't use process.env because we can only store
|
|
5
|
+
* strings into those files. And we need objects for our localized domains (different domains for each locale).
|
|
7
6
|
* Having to encode and decode those values would be inefficient.
|
|
8
|
-
*
|
|
9
|
-
* So we use our own global configuration variable: STAMHOOFD. Available everywhere and contains
|
|
10
|
-
* other information depending on the environment (frontend/backend/shared). TypeScript will
|
|
7
|
+
*
|
|
8
|
+
* So we use our own global configuration variable: STAMHOOFD. Available everywhere and contains
|
|
9
|
+
* other information depending on the environment (frontend/backend/shared). TypeScript will
|
|
11
10
|
* always suggest the possible keys.
|
|
12
11
|
*/
|
|
13
12
|
declare global {
|
|
14
|
-
const STAMHOOFD: RendererEnvironment
|
|
15
|
-
}
|
|
13
|
+
const STAMHOOFD: RendererEnvironment;
|
|
14
|
+
}
|
package/.eslintrc.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
root: true,
|
|
3
|
-
ignorePatterns: ["dist/", "node_modules/"],
|
|
4
|
-
parserOptions: {
|
|
5
|
-
"ecmaVersion": 2017
|
|
6
|
-
},
|
|
7
|
-
env: {
|
|
8
|
-
"es6": true,
|
|
9
|
-
"node": true,
|
|
10
|
-
},
|
|
11
|
-
extends: [
|
|
12
|
-
"eslint:recommended",
|
|
13
|
-
],
|
|
14
|
-
plugins: [],
|
|
15
|
-
rules: {
|
|
16
|
-
"no-console": "off",
|
|
17
|
-
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
|
|
18
|
-
"sort-imports": "off",
|
|
19
|
-
"import/order": "off"
|
|
20
|
-
},
|
|
21
|
-
overrides: [
|
|
22
|
-
{
|
|
23
|
-
// Rules for TypeScript and vue
|
|
24
|
-
files: ["*.ts"],
|
|
25
|
-
parser: "@typescript-eslint/parser",
|
|
26
|
-
parserOptions: {
|
|
27
|
-
project: ["./tsconfig.json"]
|
|
28
|
-
},
|
|
29
|
-
plugins: ["@typescript-eslint", "jest"],
|
|
30
|
-
extends: [
|
|
31
|
-
"eslint:recommended",
|
|
32
|
-
"plugin:@typescript-eslint/eslint-recommended",
|
|
33
|
-
"plugin:@typescript-eslint/recommended",
|
|
34
|
-
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
|
35
|
-
"plugin:jest/recommended",
|
|
36
|
-
],
|
|
37
|
-
rules: {
|
|
38
|
-
"no-console": "off",
|
|
39
|
-
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
|
|
40
|
-
"sort-imports": "off",
|
|
41
|
-
"import/order": "off",
|
|
42
|
-
"@typescript-eslint/explicit-function-return-type": "off",
|
|
43
|
-
"@typescript-eslint/no-explicit-any": "off",
|
|
44
|
-
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
|
45
|
-
"@typescript-eslint/no-namespace": "off",
|
|
46
|
-
"@typescript-eslint/no-floating-promises": "error",
|
|
47
|
-
"@typescript-eslint/no-misused-promises": "error",
|
|
48
|
-
"@typescript-eslint/prefer-for-of": "warn",
|
|
49
|
-
"@typescript-eslint/no-empty-interface": "off", // It is convenient to have placeholder interfaces
|
|
50
|
-
"@typescript-eslint/no-this-alias": "off", // No idea why we need this. This breaks code that is just fine. Prohibit the use of function() instead of this rule
|
|
51
|
-
"@typescript-eslint/unbound-method": "off", // Methods are automatically bound in vue, it would break removeEventListeners if we bound it every time unless we save every method in variables again...
|
|
52
|
-
"@typescript-eslint/no-unsafe-assignment": "off", // This is impossible to use with dependencies that don't have types yet, such as tiptap
|
|
53
|
-
"@typescript-eslint/no-unsafe-return": "off", // This is impossible to use with dependencies that don't have types yet, such as tiptap
|
|
54
|
-
"@typescript-eslint/no-unsafe-call": "off", // This is impossible to use with dependencies that don't have types yet, such as tiptap
|
|
55
|
-
"@typescript-eslint/no-unsafe-member-access": "off", // This is impossible to use with dependencies that don't have types yet, such as tiptap
|
|
56
|
-
"@typescript-eslint/restrict-plus-operands": "off", // bullshit one
|
|
57
|
-
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
};
|