@thi.ng/server 0.8.2 → 0.9.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/CHANGELOG.md +14 -1
- package/README.md +19 -5
- package/interceptors/static.d.ts +96 -0
- package/interceptors/static.js +73 -0
- package/package.json +2 -2
- package/server.js +1 -1
- package/static.d.ts +37 -11
- package/static.js +46 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
- **Last updated**: 2025-03-
|
|
3
|
+
- **Last updated**: 2025-03-22T12:33:45Z
|
|
4
4
|
- **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
|
|
5
5
|
|
|
6
6
|
All notable changes to this project will be documented in this file.
|
|
@@ -11,6 +11,19 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
|
|
|
11
11
|
**Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
|
|
12
12
|
and/or version bumps of transitive dependencies.
|
|
13
13
|
|
|
14
|
+
## [0.9.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.9.0) (2025-03-22)
|
|
15
|
+
|
|
16
|
+
#### 🚀 Features
|
|
17
|
+
|
|
18
|
+
- update static file serving ([e3ee3d6](https://github.com/thi-ng/umbrella/commit/e3ee3d6))
|
|
19
|
+
- update `StaticOpts` to support multiple `filters` & individual `headers` per file
|
|
20
|
+
- update `StaticOpts.prefix` type
|
|
21
|
+
- consolidate GET & HEAD handlers
|
|
22
|
+
- update `__fileHeaders()` to perform more checks and inject headers
|
|
23
|
+
- update `etagFileTimeModified()` to return base36 timestamp
|
|
24
|
+
- add `pathFilterASCII()` and `pathMaxLength()` filter predicates
|
|
25
|
+
- use `pathFilterASCII()` as default filter
|
|
26
|
+
|
|
14
27
|
## [0.8.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/server@0.8.0) (2025-03-13)
|
|
15
28
|
|
|
16
29
|
#### 🚀 Features
|
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
- [Available interceptors](#available-interceptors)
|
|
21
21
|
- [Custom interceptors](#custom-interceptors)
|
|
22
22
|
- [Using interceptors](#using-interceptors)
|
|
23
|
+
- [Static file serving](#static-file-serving)
|
|
23
24
|
- [Status](#status)
|
|
24
25
|
- [Installation](#installation)
|
|
25
26
|
- [Dependencies](#dependencies)
|
|
@@ -49,9 +50,9 @@ implementations.
|
|
|
49
50
|
- Global interceptors for all routes and/or local for individual routes & HTTP methods
|
|
50
51
|
- Automatic parsing of cookies and URL query strings (incl. nested params)
|
|
51
52
|
- In-memory session storage & route interceptor
|
|
52
|
-
- Configurable file serving
|
|
53
|
-
detection and support for
|
|
54
|
-
compression
|
|
53
|
+
- Configurable [static file serving](#static-file-serving)
|
|
54
|
+
(`ReadableStream`-based) with automatic MIME-type detection and support for
|
|
55
|
+
Etags, as well as Brotli, Gzip and Deflate compression
|
|
55
56
|
- Utilities for parsing form-encoded multipart request bodies
|
|
56
57
|
|
|
57
58
|
### Interceptors
|
|
@@ -118,6 +119,19 @@ import { cacheControl } from "@thi.ng/server";
|
|
|
118
119
|
}
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
### Static file serving
|
|
123
|
+
|
|
124
|
+
The
|
|
125
|
+
[`staticFiles()`](https://docs.thi.ng/umbrella/server/functions/staticFiles.html)
|
|
126
|
+
route provider can be used to serve files from a given local root directory.
|
|
127
|
+
Multiple such routes can be defined. The handler is highly configurable in terms
|
|
128
|
+
of path validation/filtering, global and/or per-file headers, Etag generation,
|
|
129
|
+
compression. It also supports its own set of [interceptors](#interceptors).
|
|
130
|
+
|
|
131
|
+
See
|
|
132
|
+
[`StaticOpts`](https://docs.thi.ng/umbrella/server/interfaces/StaticOpts.html)
|
|
133
|
+
and example below for more details.
|
|
134
|
+
|
|
121
135
|
## Status
|
|
122
136
|
|
|
123
137
|
**ALPHA** - bleeding edge / work-in-progress
|
|
@@ -150,7 +164,7 @@ For Node.js REPL:
|
|
|
150
164
|
const ser = await import("@thi.ng/server");
|
|
151
165
|
```
|
|
152
166
|
|
|
153
|
-
Package sizes (brotli'd, pre-treeshake): ESM: 6.
|
|
167
|
+
Package sizes (brotli'd, pre-treeshake): ESM: 6.25 KB
|
|
154
168
|
|
|
155
169
|
## Dependencies
|
|
156
170
|
|
|
@@ -220,7 +234,7 @@ const app = srv.server<AppCtx>({
|
|
|
220
234
|
// use compression (if client supports it)
|
|
221
235
|
compress: true,
|
|
222
236
|
// route prefix
|
|
223
|
-
prefix: "assets",
|
|
237
|
+
prefix: "/assets",
|
|
224
238
|
// map to current CWD
|
|
225
239
|
rootDir: ".",
|
|
226
240
|
// strategy for computing etags (optional)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Fn, MaybePromise, Predicate } from "@thi.ng/api";
|
|
2
|
+
import { type HashAlgo } from "@thi.ng/file-io";
|
|
3
|
+
import type { OutgoingHttpHeaders } from "node:http";
|
|
4
|
+
import type { Interceptor, RequestCtx, ServerRoute } from "./api.js";
|
|
5
|
+
/**
|
|
6
|
+
* Static file configuration options.
|
|
7
|
+
*/
|
|
8
|
+
export interface StaticOpts<CTX extends RequestCtx = RequestCtx> {
|
|
9
|
+
/**
|
|
10
|
+
* Path to local root directory for static assets. Also see
|
|
11
|
+
* {@link StaticOpts.prefix}
|
|
12
|
+
*
|
|
13
|
+
* @defaultValue `.` (current cwd)
|
|
14
|
+
*/
|
|
15
|
+
rootDir: string;
|
|
16
|
+
/**
|
|
17
|
+
* Filter predicate to exclude files from being served. Called with the
|
|
18
|
+
* absolute local file path. If the function returns false, the server
|
|
19
|
+
* produces a 404 response. By default all files (within
|
|
20
|
+
* {@link StaticOpts.rootDir}) will be allowed.
|
|
21
|
+
*/
|
|
22
|
+
filter: Predicate<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Base URL prefix for static assets. Also see {@link StaticOpts.rootDir}
|
|
25
|
+
*
|
|
26
|
+
* @defaultValue "static"
|
|
27
|
+
*/
|
|
28
|
+
prefix: string;
|
|
29
|
+
/**
|
|
30
|
+
* Additional route specific interceptors.
|
|
31
|
+
*/
|
|
32
|
+
intercept: Interceptor<CTX>[];
|
|
33
|
+
/**
|
|
34
|
+
* Additional common headers (e.g. cache control) for all static files
|
|
35
|
+
*/
|
|
36
|
+
headers: OutgoingHttpHeaders;
|
|
37
|
+
/**
|
|
38
|
+
* If true (default: false), files will be served with brotli, gzip or deflate
|
|
39
|
+
* compression (if the client supports it).
|
|
40
|
+
*
|
|
41
|
+
* @defaultValue false
|
|
42
|
+
*/
|
|
43
|
+
compress: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* User defined function to compute an Etag value for given file path. The
|
|
46
|
+
* file is guaranteed to exist when this function is called.
|
|
47
|
+
*/
|
|
48
|
+
etag: Fn<string, MaybePromise<string>>;
|
|
49
|
+
/**
|
|
50
|
+
* If true, the route will have its `auth` flag enabled, e.g. for use with
|
|
51
|
+
* the {@link authenticateWith} interceptor.
|
|
52
|
+
*
|
|
53
|
+
* @defaultValue false
|
|
54
|
+
*/
|
|
55
|
+
auth: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Defines a configurable {@link ServerRoute} and handler for serving static
|
|
59
|
+
* files from a local directory (optionally with compression, see
|
|
60
|
+
* {@link StaticOpts.compress} for details).
|
|
61
|
+
*
|
|
62
|
+
* @param opts
|
|
63
|
+
*/
|
|
64
|
+
export declare const staticFiles: <CTX extends RequestCtx = RequestCtx>({
|
|
65
|
+
prefix,
|
|
66
|
+
rootDir,
|
|
67
|
+
intercept,
|
|
68
|
+
filter,
|
|
69
|
+
compress,
|
|
70
|
+
auth,
|
|
71
|
+
etag,
|
|
72
|
+
headers,
|
|
73
|
+
}?: Partial<StaticOpts<CTX>>) => ServerRoute<CTX>;
|
|
74
|
+
/**
|
|
75
|
+
* Etag header value function for {@link StaticOpts.etag}. Computes Etag based
|
|
76
|
+
* on file modified date.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* Also see {@link etagFileHash}.
|
|
80
|
+
*
|
|
81
|
+
* @param path
|
|
82
|
+
*/
|
|
83
|
+
export declare const etagFileTimeModified: (path: string) => string;
|
|
84
|
+
/**
|
|
85
|
+
* Higher-order Etag header value function for {@link StaticOpts.etag}. Computes
|
|
86
|
+
* Etag value by computing the hash digest of a given file. Uses MD5 by default.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* Also see {@link etagFileTimeModified}.
|
|
90
|
+
*
|
|
91
|
+
* @param algo
|
|
92
|
+
*/
|
|
93
|
+
export declare const etagFileHash: (
|
|
94
|
+
algo?: HashAlgo
|
|
95
|
+
) => (path: string) => Promise<string>;
|
|
96
|
+
//# sourceMappingURL=static.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { fileHash as $fileHash } from "@thi.ng/file-io";
|
|
2
|
+
import { preferredTypeForPath } from "@thi.ng/mime";
|
|
3
|
+
import { existsSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { isUnmodified } from "../utils/cache.js";
|
|
6
|
+
const staticFiles = ({
|
|
7
|
+
prefix = "static",
|
|
8
|
+
rootDir = ".",
|
|
9
|
+
intercept = [],
|
|
10
|
+
filter = () => true,
|
|
11
|
+
compress = false,
|
|
12
|
+
auth = false,
|
|
13
|
+
etag,
|
|
14
|
+
headers
|
|
15
|
+
} = {}) => ({
|
|
16
|
+
id: "__static",
|
|
17
|
+
match: [prefix, "+"],
|
|
18
|
+
auth,
|
|
19
|
+
handlers: {
|
|
20
|
+
head: {
|
|
21
|
+
fn: async (ctx) => {
|
|
22
|
+
const path = join(rootDir, ...ctx.match.rest);
|
|
23
|
+
ctx.logger.debug("path", path);
|
|
24
|
+
const $headers = await __fileHeaders(
|
|
25
|
+
path,
|
|
26
|
+
ctx,
|
|
27
|
+
filter,
|
|
28
|
+
etag,
|
|
29
|
+
headers
|
|
30
|
+
);
|
|
31
|
+
if (!$headers) return;
|
|
32
|
+
ctx.res.writeHead(200, {
|
|
33
|
+
"content-type": preferredTypeForPath(path),
|
|
34
|
+
...$headers
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
intercept
|
|
38
|
+
},
|
|
39
|
+
get: {
|
|
40
|
+
fn: async (ctx) => {
|
|
41
|
+
const path = join(rootDir, ...ctx.match.rest);
|
|
42
|
+
ctx.logger.debug("path", path);
|
|
43
|
+
const $headers = await __fileHeaders(
|
|
44
|
+
path,
|
|
45
|
+
ctx,
|
|
46
|
+
filter,
|
|
47
|
+
etag,
|
|
48
|
+
headers
|
|
49
|
+
);
|
|
50
|
+
if (!$headers) return;
|
|
51
|
+
return ctx.server.sendFile(ctx, path, $headers, compress);
|
|
52
|
+
},
|
|
53
|
+
intercept
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const __fileHeaders = async (path, ctx, filter, etag, headers) => {
|
|
58
|
+
if (!(existsSync(path) && filter(path))) {
|
|
59
|
+
return ctx.res.missing();
|
|
60
|
+
}
|
|
61
|
+
if (etag) {
|
|
62
|
+
const etagValue = await etag(path);
|
|
63
|
+
return isUnmodified(etagValue, ctx.req.headers["if-none-match"]) ? ctx.res.unmodified() : { ...headers, etag: etagValue };
|
|
64
|
+
}
|
|
65
|
+
return { ...headers };
|
|
66
|
+
};
|
|
67
|
+
const etagFileTimeModified = (path) => String(statSync(path).mtimeMs);
|
|
68
|
+
const etagFileHash = (algo = "md5") => (path) => $fileHash(path, void 0, algo);
|
|
69
|
+
export {
|
|
70
|
+
etagFileHash,
|
|
71
|
+
etagFileTimeModified,
|
|
72
|
+
staticFiles
|
|
73
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thi.ng/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -167,5 +167,5 @@
|
|
|
167
167
|
"status": "alpha",
|
|
168
168
|
"year": 2024
|
|
169
169
|
},
|
|
170
|
-
"gitHead": "
|
|
170
|
+
"gitHead": "0e2dadea20613d4a349177a669d73d7a4257898d\n"
|
|
171
171
|
}
|
package/server.js
CHANGED
package/static.d.ts
CHANGED
|
@@ -14,30 +14,40 @@ export interface StaticOpts<CTX extends RequestCtx = RequestCtx> {
|
|
|
14
14
|
*/
|
|
15
15
|
rootDir: string;
|
|
16
16
|
/**
|
|
17
|
-
* Filter
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* {@link
|
|
17
|
+
* Filter predicates to validate & exclude file paths from being served.
|
|
18
|
+
* Executed in given order and each one called with the absolute local file
|
|
19
|
+
* path. If any of the predicates returns false, the server produces a 404
|
|
20
|
+
* response. By default only uses {@link pathFilterASCII}.
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
filters: Predicate<string>[];
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* URL path prefix for defining the static assets route. Also see
|
|
25
|
+
* {@link StaticOpts.rootDir}.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* If given as array, each item is a path component, i.e. `["public",
|
|
29
|
+
* "img"]` is the same as `"/public/img"`.
|
|
25
30
|
*
|
|
26
31
|
* @defaultValue "static"
|
|
27
32
|
*/
|
|
28
|
-
prefix: string;
|
|
33
|
+
prefix: string | string[];
|
|
29
34
|
/**
|
|
30
35
|
* Additional route specific interceptors.
|
|
31
36
|
*/
|
|
32
37
|
intercept: Interceptor<CTX>[];
|
|
33
38
|
/**
|
|
34
|
-
* Additional common headers (e.g. cache control) for
|
|
39
|
+
* Additional common headers (e.g. cache control) for a file. If given as
|
|
40
|
+
* function, it will be called with the absolute path (the function will
|
|
41
|
+
* only be called if the path exists and already has passed validation).
|
|
35
42
|
*/
|
|
36
|
-
headers: OutgoingHttpHeaders
|
|
43
|
+
headers: OutgoingHttpHeaders | Fn<string, OutgoingHttpHeaders>;
|
|
37
44
|
/**
|
|
38
45
|
* If true (default: false), files will be served with brotli, gzip or deflate
|
|
39
46
|
* compression (if the client supports it).
|
|
40
47
|
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* Note: Whilst compression can
|
|
50
|
+
*
|
|
41
51
|
* @defaultValue false
|
|
42
52
|
*/
|
|
43
53
|
compress: boolean;
|
|
@@ -61,10 +71,10 @@ export interface StaticOpts<CTX extends RequestCtx = RequestCtx> {
|
|
|
61
71
|
*
|
|
62
72
|
* @param opts
|
|
63
73
|
*/
|
|
64
|
-
export declare const staticFiles: <CTX extends RequestCtx = RequestCtx>({ prefix, rootDir, intercept,
|
|
74
|
+
export declare const staticFiles: <CTX extends RequestCtx = RequestCtx>({ prefix, rootDir, intercept, filters, compress, auth, etag, headers, }?: Partial<StaticOpts<CTX>>) => ServerRoute<CTX>;
|
|
65
75
|
/**
|
|
66
76
|
* Etag header value function for {@link StaticOpts.etag}. Computes Etag based
|
|
67
|
-
* on file modified
|
|
77
|
+
* on base36-encoded file modified timestamp.
|
|
68
78
|
*
|
|
69
79
|
* @remarks
|
|
70
80
|
* Also see {@link etagFileHash}.
|
|
@@ -82,4 +92,20 @@ export declare const etagFileTimeModified: (path: string) => string;
|
|
|
82
92
|
* @param algo
|
|
83
93
|
*/
|
|
84
94
|
export declare const etagFileHash: (algo?: HashAlgo) => (path: string) => Promise<string>;
|
|
95
|
+
/**
|
|
96
|
+
* Default path filter predicate for {@link StaticOpts.filter}. Rejects a file
|
|
97
|
+
* path which contains non-printable ASCII chars (i.e. outside the
|
|
98
|
+
* `[0x20,0x7fe]` range).
|
|
99
|
+
*
|
|
100
|
+
* @param path
|
|
101
|
+
*/
|
|
102
|
+
export declare const pathFilterASCII: (path: string) => boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Higher order path filter predicate for {@link StaticOpts.filter}. The
|
|
105
|
+
* returned predicate rejects an absolute file path if its length exceeds given
|
|
106
|
+
* `limit`.
|
|
107
|
+
*
|
|
108
|
+
* @param limit
|
|
109
|
+
*/
|
|
110
|
+
export declare const pathMaxLength: (limit: number) => Predicate<string>;
|
|
85
111
|
//# sourceMappingURL=static.d.ts.map
|
package/static.js
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
+
import { isFunction, isString } from "@thi.ng/checks";
|
|
1
2
|
import { fileHash as $fileHash } from "@thi.ng/file-io";
|
|
2
3
|
import { preferredTypeForPath } from "@thi.ng/mime";
|
|
3
4
|
import { existsSync, statSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
5
6
|
import { isUnmodified } from "./utils/cache.js";
|
|
6
7
|
const staticFiles = ({
|
|
7
8
|
prefix = "static",
|
|
8
9
|
rootDir = ".",
|
|
9
10
|
intercept = [],
|
|
10
|
-
|
|
11
|
+
filters = [pathFilterASCII],
|
|
11
12
|
compress = false,
|
|
12
13
|
auth = false,
|
|
13
14
|
etag,
|
|
14
15
|
headers
|
|
15
|
-
} = {}) =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ctx
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
path,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
headers
|
|
47
|
-
);
|
|
48
|
-
if (!$headers) return;
|
|
49
|
-
return ctx.server.sendFile(ctx, path, $headers, compress);
|
|
50
|
-
},
|
|
51
|
-
intercept
|
|
16
|
+
} = {}) => {
|
|
17
|
+
rootDir = resolve(rootDir);
|
|
18
|
+
const filter = (path) => filters.every((f) => f(path));
|
|
19
|
+
return {
|
|
20
|
+
id: "__static",
|
|
21
|
+
match: isString(prefix) ? prefix + "/+" : [...prefix, "+"],
|
|
22
|
+
auth,
|
|
23
|
+
handlers: {
|
|
24
|
+
get: {
|
|
25
|
+
fn: async (ctx) => {
|
|
26
|
+
const path = resolve(join(rootDir, ...ctx.match.rest));
|
|
27
|
+
const $headers = await __fileHeaders(
|
|
28
|
+
rootDir,
|
|
29
|
+
path,
|
|
30
|
+
ctx,
|
|
31
|
+
filter,
|
|
32
|
+
etag,
|
|
33
|
+
headers
|
|
34
|
+
);
|
|
35
|
+
if (!$headers) return;
|
|
36
|
+
if (ctx.origMethod === "head") {
|
|
37
|
+
ctx.res.writeHead(200, {
|
|
38
|
+
"content-type": preferredTypeForPath(path),
|
|
39
|
+
...$headers
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
return ctx.server.sendFile(ctx, path, $headers, compress);
|
|
44
|
+
},
|
|
45
|
+
intercept
|
|
46
|
+
}
|
|
52
47
|
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const __fileHeaders = async (path, ctx, filter, etag, headers) => {
|
|
56
|
-
if (!(
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const __fileHeaders = async (rootDir, path, ctx, filter, etag, headers) => {
|
|
51
|
+
if (!(path.length > rootDir.length && filter(path) && existsSync(path))) {
|
|
57
52
|
return ctx.res.missing();
|
|
58
53
|
}
|
|
54
|
+
const $headers = isFunction(headers) ? headers(path) : headers;
|
|
59
55
|
if (etag) {
|
|
60
56
|
const etagValue = await etag(path);
|
|
61
|
-
return isUnmodified(etagValue, ctx.req.headers["if-none-match"]) ? ctx.res.unmodified() : {
|
|
57
|
+
return isUnmodified(etagValue, ctx.req.headers["if-none-match"]) ? ctx.res.unmodified() : { ...$headers, etag: etagValue };
|
|
62
58
|
}
|
|
63
|
-
return {
|
|
59
|
+
return { ...$headers };
|
|
64
60
|
};
|
|
65
|
-
const etagFileTimeModified = (path) =>
|
|
61
|
+
const etagFileTimeModified = (path) => statSync(path).mtimeMs.toString(36);
|
|
66
62
|
const etagFileHash = (algo = "md5") => (path) => $fileHash(path, void 0, algo);
|
|
63
|
+
const pathFilterASCII = (path) => !/[^\x20-\x7e]/.test(path);
|
|
64
|
+
const pathMaxLength = (limit) => (path) => path.length < limit;
|
|
67
65
|
export {
|
|
68
66
|
etagFileHash,
|
|
69
67
|
etagFileTimeModified,
|
|
68
|
+
pathFilterASCII,
|
|
69
|
+
pathMaxLength,
|
|
70
70
|
staticFiles
|
|
71
71
|
};
|