@e22m4u/js-http-static-router 0.0.1 → 0.1.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/README.md +35 -29
- package/dist/cjs/index.cjs +313 -123
- package/example/server.js +29 -26
- package/example/static/nested/file.txt +11 -0
- package/package.json +4 -4
- package/src/http-static-router.d.ts +29 -26
- package/src/http-static-router.js +259 -150
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/static-route.d.ts +48 -0
- package/src/static-route.js +91 -0
- package/src/utils/get-pathname-from-url.d.ts +7 -0
- package/src/utils/get-pathname-from-url.js +27 -0
- package/src/utils/get-request-pathname.spec.js +37 -0
- package/src/utils/index.d.ts +1 -1
- package/src/utils/index.js +1 -1
- package/example/static/nested/index.html +0 -10
- package/src/types.d.ts +0 -38
- package/src/utils/normalize-path.d.ts +0 -12
- package/src/utils/normalize-path.js +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e22m4u/js-http-static-router",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "HTTP-маршрутизатор статичных ресурсов для Node.js",
|
|
5
5
|
"author": "Mikhail Evstropov <e22m4u@yandex.ru>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"router",
|
|
10
10
|
"http"
|
|
11
11
|
],
|
|
12
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://gitverse.ru/e22m4u/js-http-static-router",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "git+https://
|
|
15
|
+
"url": "git+https://gitverse.ru/e22m4u/js-http-static-router.git"
|
|
16
16
|
},
|
|
17
17
|
"type": "module",
|
|
18
18
|
"types": "./src/index.d.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"eslint-config-prettier": "~10.1.8",
|
|
55
55
|
"eslint-plugin-chai-expect": "~3.1.0",
|
|
56
56
|
"eslint-plugin-import": "~2.32.0",
|
|
57
|
-
"eslint-plugin-jsdoc": "~62.5.
|
|
57
|
+
"eslint-plugin-jsdoc": "~62.5.4",
|
|
58
58
|
"eslint-plugin-mocha": "~11.2.0",
|
|
59
59
|
"globals": "~17.3.0",
|
|
60
60
|
"husky": "~9.1.7",
|
|
@@ -1,53 +1,56 @@
|
|
|
1
1
|
import {ServerResponse} from 'node:http';
|
|
2
2
|
import {IncomingMessage} from 'node:http';
|
|
3
|
+
import {StaticRouteDefinition} from './static-route.js';
|
|
3
4
|
import {DebuggableService, ServiceContainer} from '@e22m4u/js-service';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Http static router options.
|
|
7
8
|
*/
|
|
8
|
-
export type
|
|
9
|
-
|
|
10
|
-
resourcePath: string;
|
|
11
|
-
regexp: RegExp;
|
|
12
|
-
isFile: boolean;
|
|
9
|
+
export type HttpStaticRouterOptions = {
|
|
10
|
+
rootDir?: string;
|
|
13
11
|
};
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
14
|
* Http static router.
|
|
17
15
|
*/
|
|
18
|
-
export class HttpStaticRouter extends DebuggableService {
|
|
16
|
+
export declare class HttpStaticRouter extends DebuggableService {
|
|
19
17
|
/**
|
|
20
18
|
* Constructor.
|
|
21
|
-
*
|
|
22
|
-
* @param container
|
|
19
|
+
*
|
|
20
|
+
* @param container
|
|
23
21
|
*/
|
|
24
|
-
constructor(container
|
|
22
|
+
constructor(container: ServiceContainer);
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
|
-
*
|
|
25
|
+
* Constructor.
|
|
26
|
+
*
|
|
27
|
+
* @param options
|
|
28
|
+
*/
|
|
29
|
+
constructor(options: HttpStaticRouterOptions);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Constructor.
|
|
28
33
|
*
|
|
29
|
-
* @param
|
|
30
|
-
* @param
|
|
34
|
+
* @param container
|
|
35
|
+
* @param options
|
|
31
36
|
*/
|
|
32
|
-
|
|
37
|
+
constructor(container: ServiceContainer, options: HttpStaticRouterOptions);
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
|
-
*
|
|
40
|
+
* Define route.
|
|
36
41
|
*
|
|
37
|
-
* @param
|
|
42
|
+
* @param routeDef
|
|
38
43
|
*/
|
|
39
|
-
|
|
44
|
+
defineRoute(routeDef: StaticRouteDefinition): this;
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
|
-
*
|
|
47
|
+
* Handle request.
|
|
43
48
|
*
|
|
44
|
-
* @param
|
|
45
|
-
* @param
|
|
46
|
-
* @param res
|
|
49
|
+
* @param request
|
|
50
|
+
* @param response
|
|
47
51
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
): void;
|
|
52
|
+
handleRequest(
|
|
53
|
+
request: IncomingMessage,
|
|
54
|
+
response: ServerResponse,
|
|
55
|
+
): Promise<boolean>;
|
|
53
56
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import mimeTypes from 'mime-types';
|
|
3
|
+
import {IncomingMessage} from 'http';
|
|
3
4
|
import fs, {createReadStream} from 'fs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import {StaticRoute} from './static-route.js';
|
|
6
|
+
import {getPathnameFromUrl} from './utils/index.js';
|
|
6
7
|
import {InvalidArgumentError} from '@e22m4u/js-format';
|
|
8
|
+
import {DebuggableService, isServiceContainer} from '@e22m4u/js-service';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} FileInfo
|
|
12
|
+
* @property {string} path
|
|
13
|
+
* @property {number} size
|
|
14
|
+
*/
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
17
|
* Http static router.
|
|
@@ -13,54 +21,108 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
13
21
|
* Routes.
|
|
14
22
|
*
|
|
15
23
|
* @protected
|
|
24
|
+
* @type {StaticRoute[]}
|
|
16
25
|
*/
|
|
17
26
|
_routes = [];
|
|
18
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Options.
|
|
30
|
+
*
|
|
31
|
+
* @type {import('./http-static-router.js').HttpStaticRouterOptions}
|
|
32
|
+
* @protected
|
|
33
|
+
*/
|
|
34
|
+
_options = {};
|
|
35
|
+
|
|
19
36
|
/**
|
|
20
37
|
* Constructor.
|
|
21
38
|
*
|
|
22
|
-
* @param {import('@e22m4u/js-service').ServiceContainer}
|
|
39
|
+
* @param {import('@e22m4u/js-service').ServiceContainer|import('./http-static-router.js').HttpStaticRouterOptions} containerOrOptions
|
|
40
|
+
* @param {import('./http-static-router.js').HttpStaticRouterOptions} options
|
|
23
41
|
*/
|
|
24
|
-
constructor(
|
|
25
|
-
|
|
42
|
+
constructor(containerOrOptions, options) {
|
|
43
|
+
const debugOptions = {
|
|
26
44
|
noEnvironmentNamespace: true,
|
|
27
45
|
namespace: 'jsHttpStaticRouter',
|
|
28
|
-
}
|
|
46
|
+
};
|
|
47
|
+
if (isServiceContainer(containerOrOptions)) {
|
|
48
|
+
super(containerOrOptions, debugOptions);
|
|
49
|
+
} else if (containerOrOptions !== undefined) {
|
|
50
|
+
if (
|
|
51
|
+
!containerOrOptions ||
|
|
52
|
+
typeof containerOrOptions !== 'object' ||
|
|
53
|
+
Array.isArray(containerOrOptions)
|
|
54
|
+
) {
|
|
55
|
+
throw new InvalidArgumentError(
|
|
56
|
+
'Parameter "containerOrOptions" must be an Object ' +
|
|
57
|
+
'or an instance of ServiceContainer, but %v was given.',
|
|
58
|
+
options,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
super(undefined, debugOptions);
|
|
62
|
+
if (options === undefined) {
|
|
63
|
+
options = containerOrOptions;
|
|
64
|
+
containerOrOptions = undefined;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
super(undefined, debugOptions);
|
|
68
|
+
}
|
|
69
|
+
// options
|
|
70
|
+
if (options !== undefined) {
|
|
71
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
72
|
+
throw new InvalidArgumentError(
|
|
73
|
+
'Parameter "options" must be an Object, but %v was given.',
|
|
74
|
+
options,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
// options.rootDir
|
|
78
|
+
if (options.rootDir !== undefined) {
|
|
79
|
+
if (typeof options.rootDir !== 'string') {
|
|
80
|
+
throw new InvalidArgumentError(
|
|
81
|
+
'Option "rootDir" must be a String, but %v was given.',
|
|
82
|
+
options.rootDir,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (!path.isAbsolute(options.rootDir)) {
|
|
86
|
+
throw new InvalidArgumentError(
|
|
87
|
+
'Option "rootDir" must be an absolute path, but %v was given.',
|
|
88
|
+
options.rootDir,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this._options = {...options};
|
|
93
|
+
}
|
|
94
|
+
Object.freeze(this._options);
|
|
29
95
|
}
|
|
30
96
|
|
|
31
97
|
/**
|
|
32
|
-
*
|
|
98
|
+
* Define route.
|
|
33
99
|
*
|
|
34
|
-
* @param {
|
|
35
|
-
* @
|
|
36
|
-
* @returns {object}
|
|
100
|
+
* @param {import('./static-route.js').StaticRouteDefinition} routeDef
|
|
101
|
+
* @returns {this}
|
|
37
102
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
resourcePath = path.resolve(resourcePath);
|
|
41
|
-
debug('Adding a new route.');
|
|
42
|
-
debug('Resource path is %v.', resourcePath);
|
|
43
|
-
debug('Remote path is %v.', remotePath);
|
|
44
|
-
let stats;
|
|
45
|
-
try {
|
|
46
|
-
stats = fs.statSync(resourcePath);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
// если ресурс не существует в момент старта,
|
|
49
|
-
// это может быть ошибкой конфигурации
|
|
50
|
-
console.error(error);
|
|
103
|
+
defineRoute(routeDef) {
|
|
104
|
+
if (!routeDef || typeof routeDef !== 'object' || Array.isArray(routeDef)) {
|
|
51
105
|
throw new InvalidArgumentError(
|
|
52
|
-
'
|
|
53
|
-
|
|
106
|
+
'Parameter "routeDef" must be an Object, but %v was given.',
|
|
107
|
+
routeDef,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (
|
|
111
|
+
this._options.rootDir !== undefined &&
|
|
112
|
+
!path.isAbsolute(routeDef.resourcePath)
|
|
113
|
+
) {
|
|
114
|
+
routeDef = {...routeDef};
|
|
115
|
+
routeDef.resourcePath = path.join(
|
|
116
|
+
this._options.rootDir,
|
|
117
|
+
routeDef.resourcePath,
|
|
54
118
|
);
|
|
55
119
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
: new RegExp(`^${escapedRemotePath}(?:$|\\/)`);
|
|
63
|
-
const route = {remotePath, resourcePath, regexp, isFile};
|
|
120
|
+
const debug = this.getDebuggerFor(this.defineRoute);
|
|
121
|
+
const route = new StaticRoute(routeDef);
|
|
122
|
+
debug('Adding a new route.');
|
|
123
|
+
debug('Resource path is %v.', route.resourcePath);
|
|
124
|
+
debug('Remote path is %v.', route.remotePath);
|
|
125
|
+
debug('Resource type is %s.', route.isFile ? 'File' : 'Folder');
|
|
64
126
|
this._routes.push(route);
|
|
65
127
|
// самые длинные пути проверяются первыми,
|
|
66
128
|
// чтобы избежать коллизий при поиске маршрута
|
|
@@ -69,141 +131,188 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
69
131
|
}
|
|
70
132
|
|
|
71
133
|
/**
|
|
72
|
-
*
|
|
134
|
+
* Handle request.
|
|
73
135
|
*
|
|
74
|
-
* @param {import('http').IncomingMessage}
|
|
75
|
-
* @
|
|
136
|
+
* @param {import('http').IncomingMessage} request
|
|
137
|
+
* @param {import('http').ServerResponse} response
|
|
138
|
+
* @returns {Promise<boolean>}
|
|
76
139
|
*/
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
83
|
-
debug('Method not allowed.');
|
|
84
|
-
return;
|
|
140
|
+
async handleRequest(request, response) {
|
|
141
|
+
const fileInfo = await this._findFileForRequest(request);
|
|
142
|
+
if (fileInfo !== undefined) {
|
|
143
|
+
this._sendFile(request, response, fileInfo);
|
|
144
|
+
return true;
|
|
85
145
|
}
|
|
86
|
-
|
|
87
|
-
const route = this._routes.find(route => {
|
|
88
|
-
const res = route.regexp.test(url);
|
|
89
|
-
const phrase = res ? 'matched' : 'not matched';
|
|
90
|
-
debug('Resource %v %s.', route.resourcePath, phrase);
|
|
91
|
-
return res;
|
|
92
|
-
});
|
|
93
|
-
route
|
|
94
|
-
? debug('Resource %v matched.', route.resourcePath)
|
|
95
|
-
: debug('No route matched.');
|
|
96
|
-
return route;
|
|
146
|
+
return false;
|
|
97
147
|
}
|
|
98
148
|
|
|
99
149
|
/**
|
|
100
|
-
*
|
|
150
|
+
* Find file for request.
|
|
101
151
|
*
|
|
102
|
-
* @param {
|
|
103
|
-
* @
|
|
104
|
-
* @param {import('http').ServerResponse} res
|
|
152
|
+
* @param {import('http').IncomingMessage} request
|
|
153
|
+
* @returns {Promise<FileInfo|undefined>|undefined}
|
|
105
154
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!route.isFile) {
|
|
114
|
-
// извлечение относительного пути в дополнение к адресу
|
|
115
|
-
// ресурса путем удаления из адреса запроса той части,
|
|
116
|
-
// которая была указана при объявлении маршрута
|
|
117
|
-
const relativePath = reqPath.replace(route.regexp, '');
|
|
118
|
-
// объединение адреса ресурса
|
|
119
|
-
// с дополнительной частью
|
|
120
|
-
targetPath = path.join(route.resourcePath, relativePath);
|
|
155
|
+
async _findFileForRequest(request) {
|
|
156
|
+
if (!(request instanceof IncomingMessage)) {
|
|
157
|
+
throw new InvalidArgumentError(
|
|
158
|
+
'Parameter "request" must be an instance of IncomingMessage, ' +
|
|
159
|
+
'but %v was given.',
|
|
160
|
+
request,
|
|
161
|
+
);
|
|
121
162
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
163
|
+
const debug = this.getDebuggerFor(this._findFileForRequest);
|
|
164
|
+
debug('File finding for an incoming request.');
|
|
165
|
+
debug('Incoming request %s %v.', request.method, request.url);
|
|
166
|
+
let requestPath;
|
|
167
|
+
try {
|
|
168
|
+
requestPath = decodeURIComponent(getPathnameFromUrl(request.url || ''));
|
|
169
|
+
} catch {
|
|
170
|
+
debug('Invalid URL encoding .');
|
|
129
171
|
return;
|
|
130
172
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
174
|
+
debug('Method not allowed.');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (requestPath.includes('//')) {
|
|
178
|
+
debug('Request path contains duplicate slashes.');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!this._routes.length) {
|
|
182
|
+
debug('No registered routes.');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
debug('Walking through %v routes.', this._routes.length);
|
|
186
|
+
for (const route of this._routes) {
|
|
187
|
+
const isMatched = route.regexp.test(requestPath);
|
|
188
|
+
if (isMatched) {
|
|
189
|
+
debug('Matched route %v.', route.remotePath);
|
|
190
|
+
// если ресурс ссылается на папку, то из адреса запроса
|
|
191
|
+
// извлекается дополнительная часть (если присутствует),
|
|
192
|
+
// и формируется целевой путь файловой системы
|
|
193
|
+
let targetPath = route.resourcePath;
|
|
194
|
+
if (!route.isFile) {
|
|
195
|
+
// извлечение относительного пути в дополнение к адресу
|
|
196
|
+
// ресурса путем удаления из адреса запроса той части,
|
|
197
|
+
// которая была указана при объявлении маршрута
|
|
198
|
+
const relativePath = requestPath.replace(route.regexp, '');
|
|
199
|
+
// объединение адреса ресурса
|
|
200
|
+
// с дополнительной частью
|
|
201
|
+
targetPath = path.join(route.resourcePath, relativePath);
|
|
149
202
|
}
|
|
150
|
-
// если
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
203
|
+
// если обнаружена попытка выхода за пределы
|
|
204
|
+
// директории маршрута, то возвращается undefined
|
|
205
|
+
targetPath = path.resolve(targetPath);
|
|
206
|
+
const resourceRoot = path.resolve(route.resourcePath);
|
|
207
|
+
if (
|
|
208
|
+
targetPath !== resourceRoot &&
|
|
209
|
+
!targetPath.startsWith(resourceRoot + path.sep)
|
|
210
|
+
) {
|
|
156
211
|
return;
|
|
157
212
|
}
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return;
|
|
213
|
+
// с определением размера файла одновременно выполняется
|
|
214
|
+
// отсечение отсутствующих файлов и директорий, в таких
|
|
215
|
+
// случаях значение размера будет является undefined
|
|
216
|
+
const fileSize = await new Promise(resolve => {
|
|
217
|
+
fs.stat(targetPath, (statsError, stats) => {
|
|
218
|
+
if (statsError || stats.isDirectory()) {
|
|
219
|
+
resolve(undefined);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
resolve(stats.size);
|
|
223
|
+
return;
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
// если размер файла определен, то поиск
|
|
227
|
+
// прерывается и возвращается информация
|
|
228
|
+
if (fileSize !== undefined) {
|
|
229
|
+
// если файл найден, но запрос заканчивается
|
|
230
|
+
// на слеш, то файл должен быть проигнорирован
|
|
231
|
+
if (requestPath.endsWith('/')) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
debug('File found %v.', targetPath);
|
|
235
|
+
return {path: targetPath, size: fileSize};
|
|
182
236
|
}
|
|
183
|
-
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
debug('File not found.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Send file.
|
|
244
|
+
*
|
|
245
|
+
* @param {import('http').IncomingMessage} request
|
|
246
|
+
* @param {import('http').ServerResponse} response
|
|
247
|
+
* @param {FileInfo} fileInfo
|
|
248
|
+
*/
|
|
249
|
+
_sendFile(request, response, fileInfo) {
|
|
250
|
+
const debug = this.getDebuggerFor(this._sendFile);
|
|
251
|
+
debug('File sending for an incoming request.');
|
|
252
|
+
debug('Incoming request %s %v.', request.method, request.url);
|
|
253
|
+
debug('File path %v.', fileInfo.path);
|
|
254
|
+
debug('File size %v bytes.', fileInfo.size);
|
|
255
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
256
|
+
debug('Method not allowed.');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// формирование заголовка "content-type"
|
|
260
|
+
// в зависимости от расширения файла
|
|
261
|
+
const extname = path.extname(fileInfo.path);
|
|
262
|
+
const contentType =
|
|
263
|
+
mimeTypes.contentType(extname) || 'application/octet-stream';
|
|
264
|
+
// файл читается и отправляется частями,
|
|
265
|
+
// что значительно снижает использование памяти
|
|
266
|
+
const fileStream = createReadStream(fileInfo.path);
|
|
267
|
+
fileStream.on('error', error => {
|
|
268
|
+
debug('Unable to open a file stream.');
|
|
269
|
+
this._handleFsError(error, response);
|
|
270
|
+
});
|
|
271
|
+
// отправка заголовка 200, только после
|
|
272
|
+
// этого начинается отдача файла
|
|
273
|
+
fileStream.on('open', () => {
|
|
274
|
+
response.writeHead(200, {
|
|
275
|
+
'Content-Type': contentType,
|
|
276
|
+
'Content-Length': fileInfo.size,
|
|
184
277
|
});
|
|
278
|
+
// для HEAD запроса отправляются
|
|
279
|
+
// только заголовки (без тела)
|
|
280
|
+
if (request.method === 'HEAD') {
|
|
281
|
+
response.end();
|
|
282
|
+
debug('Response has been sent without a body for the HEAD request.');
|
|
283
|
+
// важно закрыть файловый поток, чтобы операционная
|
|
284
|
+
// система не исчерпала лимит открытых файлов
|
|
285
|
+
fileStream.destroy();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
fileStream.pipe(response);
|
|
289
|
+
});
|
|
290
|
+
request.on('close', () => {
|
|
291
|
+
debug('File has been sent.');
|
|
292
|
+
fileStream.destroy();
|
|
185
293
|
});
|
|
186
294
|
}
|
|
187
|
-
}
|
|
188
295
|
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Handle filesystem error.
|
|
298
|
+
*
|
|
299
|
+
* @param {object} error
|
|
300
|
+
* @param {object} response
|
|
301
|
+
* @returns {undefined}
|
|
302
|
+
*/
|
|
303
|
+
_handleFsError(error, response) {
|
|
304
|
+
if (response.headersSent) {
|
|
305
|
+
response.destroy();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if ('code' in error && error.code === 'ENOENT') {
|
|
309
|
+
response.writeHead(404, {'Content-Type': 'text/plain'});
|
|
310
|
+
response.write('404 Not Found');
|
|
311
|
+
response.end();
|
|
312
|
+
} else {
|
|
313
|
+
response.writeHead(500, {'Content-Type': 'text/plain'});
|
|
314
|
+
response.write('500 Internal Server Error');
|
|
315
|
+
response.end();
|
|
316
|
+
}
|
|
208
317
|
}
|
|
209
318
|
}
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Static route definition.
|
|
4
|
+
*/
|
|
5
|
+
export type StaticRouteDefinition = {
|
|
6
|
+
remotePath: string;
|
|
7
|
+
resourcePath: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Static route.
|
|
12
|
+
*/
|
|
13
|
+
export class StaticRoute {
|
|
14
|
+
/**
|
|
15
|
+
* Remote path.
|
|
16
|
+
*
|
|
17
|
+
* @type {string}
|
|
18
|
+
*/
|
|
19
|
+
readonly remotePath: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resource path.
|
|
23
|
+
*
|
|
24
|
+
* @type {string}
|
|
25
|
+
*/
|
|
26
|
+
readonly resourcePath: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* RegExp.
|
|
30
|
+
*
|
|
31
|
+
* @type {RegExp}
|
|
32
|
+
*/
|
|
33
|
+
readonly regexp: RegExp;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Is file.
|
|
37
|
+
*
|
|
38
|
+
* @type {boolean}
|
|
39
|
+
*/
|
|
40
|
+
readonly isFile: boolean;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Constructor.
|
|
44
|
+
*
|
|
45
|
+
* @param routeDef
|
|
46
|
+
*/
|
|
47
|
+
constructor(routeDef: StaticRouteDefinition);
|
|
48
|
+
}
|