@entity-access/server-pages 1.0.29 → 1.0.31
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/.vscode/settings.json +1 -0
- package/dist/Content.d.ts +6 -6
- package/dist/Content.d.ts.map +1 -1
- package/dist/Content.js +21 -37
- package/dist/Content.js.map +1 -1
- package/dist/Page.d.ts +11 -40
- package/dist/Page.d.ts.map +1 -1
- package/dist/Page.js +5 -3
- package/dist/Page.js.map +1 -1
- package/dist/ServerPages.d.ts +13 -5
- package/dist/ServerPages.d.ts.map +1 -1
- package/dist/ServerPages.js +99 -81
- package/dist/ServerPages.js.map +1 -1
- package/dist/core/AsyncStream.d.ts +16 -0
- package/dist/core/AsyncStream.d.ts.map +1 -0
- package/dist/core/AsyncStream.js +55 -0
- package/dist/core/AsyncStream.js.map +1 -0
- package/dist/core/FileApi.d.ts +5 -0
- package/dist/core/FileApi.d.ts.map +1 -0
- package/dist/core/FileApi.js +95 -0
- package/dist/core/FileApi.js.map +1 -0
- package/dist/core/LocalFile.d.ts +2 -1
- package/dist/core/LocalFile.d.ts.map +1 -1
- package/dist/core/LocalFile.js +8 -0
- package/dist/core/LocalFile.js.map +1 -1
- package/dist/core/RouteTree.js +1 -1
- package/dist/core/RouteTree.js.map +1 -1
- package/dist/core/SessionUser.d.ts +2 -2
- package/dist/core/SessionUser.d.ts.map +1 -1
- package/dist/core/SessionUser.js.map +1 -1
- package/dist/core/Wrapped.d.ts +70 -0
- package/dist/core/Wrapped.d.ts.map +1 -0
- package/dist/core/Wrapped.js +253 -0
- package/dist/core/Wrapped.js.map +1 -0
- package/dist/core/cached.d.ts +2 -0
- package/dist/core/cached.d.ts.map +1 -0
- package/dist/core/cached.js +3 -0
- package/dist/core/cached.js.map +1 -0
- package/dist/decorators/Authorize.d.ts +2 -0
- package/dist/decorators/Authorize.d.ts.map +1 -0
- package/dist/decorators/Authorize.js +3 -0
- package/dist/decorators/Authorize.js.map +1 -0
- package/dist/parsers/json/jsonParser.d.ts +2 -0
- package/dist/parsers/json/jsonParser.d.ts.map +1 -0
- package/dist/parsers/json/jsonParser.js +3 -0
- package/dist/parsers/json/jsonParser.js.map +1 -0
- package/dist/services/CookieService.d.ts +3 -3
- package/dist/services/CookieService.d.ts.map +1 -1
- package/dist/services/CookieService.js +2 -4
- package/dist/services/CookieService.js.map +1 -1
- package/dist/ssl/ACME.d.ts +37 -0
- package/dist/ssl/ACME.d.ts.map +1 -0
- package/dist/ssl/ACME.js +200 -0
- package/dist/ssl/ACME.js.map +1 -0
- package/dist/ssl/ChallengeStore.d.ts +8 -0
- package/dist/ssl/ChallengeStore.d.ts.map +1 -0
- package/dist/ssl/ChallengeStore.js +34 -0
- package/dist/ssl/ChallengeStore.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -6
- package/self-signed/cert.crt +22 -0
- package/self-signed/key.pem +27 -0
- package/src/Content.tsx +28 -43
- package/src/Page.tsx +18 -62
- package/src/ServerPages.ts +114 -76
- package/src/core/AsyncStream.ts +83 -0
- package/src/core/FileApi.ts +52 -0
- package/src/core/LocalFile.ts +11 -2
- package/src/core/RouteTree.ts +1 -1
- package/src/core/SessionUser.ts +2 -2
- package/src/core/Wrapped.ts +359 -0
- package/src/core/cached.ts +3 -0
- package/src/decorators/Authorize.ts +3 -0
- package/src/parsers/json/jsonParser.ts +3 -0
- package/src/services/CookieService.ts +4 -6
- package/src/ssl/ACME.ts +247 -0
- package/src/ssl/ChallengeStore.ts +26 -0
- package/test.js +1 -3
package/src/Page.tsx
CHANGED
|
@@ -2,73 +2,21 @@ import busboy from "busboy";
|
|
|
2
2
|
import HtmlDocument from "./html/HtmlDocument.js";
|
|
3
3
|
import XNode from "./html/XNode.js";
|
|
4
4
|
import Content, { IPageResult, Redirect } from "./Content.js";
|
|
5
|
-
import { ServiceProvider } from "@entity-access/entity-access/dist/di/di.js";
|
|
6
|
-
import { Request } from "express";
|
|
7
5
|
import { LocalFile } from "./core/LocalFile.js";
|
|
8
6
|
import TempFolder from "./core/TempFolder.js";
|
|
9
7
|
import SessionUser from "./core/SessionUser.js";
|
|
8
|
+
import { WrappedRequest, WrappedResponse } from "./core/Wrapped.js";
|
|
9
|
+
import { ServiceProvider } from "@entity-access/entity-access/dist/di/di.js";
|
|
10
10
|
|
|
11
11
|
export const isPage = Symbol("isPage");
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
export interface IRouteCheck {
|
|
15
|
+
scope: ServiceProvider;
|
|
15
16
|
method: string;
|
|
16
17
|
current: string;
|
|
17
18
|
path: string[];
|
|
18
|
-
|
|
19
|
-
params: any;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface IPageContext {
|
|
23
|
-
/**
|
|
24
|
-
* path till the current folder where this page is located, including the name of current folder itself.
|
|
25
|
-
*/
|
|
26
|
-
currentPath: string[];
|
|
27
|
-
/**
|
|
28
|
-
* Path to the next children to be precessed.
|
|
29
|
-
*/
|
|
30
|
-
childPath: string[];
|
|
31
|
-
|
|
32
|
-
// /**
|
|
33
|
-
// * List of all paths that were tried before executing this page.
|
|
34
|
-
// */
|
|
35
|
-
// notFoundPath: string[];
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Query string if associated, empty object is always present.
|
|
39
|
-
*/
|
|
40
|
-
query: any;
|
|
41
|
-
|
|
42
|
-
body: any;
|
|
43
|
-
|
|
44
|
-
url: string;
|
|
45
|
-
|
|
46
|
-
signal:AbortSignal;
|
|
47
|
-
/**
|
|
48
|
-
* Request
|
|
49
|
-
*/
|
|
50
|
-
// request: Request;
|
|
51
|
-
/**
|
|
52
|
-
* Response
|
|
53
|
-
*/
|
|
54
|
-
// response: Response;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Request method
|
|
58
|
-
*/
|
|
59
|
-
method: string;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Currently logged in user
|
|
63
|
-
*/
|
|
64
|
-
sessionUser: SessionUser;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Actual file path of the page
|
|
68
|
-
*/
|
|
69
|
-
filePath: string;
|
|
70
|
-
|
|
71
|
-
disposables: Disposable[];
|
|
19
|
+
request: WrappedRequest;
|
|
72
20
|
}
|
|
73
21
|
|
|
74
22
|
export interface IFormData {
|
|
@@ -79,7 +27,7 @@ export interface IFormData {
|
|
|
79
27
|
/**
|
|
80
28
|
* Page should not contain any reference to underlying request/response objects.
|
|
81
29
|
*/
|
|
82
|
-
export default class Page
|
|
30
|
+
export default class Page {
|
|
83
31
|
|
|
84
32
|
static [isPage] = true;
|
|
85
33
|
|
|
@@ -89,10 +37,18 @@ export default class Page implements IPageContext {
|
|
|
89
37
|
* @param pageContext page related items
|
|
90
38
|
* @returns true if it can handle the path, default is true
|
|
91
39
|
*/
|
|
92
|
-
static canHandle(pageContext: IRouteCheck) {
|
|
40
|
+
static canHandle(pageContext: IRouteCheck) : boolean | Promise<boolean> {
|
|
93
41
|
return true;
|
|
94
42
|
}
|
|
95
43
|
|
|
44
|
+
request: WrappedRequest;
|
|
45
|
+
|
|
46
|
+
response: WrappedResponse;
|
|
47
|
+
|
|
48
|
+
get params() {
|
|
49
|
+
return this.request?.query;
|
|
50
|
+
}
|
|
51
|
+
|
|
96
52
|
signal: AbortSignal;
|
|
97
53
|
|
|
98
54
|
currentPath: string[];
|
|
@@ -124,7 +80,7 @@ export default class Page implements IPageContext {
|
|
|
124
80
|
|
|
125
81
|
disposables: Disposable[] = [];
|
|
126
82
|
|
|
127
|
-
private formDataPromise
|
|
83
|
+
private formDataPromise: Promise<IFormData>;
|
|
128
84
|
|
|
129
85
|
constructor() {
|
|
130
86
|
this.cacheControl = "no-cache, no-store, max-age=0";
|
|
@@ -135,13 +91,14 @@ export default class Page implements IPageContext {
|
|
|
135
91
|
}
|
|
136
92
|
|
|
137
93
|
readFormData(): Promise<IFormData> {
|
|
138
|
-
|
|
94
|
+
|
|
95
|
+
return this.formDataPromise ??= (async () => {
|
|
139
96
|
let tempFolder: TempFolder;
|
|
140
97
|
const result: IFormData = {
|
|
141
98
|
fields: {},
|
|
142
99
|
files: []
|
|
143
100
|
};
|
|
144
|
-
const req =
|
|
101
|
+
const req = this.request;
|
|
145
102
|
const bb = busboy({ headers: req.headers , defParamCharset: "utf8" });
|
|
146
103
|
const tasks = [];
|
|
147
104
|
await new Promise((resolve, reject) => {
|
|
@@ -167,7 +124,6 @@ export default class Page implements IPageContext {
|
|
|
167
124
|
await Promise.all(tasks);
|
|
168
125
|
return result;
|
|
169
126
|
})();
|
|
170
|
-
return this.formDataPromise;
|
|
171
127
|
}
|
|
172
128
|
|
|
173
129
|
|
package/src/ServerPages.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import { RegisterSingleton, ServiceProvider } from "@entity-access/entity-access/dist/di/di.js";
|
|
3
|
-
import express, { Request, Response } from "express";
|
|
4
3
|
import Page from "./Page.js";
|
|
5
4
|
import Content from "./Content.js";
|
|
6
|
-
import SessionUser from "./core/SessionUser.js";
|
|
7
5
|
import RouteTree from "./core/RouteTree.js";
|
|
8
|
-
import CookieService from "./services/CookieService.js";
|
|
9
|
-
import cookieParser from "cookie-parser";
|
|
10
|
-
import bodyParser from "body-parser";
|
|
11
6
|
import { fileURLToPath } from "node:url";
|
|
12
7
|
import { dirname, join } from "node:path";
|
|
13
|
-
import { Server
|
|
8
|
+
import { Server } from "socket.io";
|
|
14
9
|
import * as http from "http";
|
|
10
|
+
import * as http2 from "http2";
|
|
15
11
|
import SocketService from "./socket/SocketService.js";
|
|
12
|
+
import { Wrapped } from "./core/Wrapped.js";
|
|
13
|
+
import { SecureContext } from "node:tls";
|
|
14
|
+
import ACME, { IAcmeOptions } from "./ssl/ACME.js";
|
|
16
15
|
|
|
17
16
|
RegisterSingleton
|
|
18
17
|
export default class ServerPages {
|
|
@@ -46,13 +45,24 @@ export default class ServerPages {
|
|
|
46
45
|
* All services should be registered before calling build
|
|
47
46
|
* @param app Express App
|
|
48
47
|
*/
|
|
49
|
-
public async build(
|
|
48
|
+
public async build({
|
|
49
|
+
createSocketService = true,
|
|
50
|
+
port = 8080,
|
|
51
|
+
protocol = "http",
|
|
52
|
+
disableHttp2Warning = false,
|
|
53
|
+
SNICallback,
|
|
54
|
+
acmeOptions
|
|
55
|
+
}:{
|
|
56
|
+
createSocketService?: boolean,
|
|
57
|
+
port: number,
|
|
58
|
+
disableHttp2Warning?: boolean,
|
|
59
|
+
protocol: "http" | "http2" | "https2",
|
|
60
|
+
SNICallback?: (servername: string, cb: (err: Error | null, ctx?: SecureContext) => void) => void,
|
|
61
|
+
acmeOptions?: IAcmeOptions
|
|
62
|
+
}) {
|
|
50
63
|
try {
|
|
51
|
-
// etag must be set by individual request processors if needed.
|
|
52
|
-
app.set("etag", false);
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
app.use(bodyParser.json());
|
|
65
|
+
let httpServer = null as http.Server | http2.Http2Server | http2.Http2SecureServer;
|
|
56
66
|
|
|
57
67
|
let socketServer = null as Server;
|
|
58
68
|
if (createSocketService) {
|
|
@@ -61,95 +71,123 @@ export default class ServerPages {
|
|
|
61
71
|
(ss as any).attach(socketServer);
|
|
62
72
|
await (ss as any).init();
|
|
63
73
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
|
|
75
|
+
switch(protocol) {
|
|
76
|
+
case "http":
|
|
77
|
+
httpServer = http.createServer((req, res) => this.process(req, res))
|
|
78
|
+
break;
|
|
79
|
+
case "https2":
|
|
80
|
+
let sc = null;
|
|
81
|
+
SNICallback ??= (name, cb) => {
|
|
82
|
+
const acme = ServiceProvider.resolve(this, ACME);
|
|
83
|
+
acme.getSecureContext({ ... ( acmeOptions ?? {}), host: name }).then((v) => {
|
|
84
|
+
cb(null, v);
|
|
85
|
+
},cb);
|
|
86
|
+
};
|
|
87
|
+
httpServer = http2.createSecureServer({
|
|
88
|
+
SNICallback
|
|
89
|
+
}, (req, res) => this.process(req, res))
|
|
90
|
+
break;
|
|
91
|
+
case "http2":
|
|
92
|
+
httpServer = http2.createSecureServer({
|
|
93
|
+
},(req, res) => this.process(req, res))
|
|
94
|
+
if (!disableHttp2Warning) {
|
|
95
|
+
console.warn("Http2 without SSL should not be used in production");
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
await new Promise<void>((resolve, reject) => {
|
|
102
|
+
const server = httpServer.listen(port, () => {
|
|
103
|
+
resolve();
|
|
68
104
|
});
|
|
69
105
|
socketServer?.attach(server);
|
|
70
106
|
});
|
|
107
|
+
return httpServer;
|
|
71
108
|
} catch (error) {
|
|
72
109
|
console.error(error);
|
|
73
110
|
}
|
|
74
111
|
return null;
|
|
75
112
|
}
|
|
76
113
|
|
|
77
|
-
protected async process(req:
|
|
114
|
+
protected async process(req: any, resp: any) {
|
|
115
|
+
|
|
116
|
+
req = Wrapped.request(req);
|
|
117
|
+
resp = Wrapped.response(req, resp);
|
|
118
|
+
|
|
119
|
+
req.response = resp;
|
|
78
120
|
|
|
79
121
|
if((req as any).processed) {
|
|
80
122
|
return;
|
|
81
123
|
}
|
|
82
124
|
(req as any).processed = true;
|
|
83
125
|
|
|
84
|
-
// defaulting to no cache
|
|
85
|
-
// static content delivery should override this
|
|
86
|
-
resp.setHeader("cache-control", "no-cache");
|
|
87
|
-
|
|
88
|
-
using scope = ServiceProvider.createScope(this);
|
|
89
|
-
let sent = false;
|
|
90
|
-
const acceptJson = req.accepts().some((s) => /\/json$/i.test(s));
|
|
91
126
|
try {
|
|
92
127
|
|
|
93
|
-
|
|
128
|
+
// defaulting to no cache
|
|
129
|
+
// static content delivery should override this
|
|
130
|
+
resp.setHeader("cache-control", "no-cache");
|
|
131
|
+
|
|
132
|
+
using scope = ServiceProvider.createScope(this);
|
|
133
|
+
let sent = false;
|
|
134
|
+
const acceptJson = req.accepts().some((s) => /\/json$/i.test(s));
|
|
135
|
+
|
|
94
136
|
|
|
95
137
|
try {
|
|
96
|
-
|
|
138
|
+
const path = req.path.split("/").filter((x) => x);
|
|
139
|
+
const method = req.method;
|
|
140
|
+
const { pageClass, childPath } = (await this.root.getRoute({
|
|
141
|
+
scope,
|
|
142
|
+
method,
|
|
143
|
+
current: "",
|
|
144
|
+
path,
|
|
145
|
+
request: req
|
|
146
|
+
})) ?? {
|
|
147
|
+
pageClass: Page,
|
|
148
|
+
childPath: path
|
|
149
|
+
};
|
|
150
|
+
const page = scope.create(pageClass);
|
|
151
|
+
page.method = method;
|
|
152
|
+
page.childPath = childPath;
|
|
153
|
+
page.request = req;
|
|
154
|
+
page.response = resp;
|
|
155
|
+
const content = await page.all(page.params);
|
|
156
|
+
resp.setHeader("cache-control", page.cacheControl);
|
|
157
|
+
resp.removeHeader("etag");
|
|
158
|
+
sent = true;
|
|
159
|
+
await content.send(resp);
|
|
97
160
|
} catch (error) {
|
|
161
|
+
if (!sent) {
|
|
162
|
+
try {
|
|
163
|
+
|
|
164
|
+
if (acceptJson || error.errorModel) {
|
|
165
|
+
await Content.json(
|
|
166
|
+
{
|
|
167
|
+
... error.errorModel ?? {},
|
|
168
|
+
message: error.message ?? error,
|
|
169
|
+
detail: error.stack ?? error,
|
|
170
|
+
}
|
|
171
|
+
, 500).send(resp);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const content = Content.html(`<!DOCTYPE html>\n<html><body><pre>Server Error for ${req.url}\r\n${error?.stack ?? error}</pre></body></html>`, 500);
|
|
176
|
+
await content.send(resp);
|
|
177
|
+
} catch (e1) {
|
|
178
|
+
resp.send(e1.stack ?? e1, 500);
|
|
179
|
+
console.error(e1);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
98
183
|
console.error(error);
|
|
99
184
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const method = req.method;
|
|
105
|
-
const params = { ... req.params, ... req.query, ... req.body ?? {} };
|
|
106
|
-
const { pageClass, childPath } = (await this.root.getRoute({
|
|
107
|
-
method,
|
|
108
|
-
current: "",
|
|
109
|
-
path,
|
|
110
|
-
params,
|
|
111
|
-
sessionUser
|
|
112
|
-
})) ?? {
|
|
113
|
-
pageClass: Page,
|
|
114
|
-
childPath: path
|
|
115
|
-
};
|
|
116
|
-
const page = scope.create(pageClass);
|
|
117
|
-
page.method = method;
|
|
118
|
-
page.childPath = childPath;
|
|
119
|
-
page.body = req.body;
|
|
120
|
-
page.query = req.query;
|
|
121
|
-
page.sessionUser = sessionUser;
|
|
122
|
-
(page as any).req = req;
|
|
123
|
-
(page as any).res = resp;
|
|
124
|
-
const content = await page.all(params);
|
|
125
|
-
resp.setHeader("cache-control", page.cacheControl);
|
|
126
|
-
resp.removeHeader("etag");
|
|
127
|
-
sent = true;
|
|
128
|
-
await content.send(resp);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
if (!sent) {
|
|
131
|
-
try {
|
|
132
|
-
|
|
133
|
-
if (acceptJson || error.errorModel) {
|
|
134
|
-
await Content.json(
|
|
135
|
-
{
|
|
136
|
-
... error.errorModel ?? {},
|
|
137
|
-
message: error.message ?? error,
|
|
138
|
-
detail: error.stack ?? error,
|
|
139
|
-
}
|
|
140
|
-
, 500).send(resp);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const content = Content.html(`<!DOCTYPE html>\n<html><body><pre>Server Error for ${req.url}\r\n${error?.stack ?? error}</pre></body></html>`, 500);
|
|
145
|
-
await content.send(resp);
|
|
146
|
-
} catch (e1) {
|
|
147
|
-
resp.send(e1.stack ?? e1);
|
|
148
|
-
console.error(e1);
|
|
185
|
+
} finally {
|
|
186
|
+
if(Array.isArray(req.disposables)) {
|
|
187
|
+
for (const iterator of req.disposables) {
|
|
188
|
+
iterator[Symbol.dispose]?.();
|
|
149
189
|
}
|
|
150
|
-
return;
|
|
151
190
|
}
|
|
152
|
-
console.error(error);
|
|
153
191
|
}
|
|
154
192
|
}
|
|
155
193
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { closeSync, openSync, read, statSync } from "fs";
|
|
2
|
+
|
|
3
|
+
export abstract class AsyncStream implements Disposable {
|
|
4
|
+
|
|
5
|
+
abstract read(n: number): Promise<Buffer>;
|
|
6
|
+
|
|
7
|
+
abstract [Symbol.dispose]();
|
|
8
|
+
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const maxBufferSize = 16*1024;
|
|
12
|
+
|
|
13
|
+
export class AsyncFileStream extends AsyncStream {
|
|
14
|
+
|
|
15
|
+
public readonly size: number;
|
|
16
|
+
|
|
17
|
+
private fd: any;
|
|
18
|
+
|
|
19
|
+
private buffer: Buffer;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly filePath: string,
|
|
23
|
+
public readPosition = 0,
|
|
24
|
+
lockFile = true,
|
|
25
|
+
bufferSize = maxBufferSize
|
|
26
|
+
) {
|
|
27
|
+
super();
|
|
28
|
+
|
|
29
|
+
if (lockFile) {
|
|
30
|
+
this.fd = openSync(filePath, "r");
|
|
31
|
+
}
|
|
32
|
+
this.buffer = Buffer.alloc(bufferSize);
|
|
33
|
+
const { size } = statSync(filePath);
|
|
34
|
+
this.size = size;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
read(): Promise<Buffer> {
|
|
38
|
+
|
|
39
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
40
|
+
|
|
41
|
+
const size = this.size - this.readPosition;
|
|
42
|
+
|
|
43
|
+
if (size <= 0) {
|
|
44
|
+
resolve(null);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const buffer = (size) > this.buffer.byteLength
|
|
49
|
+
? this.buffer
|
|
50
|
+
: Buffer.alloc(size);
|
|
51
|
+
|
|
52
|
+
this.readPosition += size;
|
|
53
|
+
|
|
54
|
+
if (this.fd) {
|
|
55
|
+
|
|
56
|
+
read(this.fd, buffer, 0, buffer.byteLength, this.readPosition,
|
|
57
|
+
(error) => error ? reject(error) : resolve(buffer));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const fd = openSync(this.filePath, "r");
|
|
61
|
+
read(this.fd, buffer, 0, buffer.byteLength, this.readPosition,
|
|
62
|
+
(error) => {
|
|
63
|
+
try {
|
|
64
|
+
closeSync(fd);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
reject(e);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if(error ) {
|
|
70
|
+
reject(error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
resolve(buffer);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
[Symbol.dispose]() {
|
|
78
|
+
if (this.fd) {
|
|
79
|
+
closeSync(this.fd);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, unlinkSync, mkdirSync } from "fs";
|
|
2
|
+
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { AsyncFileStream } from "./AsyncStream.js";
|
|
5
|
+
|
|
6
|
+
export function ensureParentFolder(filePath: string) {
|
|
7
|
+
ensureDir(dirname(filePath));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function ensureDir(folder: string) {
|
|
11
|
+
|
|
12
|
+
if (existsSync(folder)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parent = dirname(folder);
|
|
17
|
+
if (parent !== "/") {
|
|
18
|
+
ensureDir(parent);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
mkdirSync(folder);
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const deleteIfExists = (path) => existsSync(path) ? unlinkSync(path) : void 0;
|
|
26
|
+
|
|
27
|
+
export const areFilesEqual = async (file1: string, file2: string) => {
|
|
28
|
+
|
|
29
|
+
using f1 = new AsyncFileStream(file1);
|
|
30
|
+
using f2 = new AsyncFileStream(file2);
|
|
31
|
+
|
|
32
|
+
if(f1.size !== f2.size) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for(;;) {
|
|
37
|
+
const [b1,b2] = await Promise.all([f1.read(), f2.read()]);
|
|
38
|
+
if (b1 === null && b2 === null) {
|
|
39
|
+
return f1.readPosition === f2.readPosition;
|
|
40
|
+
}
|
|
41
|
+
if (b1 === null) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (b2 === null) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (!b1.equals(b2)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
};
|
package/src/core/LocalFile.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createReadStream, createWriteStream, existsSync, statSync } from "fs";
|
|
1
|
+
import { createReadStream, createWriteStream, existsSync, read, statSync } from "fs";
|
|
2
2
|
import { basename } from "path";
|
|
3
3
|
import mime from "mime-types";
|
|
4
|
-
import internal, { Stream } from "stream";
|
|
4
|
+
import internal, { Stream, Writable } from "stream";
|
|
5
5
|
import { appendFile, open, readFile, writeFile } from "fs/promises";
|
|
6
6
|
|
|
7
7
|
|
|
@@ -54,6 +54,15 @@ export class LocalFile {
|
|
|
54
54
|
return await readFile(this.path, { flag: "r" });
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
public async writeTo(writable: Writable, start?: number, end?: number) {
|
|
58
|
+
const readable = createReadStream(this.path, { start, end });
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
readable.pipe(writable, { end: true })
|
|
61
|
+
.on("end", resolve)
|
|
62
|
+
.on("error", reject);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
public async delete() {
|
|
58
67
|
return this.onDispose?.();
|
|
59
68
|
}
|
package/src/core/RouteTree.ts
CHANGED
|
@@ -60,7 +60,7 @@ export default class RouteTree {
|
|
|
60
60
|
const pageClassPromise = (this.handler[method] ??= this.handler[method.toLowerCase()]) ?? this.handler["index"];
|
|
61
61
|
if (pageClassPromise) {
|
|
62
62
|
const pageClass = await pageClassPromise;
|
|
63
|
-
if(pageClass.canHandle(rc)) {
|
|
63
|
+
if(await pageClass.canHandle(rc)) {
|
|
64
64
|
return { pageClass, childPath: rc.path };
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/core/SessionUser.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Request, Response } from "express";
|
|
2
1
|
import EntityAccessError from "@entity-access/entity-access/dist/common/EntityAccessError.js";
|
|
3
2
|
import { RegisterScoped } from "@entity-access/entity-access/dist/di/di.js";
|
|
4
3
|
import DateTime from "@entity-access/entity-access/dist/types/DateTime.js";
|
|
5
4
|
import TokenService, { IAuthCookie } from "../services/TokenService.js";
|
|
5
|
+
import { WrappedResponse } from "./Wrapped.js";
|
|
6
6
|
|
|
7
7
|
const secure = (process.env["SOCIAL_MAIL_AUTH_COOKIE_SECURE"] ?? "true") === "true";
|
|
8
8
|
|
|
@@ -48,7 +48,7 @@ export default class SessionUser {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
constructor(
|
|
51
|
-
private resp:
|
|
51
|
+
private resp: WrappedResponse,
|
|
52
52
|
private cookieName: string,
|
|
53
53
|
private tokenService: TokenService
|
|
54
54
|
) {}
|