koa-files 5.0.1 → 5.0.2
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/cjs/ReadStream.cjs +5 -5
- package/cjs/Service.cjs +9 -15
- package/cjs/index.cjs +1 -1
- package/cjs/utils/fs.cjs +1 -1
- package/cjs/utils/http.cjs +57 -31
- package/cjs/utils/http.d.cts +26 -1
- package/cjs/utils/path.cjs +1 -1
- package/cjs/utils/type.cjs +1 -1
- package/esm/ReadStream.js +5 -5
- package/esm/Service.js +9 -14
- package/esm/index.js +1 -1
- package/esm/utils/fs.js +1 -1
- package/esm/utils/http.d.ts +26 -1
- package/esm/utils/http.js +63 -32
- package/esm/utils/path.js +1 -1
- package/esm/utils/type.js +1 -1
- package/package.json +23 -23
- package/cjs/utils/hash.cjs +0 -47
- package/esm/utils/hash.js +0 -45
package/cjs/ReadStream.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module koa-files
|
|
3
3
|
* @package koa-files
|
|
4
4
|
* @license MIT
|
|
5
|
-
* @version 5.0.
|
|
5
|
+
* @version 5.0.2
|
|
6
6
|
* @author nuintun <nuintun@qq.com>
|
|
7
7
|
* @description A static files serving middleware for koa.
|
|
8
8
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -99,12 +99,12 @@ class ReadStream extends node_stream.Readable {
|
|
|
99
99
|
// Change read state.
|
|
100
100
|
switch (this.#readState) {
|
|
101
101
|
case 0 /* ReadState.PREFIX */:
|
|
102
|
-
this.#readState = 1 /* ReadState.RANGE
|
|
102
|
+
this.#readState = 1; /* ReadState.RANGE */
|
|
103
103
|
this.#readFileRange(fd, range, size - bytesRead);
|
|
104
104
|
break;
|
|
105
105
|
case 2 /* ReadState.SUFFIX */:
|
|
106
106
|
this.#currentRangeIndex++;
|
|
107
|
-
this.#readState = 0 /* ReadState.PREFIX
|
|
107
|
+
this.#readState = 0; /* ReadState.PREFIX */
|
|
108
108
|
const nextRange = this.#getRange();
|
|
109
109
|
if (nextRange == null) {
|
|
110
110
|
this.push(null);
|
|
@@ -147,14 +147,14 @@ class ReadStream extends node_stream.Readable {
|
|
|
147
147
|
// file range read completed.
|
|
148
148
|
if (bytesRead < size) {
|
|
149
149
|
this.#bytesRead = 0;
|
|
150
|
-
this.#readState = 2 /* ReadState.SUFFIX
|
|
150
|
+
this.#readState = 2; /* ReadState.SUFFIX */
|
|
151
151
|
this.#readPadding(fd, range, size - bytesRead);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
155
|
} else {
|
|
156
156
|
this.#bytesRead = 0;
|
|
157
|
-
this.#readState = 2 /* ReadState.SUFFIX
|
|
157
|
+
this.#readState = 2; /* ReadState.SUFFIX */
|
|
158
158
|
this.#readPadding(fd, range, size);
|
|
159
159
|
}
|
|
160
160
|
}
|
package/cjs/Service.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module koa-files
|
|
3
3
|
* @package koa-files
|
|
4
4
|
* @license MIT
|
|
5
|
-
* @version 5.0.
|
|
5
|
+
* @version 5.0.2
|
|
6
6
|
* @author nuintun <nuintun@qq.com>
|
|
7
7
|
* @description A static files serving middleware for koa.
|
|
8
8
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
const createETag = require('etag');
|
|
14
13
|
const fs = require('node:fs');
|
|
15
14
|
const ReadStream = require('./ReadStream.cjs');
|
|
16
15
|
const type = require('./utils/type.cjs');
|
|
@@ -23,7 +22,6 @@ function _interopDefault(e) {
|
|
|
23
22
|
return e && e.__esModule ? e : { default: e };
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
const createETag__default = /*#__PURE__*/ _interopDefault(createETag);
|
|
27
25
|
const fs__default = /*#__PURE__*/ _interopDefault(fs);
|
|
28
26
|
|
|
29
27
|
/**
|
|
@@ -86,7 +84,7 @@ class Service {
|
|
|
86
84
|
response.remove('ETag');
|
|
87
85
|
} else if (!response.get('ETag')) {
|
|
88
86
|
// Set weak ETag.
|
|
89
|
-
response.set('ETag',
|
|
87
|
+
response.set('ETag', http.createETag(stats));
|
|
90
88
|
}
|
|
91
89
|
// Last-Modified.
|
|
92
90
|
if (options.lastModified === false) {
|
|
@@ -165,10 +163,8 @@ class Service {
|
|
|
165
163
|
}
|
|
166
164
|
// Head request.
|
|
167
165
|
if (method === 'HEAD') {
|
|
168
|
-
// Set Content-Length.
|
|
166
|
+
// Set Content-Length, koa keeps it and sends no body for HEAD.
|
|
169
167
|
response.length = stats.size;
|
|
170
|
-
// Set body null
|
|
171
|
-
response.body = null;
|
|
172
168
|
// File found.
|
|
173
169
|
return true;
|
|
174
170
|
}
|
|
@@ -176,14 +172,12 @@ class Service {
|
|
|
176
172
|
const ranges = http.parseRanges(context, stats);
|
|
177
173
|
// 416
|
|
178
174
|
if (ranges === -1) {
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (ranges === -2) {
|
|
186
|
-
return context.throw(400);
|
|
175
|
+
// koa strips all response headers on error, pass Content-Range via err.headers.
|
|
176
|
+
return context.throw(416, {
|
|
177
|
+
headers: {
|
|
178
|
+
'Content-Range': `bytes */${stats.size}`
|
|
179
|
+
}
|
|
180
|
+
});
|
|
187
181
|
}
|
|
188
182
|
// Set response body.
|
|
189
183
|
response.body = new ReadStream.ReadStream(path$1, ranges, {
|
package/cjs/index.cjs
CHANGED
package/cjs/utils/fs.cjs
CHANGED
package/cjs/utils/http.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module koa-files
|
|
3
3
|
* @package koa-files
|
|
4
4
|
* @license MIT
|
|
5
|
-
* @version 5.0.
|
|
5
|
+
* @version 5.0.2
|
|
6
6
|
* @author nuintun <nuintun@qq.com>
|
|
7
7
|
* @description A static files serving middleware for koa.
|
|
8
8
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
const hash = require('./hash.cjs');
|
|
14
13
|
const node_buffer = require('node:buffer');
|
|
15
14
|
const parseRange = require('range-parser');
|
|
15
|
+
const node_crypto = require('node:crypto');
|
|
16
16
|
|
|
17
17
|
function _interopDefault(e) {
|
|
18
18
|
return e && e.__esModule ? e : { default: e };
|
|
@@ -24,6 +24,7 @@ const parseRange__default = /*#__PURE__*/ _interopDefault(parseRange);
|
|
|
24
24
|
* @module http
|
|
25
25
|
*/
|
|
26
26
|
const SPLIT_ETAG_RE = /\s*,\s*/;
|
|
27
|
+
const BOUNDARY_SCRATCH = node_buffer.Buffer.allocUnsafeSlow(24);
|
|
27
28
|
const SINGLE_ETAG_RE = /^(?:W\/)?"[ !#-\x7E\x80-\xFF]+"$/;
|
|
28
29
|
/**
|
|
29
30
|
* @function decodeURI
|
|
@@ -37,6 +38,13 @@ function decodeURI(URI) {
|
|
|
37
38
|
return -1;
|
|
38
39
|
}
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* @function generateBoundary
|
|
43
|
+
* @description Generate a boundary string.
|
|
44
|
+
*/
|
|
45
|
+
function generateBoundary() {
|
|
46
|
+
return node_crypto.getRandomValues(BOUNDARY_SCRATCH).toString('base64url');
|
|
47
|
+
}
|
|
40
48
|
/**
|
|
41
49
|
* @function isConditionalGET
|
|
42
50
|
* @description Check if request is conditional GET.
|
|
@@ -50,6 +58,14 @@ function isConditionalGET({ request }) {
|
|
|
50
58
|
request.get('If-Unmodified-Since')
|
|
51
59
|
);
|
|
52
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* @function createETag
|
|
63
|
+
* @description Generate a weak entity tag from file stats.
|
|
64
|
+
* @param stats The file stats.
|
|
65
|
+
*/
|
|
66
|
+
function createETag(stats) {
|
|
67
|
+
return `W/"${stats.size.toString(16)}-${stats.mtime.getTime().toString(16)}"`;
|
|
68
|
+
}
|
|
53
69
|
/**
|
|
54
70
|
* @function isETagMatch
|
|
55
71
|
* @see https://httpwg.org/specs/rfc9110.html
|
|
@@ -58,25 +74,45 @@ function isConditionalGET({ request }) {
|
|
|
58
74
|
* @param isIfMatch The flag of if-match.
|
|
59
75
|
*/
|
|
60
76
|
function isETagMatch(match, etag, isIfMatch) {
|
|
77
|
+
// Trim match.
|
|
78
|
+
match = match.trim();
|
|
79
|
+
// Check If-Match.
|
|
80
|
+
if (isIfMatch && match === '*') {
|
|
81
|
+
// The condition is true if the selected representation exists.
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
61
84
|
// Trim etag.
|
|
62
85
|
etag = etag.trim();
|
|
63
86
|
// Weak tags cannot be matched and return false.
|
|
64
87
|
if (!etag || etag.startsWith('W/')) {
|
|
65
88
|
return false;
|
|
66
89
|
}
|
|
67
|
-
// Trim match.
|
|
68
|
-
match = match.trim();
|
|
69
90
|
// Check If-Match.
|
|
70
91
|
if (isIfMatch) {
|
|
71
|
-
if (match === '*') {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
92
|
// If-Match maybe a list of etag.
|
|
75
93
|
return match.split(SPLIT_ETAG_RE).includes(etag);
|
|
76
94
|
}
|
|
77
95
|
// Check If-Range.
|
|
78
96
|
return match === etag;
|
|
79
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* @function isRangeFresh
|
|
100
|
+
* @description Check if request range fresh.
|
|
101
|
+
* @param context Koa context.
|
|
102
|
+
*/
|
|
103
|
+
function isRangeFresh({ request, response }) {
|
|
104
|
+
const ifRange = request.get('If-Range');
|
|
105
|
+
// No If-Range.
|
|
106
|
+
if (!ifRange) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
// If-Range as modified date failed.
|
|
110
|
+
if (SINGLE_ETAG_RE.test(ifRange)) {
|
|
111
|
+
return isETagMatch(ifRange, response.get('ETag'));
|
|
112
|
+
}
|
|
113
|
+
// Check if Last-Modified is valid and equal to If-Range date
|
|
114
|
+
return Date.parse(response.get('Last-Modified')) <= Date.parse(ifRange);
|
|
115
|
+
}
|
|
80
116
|
/**
|
|
81
117
|
* @function isPreconditionFailed
|
|
82
118
|
* @description Check if request precondition failed.
|
|
@@ -99,24 +135,6 @@ function isPreconditionFailed({ request, response }) {
|
|
|
99
135
|
// Check precondition passed.
|
|
100
136
|
return false;
|
|
101
137
|
}
|
|
102
|
-
/**
|
|
103
|
-
* @function isRangeFresh
|
|
104
|
-
* @description Check if request range fresh.
|
|
105
|
-
* @param context Koa context.
|
|
106
|
-
*/
|
|
107
|
-
function isRangeFresh({ request, response }) {
|
|
108
|
-
const ifRange = request.get('If-Range');
|
|
109
|
-
// No If-Range.
|
|
110
|
-
if (!ifRange) {
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
// If-Range as modified date failed.
|
|
114
|
-
if (SINGLE_ETAG_RE.test(ifRange)) {
|
|
115
|
-
return isETagMatch(ifRange, response.get('ETag'));
|
|
116
|
-
}
|
|
117
|
-
// Check if Last-Modified is valid and equal to If-Range date
|
|
118
|
-
return Date.parse(response.get('Last-Modified')) <= Date.parse(ifRange);
|
|
119
|
-
}
|
|
120
138
|
/**
|
|
121
139
|
* @function parseRanges
|
|
122
140
|
* @description Parse ranges.
|
|
@@ -134,10 +152,16 @@ function parseRanges(context, stats) {
|
|
|
134
152
|
// Parse range -1 -2 or [].
|
|
135
153
|
const parsed = parseRange__default.default(size, range, { combine: true });
|
|
136
154
|
// -1 signals an unsatisfiable range.
|
|
137
|
-
|
|
138
|
-
if (parsed === -1 || parsed === -2) {
|
|
155
|
+
if (parsed === -1) {
|
|
139
156
|
return parsed;
|
|
140
157
|
}
|
|
158
|
+
// -2 signals a malformed or unknown-unit range, ignore it (RFC 9110 §14.2).
|
|
159
|
+
if (parsed === -2) {
|
|
160
|
+
// Set Content-Length.
|
|
161
|
+
response.length = size;
|
|
162
|
+
// Return ranges.
|
|
163
|
+
return [{ offset: 0, length: size }];
|
|
164
|
+
}
|
|
141
165
|
// Ranges ok, support multiple ranges.
|
|
142
166
|
if (parsed.type === 'bytes') {
|
|
143
167
|
// Set 206 status.
|
|
@@ -152,7 +176,7 @@ function parseRanges(context, stats) {
|
|
|
152
176
|
// Parsed entries.
|
|
153
177
|
const entries = parsed.entries();
|
|
154
178
|
// Range boundary.
|
|
155
|
-
const boundary =
|
|
179
|
+
const boundary = generateBoundary();
|
|
156
180
|
// Multipart Content-Type.
|
|
157
181
|
const contentType = `Content-Type: ${response.type}`;
|
|
158
182
|
// Range suffix.
|
|
@@ -165,9 +189,7 @@ function parseRanges(context, stats) {
|
|
|
165
189
|
const length = end - start + 1;
|
|
166
190
|
const head = index > 0 ? '\r\n' : '';
|
|
167
191
|
const contentRange = `Content-Range: bytes ${start}-${end}/${size}`;
|
|
168
|
-
const prefix = node_buffer.Buffer.from(
|
|
169
|
-
`${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`
|
|
170
|
-
);
|
|
192
|
+
const prefix = node_buffer.Buffer.from(`${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`);
|
|
171
193
|
// Compute Content-Length
|
|
172
194
|
contentLength += prefix.length + length;
|
|
173
195
|
// Cache range.
|
|
@@ -200,7 +222,11 @@ function parseRanges(context, stats) {
|
|
|
200
222
|
return [{ offset: 0, length: size }];
|
|
201
223
|
}
|
|
202
224
|
|
|
225
|
+
exports.createETag = createETag;
|
|
203
226
|
exports.decodeURI = decodeURI;
|
|
227
|
+
exports.generateBoundary = generateBoundary;
|
|
204
228
|
exports.isConditionalGET = isConditionalGET;
|
|
229
|
+
exports.isETagMatch = isETagMatch;
|
|
205
230
|
exports.isPreconditionFailed = isPreconditionFailed;
|
|
231
|
+
exports.isRangeFresh = isRangeFresh;
|
|
206
232
|
exports.parseRanges = parseRanges;
|
package/cjs/utils/http.d.cts
CHANGED
|
@@ -16,12 +16,37 @@ export interface Range {
|
|
|
16
16
|
* @param URI The URI to decode.
|
|
17
17
|
*/
|
|
18
18
|
export declare function decodeURI(URI: string): string | -1;
|
|
19
|
+
/**
|
|
20
|
+
* @function generateBoundary
|
|
21
|
+
* @description Generate a boundary string.
|
|
22
|
+
*/
|
|
23
|
+
export declare function generateBoundary(): string;
|
|
19
24
|
/**
|
|
20
25
|
* @function isConditionalGET
|
|
21
26
|
* @description Check if request is conditional GET.
|
|
22
27
|
* @param context The koa context.
|
|
23
28
|
*/
|
|
24
29
|
export declare function isConditionalGET({ request }: Context): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* @function createETag
|
|
32
|
+
* @description Generate a weak entity tag from file stats.
|
|
33
|
+
* @param stats The file stats.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createETag(stats: Stats): string;
|
|
36
|
+
/**
|
|
37
|
+
* @function isETagMatch
|
|
38
|
+
* @see https://httpwg.org/specs/rfc9110.html
|
|
39
|
+
* @param match The match value.
|
|
40
|
+
* @param etag The etag value.
|
|
41
|
+
* @param isIfMatch The flag of if-match.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isETagMatch(match: string, etag: string, isIfMatch?: boolean): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* @function isRangeFresh
|
|
46
|
+
* @description Check if request range fresh.
|
|
47
|
+
* @param context Koa context.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isRangeFresh({ request, response }: Context): boolean;
|
|
25
50
|
/**
|
|
26
51
|
* @function isPreconditionFailed
|
|
27
52
|
* @description Check if request precondition failed.
|
|
@@ -34,4 +59,4 @@ export declare function isPreconditionFailed({ request, response }: Context): bo
|
|
|
34
59
|
* @param context The koa context.
|
|
35
60
|
* @param stats The file stats.
|
|
36
61
|
*/
|
|
37
|
-
export declare function parseRanges(context: Context, stats: Stats): -1 |
|
|
62
|
+
export declare function parseRanges(context: Context, stats: Stats): -1 | Range[];
|
package/cjs/utils/path.cjs
CHANGED
package/cjs/utils/type.cjs
CHANGED
package/esm/ReadStream.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module koa-files
|
|
3
3
|
* @package koa-files
|
|
4
4
|
* @license MIT
|
|
5
|
-
* @version 5.0.
|
|
5
|
+
* @version 5.0.2
|
|
6
6
|
* @author nuintun <nuintun@qq.com>
|
|
7
7
|
* @description A static files serving middleware for koa.
|
|
8
8
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -97,12 +97,12 @@ class ReadStream extends Readable {
|
|
|
97
97
|
// Change read state.
|
|
98
98
|
switch (this.#readState) {
|
|
99
99
|
case 0 /* ReadState.PREFIX */:
|
|
100
|
-
this.#readState = 1 /* ReadState.RANGE
|
|
100
|
+
this.#readState = 1; /* ReadState.RANGE */
|
|
101
101
|
this.#readFileRange(fd, range, size - bytesRead);
|
|
102
102
|
break;
|
|
103
103
|
case 2 /* ReadState.SUFFIX */:
|
|
104
104
|
this.#currentRangeIndex++;
|
|
105
|
-
this.#readState = 0 /* ReadState.PREFIX
|
|
105
|
+
this.#readState = 0; /* ReadState.PREFIX */
|
|
106
106
|
const nextRange = this.#getRange();
|
|
107
107
|
if (nextRange == null) {
|
|
108
108
|
this.push(null);
|
|
@@ -145,14 +145,14 @@ class ReadStream extends Readable {
|
|
|
145
145
|
// file range read completed.
|
|
146
146
|
if (bytesRead < size) {
|
|
147
147
|
this.#bytesRead = 0;
|
|
148
|
-
this.#readState = 2 /* ReadState.SUFFIX
|
|
148
|
+
this.#readState = 2; /* ReadState.SUFFIX */
|
|
149
149
|
this.#readPadding(fd, range, size - bytesRead);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
});
|
|
153
153
|
} else {
|
|
154
154
|
this.#bytesRead = 0;
|
|
155
|
-
this.#readState = 2 /* ReadState.SUFFIX
|
|
155
|
+
this.#readState = 2; /* ReadState.SUFFIX */
|
|
156
156
|
this.#readPadding(fd, range, size);
|
|
157
157
|
}
|
|
158
158
|
}
|
package/esm/Service.js
CHANGED
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
* @module koa-files
|
|
3
3
|
* @package koa-files
|
|
4
4
|
* @license MIT
|
|
5
|
-
* @version 5.0.
|
|
5
|
+
* @version 5.0.2
|
|
6
6
|
* @author nuintun <nuintun@qq.com>
|
|
7
7
|
* @description A static files serving middleware for koa.
|
|
8
8
|
* @see https://github.com/nuintun/koa-files#readme
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import createETag from 'etag';
|
|
12
11
|
import fs from 'node:fs';
|
|
13
12
|
import { ReadStream } from './ReadStream.js';
|
|
14
13
|
import { isFunction } from './utils/type.js';
|
|
15
14
|
import { stat } from './utils/fs.js';
|
|
16
15
|
import { resolve, extname, join } from 'node:path';
|
|
17
16
|
import { unixify, isOutRoot, hasTrailingSlash } from './utils/path.js';
|
|
18
|
-
import { decodeURI, isConditionalGET, isPreconditionFailed, parseRanges } from './utils/http.js';
|
|
17
|
+
import { createETag, decodeURI, isConditionalGET, isPreconditionFailed, parseRanges } from './utils/http.js';
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* @module Service
|
|
@@ -156,10 +155,8 @@ class Service {
|
|
|
156
155
|
}
|
|
157
156
|
// Head request.
|
|
158
157
|
if (method === 'HEAD') {
|
|
159
|
-
// Set Content-Length.
|
|
158
|
+
// Set Content-Length, koa keeps it and sends no body for HEAD.
|
|
160
159
|
response.length = stats.size;
|
|
161
|
-
// Set body null
|
|
162
|
-
response.body = null;
|
|
163
160
|
// File found.
|
|
164
161
|
return true;
|
|
165
162
|
}
|
|
@@ -167,14 +164,12 @@ class Service {
|
|
|
167
164
|
const ranges = parseRanges(context, stats);
|
|
168
165
|
// 416
|
|
169
166
|
if (ranges === -1) {
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (ranges === -2) {
|
|
177
|
-
return context.throw(400);
|
|
167
|
+
// koa strips all response headers on error, pass Content-Range via err.headers.
|
|
168
|
+
return context.throw(416, {
|
|
169
|
+
headers: {
|
|
170
|
+
'Content-Range': `bytes */${stats.size}`
|
|
171
|
+
}
|
|
172
|
+
});
|
|
178
173
|
}
|
|
179
174
|
// Set response body.
|
|
180
175
|
response.body = new ReadStream(path, ranges, {
|
package/esm/index.js
CHANGED
package/esm/utils/fs.js
CHANGED
package/esm/utils/http.d.ts
CHANGED
|
@@ -16,12 +16,37 @@ export interface Range {
|
|
|
16
16
|
* @param URI The URI to decode.
|
|
17
17
|
*/
|
|
18
18
|
export declare function decodeURI(URI: string): string | -1;
|
|
19
|
+
/**
|
|
20
|
+
* @function generateBoundary
|
|
21
|
+
* @description Generate a boundary string.
|
|
22
|
+
*/
|
|
23
|
+
export declare function generateBoundary(): string;
|
|
19
24
|
/**
|
|
20
25
|
* @function isConditionalGET
|
|
21
26
|
* @description Check if request is conditional GET.
|
|
22
27
|
* @param context The koa context.
|
|
23
28
|
*/
|
|
24
29
|
export declare function isConditionalGET({ request }: Context): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* @function createETag
|
|
32
|
+
* @description Generate a weak entity tag from file stats.
|
|
33
|
+
* @param stats The file stats.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createETag(stats: Stats): string;
|
|
36
|
+
/**
|
|
37
|
+
* @function isETagMatch
|
|
38
|
+
* @see https://httpwg.org/specs/rfc9110.html
|
|
39
|
+
* @param match The match value.
|
|
40
|
+
* @param etag The etag value.
|
|
41
|
+
* @param isIfMatch The flag of if-match.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isETagMatch(match: string, etag: string, isIfMatch?: boolean): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* @function isRangeFresh
|
|
46
|
+
* @description Check if request range fresh.
|
|
47
|
+
* @param context Koa context.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isRangeFresh({ request, response }: Context): boolean;
|
|
25
50
|
/**
|
|
26
51
|
* @function isPreconditionFailed
|
|
27
52
|
* @description Check if request precondition failed.
|
|
@@ -34,4 +59,4 @@ export declare function isPreconditionFailed({ request, response }: Context): bo
|
|
|
34
59
|
* @param context The koa context.
|
|
35
60
|
* @param stats The file stats.
|
|
36
61
|
*/
|
|
37
|
-
export declare function parseRanges(context: Context, stats: Stats): -1 |
|
|
62
|
+
export declare function parseRanges(context: Context, stats: Stats): -1 | Range[];
|
package/esm/utils/http.js
CHANGED
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
* @module koa-files
|
|
3
3
|
* @package koa-files
|
|
4
4
|
* @license MIT
|
|
5
|
-
* @version 5.0.
|
|
5
|
+
* @version 5.0.2
|
|
6
6
|
* @author nuintun <nuintun@qq.com>
|
|
7
7
|
* @description A static files serving middleware for koa.
|
|
8
8
|
* @see https://github.com/nuintun/koa-files#readme
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { generate } from './hash.js';
|
|
12
11
|
import { Buffer } from 'node:buffer';
|
|
13
12
|
import parseRange from 'range-parser';
|
|
13
|
+
import { getRandomValues } from 'node:crypto';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @module http
|
|
17
17
|
*/
|
|
18
18
|
const SPLIT_ETAG_RE = /\s*,\s*/;
|
|
19
|
+
const BOUNDARY_SCRATCH = Buffer.allocUnsafeSlow(24);
|
|
19
20
|
const SINGLE_ETAG_RE = /^(?:W\/)?"[ !#-\x7E\x80-\xFF]+"$/;
|
|
20
21
|
/**
|
|
21
22
|
* @function decodeURI
|
|
@@ -29,6 +30,13 @@ function decodeURI(URI) {
|
|
|
29
30
|
return -1;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* @function generateBoundary
|
|
35
|
+
* @description Generate a boundary string.
|
|
36
|
+
*/
|
|
37
|
+
function generateBoundary() {
|
|
38
|
+
return getRandomValues(BOUNDARY_SCRATCH).toString('base64url');
|
|
39
|
+
}
|
|
32
40
|
/**
|
|
33
41
|
* @function isConditionalGET
|
|
34
42
|
* @description Check if request is conditional GET.
|
|
@@ -42,6 +50,14 @@ function isConditionalGET({ request }) {
|
|
|
42
50
|
request.get('If-Unmodified-Since')
|
|
43
51
|
);
|
|
44
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* @function createETag
|
|
55
|
+
* @description Generate a weak entity tag from file stats.
|
|
56
|
+
* @param stats The file stats.
|
|
57
|
+
*/
|
|
58
|
+
function createETag(stats) {
|
|
59
|
+
return `W/"${stats.size.toString(16)}-${stats.mtime.getTime().toString(16)}"`;
|
|
60
|
+
}
|
|
45
61
|
/**
|
|
46
62
|
* @function isETagMatch
|
|
47
63
|
* @see https://httpwg.org/specs/rfc9110.html
|
|
@@ -50,25 +66,45 @@ function isConditionalGET({ request }) {
|
|
|
50
66
|
* @param isIfMatch The flag of if-match.
|
|
51
67
|
*/
|
|
52
68
|
function isETagMatch(match, etag, isIfMatch) {
|
|
69
|
+
// Trim match.
|
|
70
|
+
match = match.trim();
|
|
71
|
+
// Check If-Match.
|
|
72
|
+
if (isIfMatch && match === '*') {
|
|
73
|
+
// The condition is true if the selected representation exists.
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
53
76
|
// Trim etag.
|
|
54
77
|
etag = etag.trim();
|
|
55
78
|
// Weak tags cannot be matched and return false.
|
|
56
79
|
if (!etag || etag.startsWith('W/')) {
|
|
57
80
|
return false;
|
|
58
81
|
}
|
|
59
|
-
// Trim match.
|
|
60
|
-
match = match.trim();
|
|
61
82
|
// Check If-Match.
|
|
62
83
|
if (isIfMatch) {
|
|
63
|
-
if (match === '*') {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
84
|
// If-Match maybe a list of etag.
|
|
67
85
|
return match.split(SPLIT_ETAG_RE).includes(etag);
|
|
68
86
|
}
|
|
69
87
|
// Check If-Range.
|
|
70
88
|
return match === etag;
|
|
71
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* @function isRangeFresh
|
|
92
|
+
* @description Check if request range fresh.
|
|
93
|
+
* @param context Koa context.
|
|
94
|
+
*/
|
|
95
|
+
function isRangeFresh({ request, response }) {
|
|
96
|
+
const ifRange = request.get('If-Range');
|
|
97
|
+
// No If-Range.
|
|
98
|
+
if (!ifRange) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
// If-Range as modified date failed.
|
|
102
|
+
if (SINGLE_ETAG_RE.test(ifRange)) {
|
|
103
|
+
return isETagMatch(ifRange, response.get('ETag'));
|
|
104
|
+
}
|
|
105
|
+
// Check if Last-Modified is valid and equal to If-Range date
|
|
106
|
+
return Date.parse(response.get('Last-Modified')) <= Date.parse(ifRange);
|
|
107
|
+
}
|
|
72
108
|
/**
|
|
73
109
|
* @function isPreconditionFailed
|
|
74
110
|
* @description Check if request precondition failed.
|
|
@@ -91,24 +127,6 @@ function isPreconditionFailed({ request, response }) {
|
|
|
91
127
|
// Check precondition passed.
|
|
92
128
|
return false;
|
|
93
129
|
}
|
|
94
|
-
/**
|
|
95
|
-
* @function isRangeFresh
|
|
96
|
-
* @description Check if request range fresh.
|
|
97
|
-
* @param context Koa context.
|
|
98
|
-
*/
|
|
99
|
-
function isRangeFresh({ request, response }) {
|
|
100
|
-
const ifRange = request.get('If-Range');
|
|
101
|
-
// No If-Range.
|
|
102
|
-
if (!ifRange) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
// If-Range as modified date failed.
|
|
106
|
-
if (SINGLE_ETAG_RE.test(ifRange)) {
|
|
107
|
-
return isETagMatch(ifRange, response.get('ETag'));
|
|
108
|
-
}
|
|
109
|
-
// Check if Last-Modified is valid and equal to If-Range date
|
|
110
|
-
return Date.parse(response.get('Last-Modified')) <= Date.parse(ifRange);
|
|
111
|
-
}
|
|
112
130
|
/**
|
|
113
131
|
* @function parseRanges
|
|
114
132
|
* @description Parse ranges.
|
|
@@ -126,10 +144,16 @@ function parseRanges(context, stats) {
|
|
|
126
144
|
// Parse range -1 -2 or [].
|
|
127
145
|
const parsed = parseRange(size, range, { combine: true });
|
|
128
146
|
// -1 signals an unsatisfiable range.
|
|
129
|
-
|
|
130
|
-
if (parsed === -1 || parsed === -2) {
|
|
147
|
+
if (parsed === -1) {
|
|
131
148
|
return parsed;
|
|
132
149
|
}
|
|
150
|
+
// -2 signals a malformed or unknown-unit range, ignore it (RFC 9110 §14.2).
|
|
151
|
+
if (parsed === -2) {
|
|
152
|
+
// Set Content-Length.
|
|
153
|
+
response.length = size;
|
|
154
|
+
// Return ranges.
|
|
155
|
+
return [{ offset: 0, length: size }];
|
|
156
|
+
}
|
|
133
157
|
// Ranges ok, support multiple ranges.
|
|
134
158
|
if (parsed.type === 'bytes') {
|
|
135
159
|
// Set 206 status.
|
|
@@ -144,7 +168,7 @@ function parseRanges(context, stats) {
|
|
|
144
168
|
// Parsed entries.
|
|
145
169
|
const entries = parsed.entries();
|
|
146
170
|
// Range boundary.
|
|
147
|
-
const boundary =
|
|
171
|
+
const boundary = generateBoundary();
|
|
148
172
|
// Multipart Content-Type.
|
|
149
173
|
const contentType = `Content-Type: ${response.type}`;
|
|
150
174
|
// Range suffix.
|
|
@@ -157,9 +181,7 @@ function parseRanges(context, stats) {
|
|
|
157
181
|
const length = end - start + 1;
|
|
158
182
|
const head = index > 0 ? '\r\n' : '';
|
|
159
183
|
const contentRange = `Content-Range: bytes ${start}-${end}/${size}`;
|
|
160
|
-
const prefix = Buffer.from(
|
|
161
|
-
`${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`
|
|
162
|
-
);
|
|
184
|
+
const prefix = Buffer.from(`${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`);
|
|
163
185
|
// Compute Content-Length
|
|
164
186
|
contentLength += prefix.length + length;
|
|
165
187
|
// Cache range.
|
|
@@ -192,4 +214,13 @@ function parseRanges(context, stats) {
|
|
|
192
214
|
return [{ offset: 0, length: size }];
|
|
193
215
|
}
|
|
194
216
|
|
|
195
|
-
export {
|
|
217
|
+
export {
|
|
218
|
+
createETag,
|
|
219
|
+
decodeURI,
|
|
220
|
+
generateBoundary,
|
|
221
|
+
isConditionalGET,
|
|
222
|
+
isETagMatch,
|
|
223
|
+
isPreconditionFailed,
|
|
224
|
+
isRangeFresh,
|
|
225
|
+
parseRanges
|
|
226
|
+
};
|
package/esm/utils/path.js
CHANGED
package/esm/utils/type.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-files",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"description": "A static files serving middleware for koa.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -41,31 +41,11 @@
|
|
|
41
41
|
"url": "https://github.com/nuintun/koa-files/issues"
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/nuintun/koa-files#readme",
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"etag": "^1.8.1",
|
|
46
|
-
"range-parser": "^1.2.1",
|
|
47
|
-
"tslib": "^2.8.1"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@rollup/plugin-typescript": "^12.3.0",
|
|
51
|
-
"@types/etag": "^1.8.4",
|
|
52
|
-
"@types/koa": "^3.0.2",
|
|
53
|
-
"@types/node": "^25.5.2",
|
|
54
|
-
"@types/range-parser": "^1.2.7",
|
|
55
|
-
"dts-paths": "^2.2.1",
|
|
56
|
-
"koa": "^3.2.0",
|
|
57
|
-
"prettier": "^3.8.1",
|
|
58
|
-
"rimraf": "^6.1.3",
|
|
59
|
-
"rollup": "^4.60.1",
|
|
60
|
-
"typescript": "^6.0.2"
|
|
61
|
-
},
|
|
62
|
-
"peerDependencies": {
|
|
63
|
-
"koa": ">=2.0.0"
|
|
64
|
-
},
|
|
65
44
|
"scripts": {
|
|
66
45
|
"lint": "tsc --noEmit",
|
|
67
46
|
"pretest": "pnpm build",
|
|
68
47
|
"clean": "node tools/clean.ts",
|
|
48
|
+
"prepublishOnly": "pnpm build",
|
|
69
49
|
"test": "node --test tests/index.ts",
|
|
70
50
|
"build:cjs": "rollup -c tools/rollup.cjs.ts",
|
|
71
51
|
"build:esm": "rollup -c tools/rollup.esm.ts",
|
|
@@ -73,5 +53,25 @@
|
|
|
73
53
|
"build": "pnpm clean && pnpm build:main && pnpm format",
|
|
74
54
|
"format": "prettier --write . --ignore-path .prettierignore",
|
|
75
55
|
"build:main": "pnpm build:cjs && pnpm build:esm && pnpm build:fix-types"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"range-parser": "^1.3.0",
|
|
59
|
+
"tslib": "^2.8.1"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
63
|
+
"@types/etag": "^1.8.4",
|
|
64
|
+
"@types/koa": "^3.0.3",
|
|
65
|
+
"@types/node": "^26.6.5",
|
|
66
|
+
"@types/range-parser": "^1.2.7",
|
|
67
|
+
"dts-paths": "^2.2.2",
|
|
68
|
+
"koa": "^3.2.1",
|
|
69
|
+
"prettier": "^3.9.10",
|
|
70
|
+
"rimraf": "^6.1.3",
|
|
71
|
+
"rollup": "^4.64.12",
|
|
72
|
+
"typescript": "^6.0.3"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"koa": ">=2.0.0"
|
|
76
76
|
}
|
|
77
|
-
}
|
|
77
|
+
}
|
package/cjs/utils/hash.cjs
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module koa-files
|
|
3
|
-
* @package koa-files
|
|
4
|
-
* @license MIT
|
|
5
|
-
* @version 5.0.1
|
|
6
|
-
* @author nuintun <nuintun@qq.com>
|
|
7
|
-
* @description A static files serving middleware for koa.
|
|
8
|
-
* @see https://github.com/nuintun/koa-files#readme
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
'use strict';
|
|
12
|
-
|
|
13
|
-
const node_crypto = require('node:crypto');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @module hash
|
|
17
|
-
*/
|
|
18
|
-
// prettier-ignore
|
|
19
|
-
const CHARS = [
|
|
20
|
-
// 0-9
|
|
21
|
-
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
22
|
-
// A-M
|
|
23
|
-
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
|
24
|
-
// N-Z
|
|
25
|
-
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
|
26
|
-
// a-m
|
|
27
|
-
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
|
28
|
-
// n-z
|
|
29
|
-
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
|
|
30
|
-
];
|
|
31
|
-
/**
|
|
32
|
-
* @function generate
|
|
33
|
-
* @description Generate a hash.
|
|
34
|
-
* @param length The length of hash.
|
|
35
|
-
*/
|
|
36
|
-
function generate(length) {
|
|
37
|
-
let hash = '';
|
|
38
|
-
const randomValues = node_crypto.getRandomValues(new Uint8Array(length));
|
|
39
|
-
// Create hash.
|
|
40
|
-
for (const value of randomValues) {
|
|
41
|
-
hash += CHARS[value % 62];
|
|
42
|
-
}
|
|
43
|
-
// Return hash.
|
|
44
|
-
return hash;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
exports.generate = generate;
|
package/esm/utils/hash.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module koa-files
|
|
3
|
-
* @package koa-files
|
|
4
|
-
* @license MIT
|
|
5
|
-
* @version 5.0.1
|
|
6
|
-
* @author nuintun <nuintun@qq.com>
|
|
7
|
-
* @description A static files serving middleware for koa.
|
|
8
|
-
* @see https://github.com/nuintun/koa-files#readme
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { getRandomValues } from 'node:crypto';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @module hash
|
|
15
|
-
*/
|
|
16
|
-
// prettier-ignore
|
|
17
|
-
const CHARS = [
|
|
18
|
-
// 0-9
|
|
19
|
-
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
20
|
-
// A-M
|
|
21
|
-
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
|
22
|
-
// N-Z
|
|
23
|
-
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
|
24
|
-
// a-m
|
|
25
|
-
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
|
26
|
-
// n-z
|
|
27
|
-
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
|
|
28
|
-
];
|
|
29
|
-
/**
|
|
30
|
-
* @function generate
|
|
31
|
-
* @description Generate a hash.
|
|
32
|
-
* @param length The length of hash.
|
|
33
|
-
*/
|
|
34
|
-
function generate(length) {
|
|
35
|
-
let hash = '';
|
|
36
|
-
const randomValues = getRandomValues(new Uint8Array(length));
|
|
37
|
-
// Create hash.
|
|
38
|
-
for (const value of randomValues) {
|
|
39
|
-
hash += CHARS[value % 62];
|
|
40
|
-
}
|
|
41
|
-
// Return hash.
|
|
42
|
-
return hash;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export { generate };
|