koa-files 3.1.11 → 3.2.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 +3 -3
- package/cjs/{utils/stream.cjs → ReadStream.cjs} +4 -4
- package/cjs/Service.cjs +48 -41
- package/cjs/index.cjs +1 -1
- package/cjs/utils/fs.cjs +1 -1
- package/cjs/utils/hash.cjs +1 -1
- package/cjs/utils/http.cjs +68 -73
- package/cjs/utils/path.cjs +1 -1
- package/esm/{utils/stream.js → ReadStream.js} +4 -4
- package/esm/Service.js +49 -42
- package/esm/index.js +1 -1
- package/esm/utils/fs.js +1 -1
- package/esm/utils/hash.js +1 -1
- package/esm/utils/http.js +68 -73
- package/esm/utils/path.js +1 -1
- package/package.json +1 -1
- package/types/{utils/stream.d.ts → ReadStream.d.ts} +4 -10
- package/types/Service.d.ts +3 -3
- package/types/utils/http.d.ts +12 -8
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ interface Headers {
|
|
|
28
28
|
[key: string]: string | string[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
interface
|
|
32
|
-
(path: string, stats: Stats): Headers | void;
|
|
31
|
+
interface HeadersFunction {
|
|
32
|
+
(path: string, stats: Stats): Promise<Headers | void> | Headers | void;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface FileSystem {
|
|
@@ -47,7 +47,7 @@ export interface Options {
|
|
|
47
47
|
lastModified?: boolean;
|
|
48
48
|
highWaterMark?: number;
|
|
49
49
|
ignore?: IgnoreFunction;
|
|
50
|
-
headers?: Headers |
|
|
50
|
+
headers?: Headers | HeadersFunction;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export default function server(root: string, options?: Options): Middleware;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @package koa-files
|
|
3
3
|
* @license MIT
|
|
4
|
-
* @version 3.
|
|
4
|
+
* @version 3.2.0
|
|
5
5
|
* @author nuintun <nuintun@qq.com>
|
|
6
6
|
* @description A static files serving middleware for koa.
|
|
7
7
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -13,10 +13,10 @@ const buffer = require('buffer');
|
|
|
13
13
|
const stream = require('stream');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* @module
|
|
16
|
+
* @module ReadStream
|
|
17
17
|
*/
|
|
18
18
|
const DISPOSE_EVENT = Symbol('dispose');
|
|
19
|
-
class
|
|
19
|
+
class ReadStream extends stream.Readable {
|
|
20
20
|
/**
|
|
21
21
|
* @constructor
|
|
22
22
|
* @param path The file path.
|
|
@@ -218,4 +218,4 @@ class FileReadStream extends stream.Readable {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
exports.
|
|
221
|
+
exports.ReadStream = ReadStream;
|
package/cjs/Service.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @package koa-files
|
|
3
3
|
* @license MIT
|
|
4
|
-
* @version 3.
|
|
4
|
+
* @version 3.2.0
|
|
5
5
|
* @author nuintun <nuintun@qq.com>
|
|
6
6
|
* @description A static files serving middleware for koa.
|
|
7
7
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
const createETag = require('etag');
|
|
13
13
|
const fs = require('fs');
|
|
14
|
+
const ReadStream = require('./ReadStream.cjs');
|
|
14
15
|
const fs$1 = require('./utils/fs.cjs');
|
|
15
16
|
const path$1 = require('path');
|
|
16
|
-
const stream = require('./utils/stream.cjs');
|
|
17
17
|
const path = require('./utils/path.cjs');
|
|
18
18
|
const http = require('./utils/http.cjs');
|
|
19
19
|
|
|
@@ -67,46 +67,47 @@ class Service {
|
|
|
67
67
|
* @param path The file path.
|
|
68
68
|
* @param stats The file stats.
|
|
69
69
|
*/
|
|
70
|
-
setupHeaders(
|
|
70
|
+
async setupHeaders({ response }, path, stats) {
|
|
71
71
|
const { options } = this;
|
|
72
|
-
const { headers
|
|
72
|
+
const { headers } = options;
|
|
73
73
|
// Set status.
|
|
74
|
-
|
|
74
|
+
response.status = 200;
|
|
75
75
|
// Set Content-Type.
|
|
76
|
-
|
|
76
|
+
response.type = path$1.extname(path);
|
|
77
|
+
// Set headers.
|
|
78
|
+
if (headers) {
|
|
79
|
+
if (isFunction(headers)) {
|
|
80
|
+
const fields = await headers(path, stats);
|
|
81
|
+
if (fields) {
|
|
82
|
+
response.set(fields);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
response.set(headers);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
77
88
|
// Accept-Ranges.
|
|
78
89
|
if (options.acceptRanges === false) {
|
|
79
90
|
// Set Accept-Ranges to none tell client not support.
|
|
80
|
-
|
|
91
|
+
response.set('Accept-Ranges', 'none');
|
|
81
92
|
} else {
|
|
82
93
|
// Set Accept-Ranges.
|
|
83
|
-
|
|
94
|
+
response.set('Accept-Ranges', 'bytes');
|
|
84
95
|
}
|
|
85
96
|
// ETag.
|
|
86
|
-
if (etag === false) {
|
|
97
|
+
if (options.etag === false) {
|
|
87
98
|
// Remove ETag.
|
|
88
|
-
|
|
89
|
-
} else {
|
|
90
|
-
|
|
99
|
+
response.remove('ETag');
|
|
100
|
+
} else if (!response.get('ETag')) {
|
|
101
|
+
// Set weak ETag.
|
|
102
|
+
response.set('ETag', createETag__default.default(stats));
|
|
91
103
|
}
|
|
92
104
|
// Last-Modified.
|
|
93
105
|
if (options.lastModified === false) {
|
|
94
106
|
// Remove Last-Modified.
|
|
95
|
-
|
|
96
|
-
} else {
|
|
97
|
-
// Set
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
// Set headers.
|
|
101
|
-
if (headers) {
|
|
102
|
-
if (isFunction(headers)) {
|
|
103
|
-
const fields = headers(path, stats);
|
|
104
|
-
if (fields) {
|
|
105
|
-
context.set(fields);
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
context.set(headers);
|
|
109
|
-
}
|
|
107
|
+
response.remove('Last-Modified');
|
|
108
|
+
} else if (!response.get('Last-Modified')) {
|
|
109
|
+
// Set last modified from mtime.
|
|
110
|
+
response.set('Last-Modified', stats.mtime.toUTCString());
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
/**
|
|
@@ -116,17 +117,21 @@ class Service {
|
|
|
116
117
|
* @param context The koa context.
|
|
117
118
|
*/
|
|
118
119
|
async respond(context) {
|
|
119
|
-
const {
|
|
120
|
+
const { request } = context;
|
|
121
|
+
const { method } = request;
|
|
120
122
|
// Only support GET and HEAD (405).
|
|
121
|
-
if (
|
|
123
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
122
124
|
return false;
|
|
123
125
|
}
|
|
124
126
|
// Get pathname of file.
|
|
125
|
-
const pathname = http.decodeURI(
|
|
127
|
+
const pathname = http.decodeURI(request.path);
|
|
126
128
|
// Pathname decode failed or includes null byte(s).
|
|
127
129
|
if (pathname === -1 || pathname.includes('\0')) {
|
|
128
130
|
return context.throw(400);
|
|
129
131
|
}
|
|
132
|
+
// Get service root.
|
|
133
|
+
const { root } = this;
|
|
134
|
+
// Get file path.
|
|
130
135
|
const path$2 = path.unixify(path$1.join(root, pathname));
|
|
131
136
|
// Malicious path (403).
|
|
132
137
|
if (path.isOutRoot(path$2, root)) {
|
|
@@ -151,30 +156,32 @@ class Service {
|
|
|
151
156
|
) {
|
|
152
157
|
return false;
|
|
153
158
|
}
|
|
159
|
+
// Koa response.
|
|
160
|
+
const { response } = context;
|
|
154
161
|
// Setup headers.
|
|
155
|
-
this.setupHeaders(context, path$2, stats);
|
|
162
|
+
await this.setupHeaders(context, path$2, stats);
|
|
156
163
|
// Conditional get support.
|
|
157
164
|
if (http.isConditionalGET(context)) {
|
|
158
|
-
// Request precondition
|
|
159
|
-
if (http.
|
|
165
|
+
// Request precondition failed.
|
|
166
|
+
if (http.isPreconditionFailed(context)) {
|
|
160
167
|
return context.throw(412);
|
|
161
168
|
}
|
|
162
169
|
// Request fresh (304).
|
|
163
|
-
if (
|
|
170
|
+
if (request.fresh) {
|
|
164
171
|
// Set status.
|
|
165
|
-
|
|
172
|
+
response.status = 304;
|
|
166
173
|
// Set body null.
|
|
167
|
-
|
|
174
|
+
response.body = null;
|
|
168
175
|
// File found.
|
|
169
176
|
return true;
|
|
170
177
|
}
|
|
171
178
|
}
|
|
172
179
|
// Head request.
|
|
173
|
-
if (
|
|
180
|
+
if (method === 'HEAD') {
|
|
174
181
|
// Set Content-Length.
|
|
175
|
-
|
|
182
|
+
response.length = stats.size;
|
|
176
183
|
// Set body null
|
|
177
|
-
|
|
184
|
+
response.body = null;
|
|
178
185
|
// File found.
|
|
179
186
|
return true;
|
|
180
187
|
}
|
|
@@ -183,7 +190,7 @@ class Service {
|
|
|
183
190
|
// 416
|
|
184
191
|
if (ranges === -1) {
|
|
185
192
|
// Set Content-Range.
|
|
186
|
-
|
|
193
|
+
response.set('Content-Range', `bytes */${stats.size}`);
|
|
187
194
|
// Unsatisfiable 416.
|
|
188
195
|
return context.throw(416);
|
|
189
196
|
}
|
|
@@ -192,7 +199,7 @@ class Service {
|
|
|
192
199
|
return context.throw(400);
|
|
193
200
|
}
|
|
194
201
|
// Set response body.
|
|
195
|
-
|
|
202
|
+
response.body = new ReadStream.ReadStream(path$2, ranges, options);
|
|
196
203
|
// File found.
|
|
197
204
|
return true;
|
|
198
205
|
}
|
package/cjs/index.cjs
CHANGED
package/cjs/utils/fs.cjs
CHANGED
package/cjs/utils/hash.cjs
CHANGED
package/cjs/utils/http.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @package koa-files
|
|
3
3
|
* @license MIT
|
|
4
|
-
* @version 3.
|
|
4
|
+
* @version 3.2.0
|
|
5
5
|
* @author nuintun <nuintun@qq.com>
|
|
6
6
|
* @description A static files serving middleware for koa.
|
|
7
7
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -22,23 +22,8 @@ const parseRange__default = /*#__PURE__*/ _interopDefault(parseRange);
|
|
|
22
22
|
/**
|
|
23
23
|
* @module http
|
|
24
24
|
*/
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
* @function isETag
|
|
28
|
-
* @description Check if etag is valid.
|
|
29
|
-
* @param value The value to check.
|
|
30
|
-
*/
|
|
31
|
-
function isETag(value) {
|
|
32
|
-
return /^(?:W\/)?"[\s\S]+"$/.test(value);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* @function parseTokens
|
|
36
|
-
* @description Parse HTTP tokens.
|
|
37
|
-
* @param value The tokens value string.
|
|
38
|
-
*/
|
|
39
|
-
function parseTokens(value) {
|
|
40
|
-
return value.trim().split(TOKEN_SPLIT_REGEX);
|
|
41
|
-
}
|
|
25
|
+
const SPLIT_ETAG_RE = /\s*,\s*/;
|
|
26
|
+
const SINGLE_ETAG_RE = /^(?:W\/)?"[ !#-\x7E\x80-\xFF]+"$/;
|
|
42
27
|
/**
|
|
43
28
|
* @function decodeURI
|
|
44
29
|
* @description Decode URI component.
|
|
@@ -51,45 +36,12 @@ function decodeURI(URI) {
|
|
|
51
36
|
return -1;
|
|
52
37
|
}
|
|
53
38
|
}
|
|
54
|
-
/**
|
|
55
|
-
* @function isRangeFresh
|
|
56
|
-
* @description Check if request range fresh.
|
|
57
|
-
* @param context Koa context.
|
|
58
|
-
*/
|
|
59
|
-
function isRangeFresh(context) {
|
|
60
|
-
const { request, response } = context;
|
|
61
|
-
const ifRange = request.get('If-Range');
|
|
62
|
-
// No If-Range.
|
|
63
|
-
if (!ifRange) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
// If-Range as etag.
|
|
67
|
-
if (isETag(ifRange)) {
|
|
68
|
-
const etag = response.get('ETag');
|
|
69
|
-
return !!(etag && isETagFresh(ifRange, etag));
|
|
70
|
-
}
|
|
71
|
-
// If-Range as modified date.
|
|
72
|
-
const lastModified = response.get('Last-Modified');
|
|
73
|
-
return Date.parse(lastModified) <= Date.parse(ifRange);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* @function isETagFresh
|
|
77
|
-
* @description Check if etag is fresh.
|
|
78
|
-
* @param match The match value.
|
|
79
|
-
* @param etag The etag value.
|
|
80
|
-
*/
|
|
81
|
-
function isETagFresh(match, etag) {
|
|
82
|
-
return parseTokens(match).some(match => {
|
|
83
|
-
return match === etag || match === 'W/' + etag || 'W/' + match === etag;
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
39
|
/**
|
|
87
40
|
* @function isConditionalGET
|
|
88
41
|
* @description Check if request is conditional GET.
|
|
89
42
|
* @param context The koa context.
|
|
90
43
|
*/
|
|
91
|
-
function isConditionalGET(
|
|
92
|
-
const { request } = context;
|
|
44
|
+
function isConditionalGET({ request }) {
|
|
93
45
|
return !!(
|
|
94
46
|
request.get('If-Match') ||
|
|
95
47
|
request.get('If-None-Match') ||
|
|
@@ -98,30 +50,72 @@ function isConditionalGET(context) {
|
|
|
98
50
|
);
|
|
99
51
|
}
|
|
100
52
|
/**
|
|
101
|
-
* @function
|
|
102
|
-
* @
|
|
53
|
+
* @function isETagMatch
|
|
54
|
+
* @see https://httpwg.org/specs/rfc9110.html
|
|
55
|
+
* @param match The match value.
|
|
56
|
+
* @param etag The etag value.
|
|
57
|
+
* @param isIfMatch The flag of if-match.
|
|
58
|
+
*/
|
|
59
|
+
function isETagMatch(match, etag, isIfMatch) {
|
|
60
|
+
// Trim etag.
|
|
61
|
+
etag = etag.trim();
|
|
62
|
+
// Weak tags cannot be matched and return false.
|
|
63
|
+
if (!etag || etag.startsWith('W/')) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
// Trim match.
|
|
67
|
+
match = match.trim();
|
|
68
|
+
// Check If-Match.
|
|
69
|
+
if (isIfMatch) {
|
|
70
|
+
if (match === '*') {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// If-Match maybe a list of etag.
|
|
74
|
+
return match.split(SPLIT_ETAG_RE).includes(etag);
|
|
75
|
+
}
|
|
76
|
+
// Check If-Range.
|
|
77
|
+
return match === etag;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* @function isPreconditionFailed
|
|
81
|
+
* @description Check if request precondition failed.
|
|
103
82
|
* @param context The koa context.
|
|
104
83
|
*/
|
|
105
|
-
function
|
|
84
|
+
function isPreconditionFailed({ request, response }) {
|
|
106
85
|
// If-Match.
|
|
107
|
-
const
|
|
108
|
-
// Check
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
const etag = response.get('ETag');
|
|
112
|
-
return !etag || match === '*' || !isETagFresh(match, etag);
|
|
86
|
+
const ifMatch = request.get('If-Match');
|
|
87
|
+
// Check If-Match.
|
|
88
|
+
if (ifMatch) {
|
|
89
|
+
return !isETagMatch(ifMatch, response.get('ETag'), true);
|
|
113
90
|
}
|
|
114
91
|
// If-Unmodified-Since.
|
|
115
92
|
const unmodifiedSince = Date.parse(request.get('If-Unmodified-Since'));
|
|
116
|
-
// Check
|
|
93
|
+
// Check If-Unmodified-Since.
|
|
117
94
|
if (!Number.isNaN(unmodifiedSince)) {
|
|
118
|
-
// Last-Modified.
|
|
119
95
|
const lastModified = Date.parse(response.get('Last-Modified'));
|
|
120
96
|
return Number.isNaN(lastModified) || lastModified > unmodifiedSince;
|
|
121
97
|
}
|
|
122
98
|
// Check precondition passed.
|
|
123
99
|
return false;
|
|
124
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* @function isRangeFresh
|
|
103
|
+
* @description Check if request range fresh.
|
|
104
|
+
* @param context Koa context.
|
|
105
|
+
*/
|
|
106
|
+
function isRangeFresh({ request, response }) {
|
|
107
|
+
const ifRange = request.get('If-Range');
|
|
108
|
+
// No If-Range.
|
|
109
|
+
if (!ifRange) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// If-Range as modified date failed.
|
|
113
|
+
if (SINGLE_ETAG_RE.test(ifRange)) {
|
|
114
|
+
return isETagMatch(ifRange, response.get('ETag'));
|
|
115
|
+
}
|
|
116
|
+
// Check if Last-Modified is valid and equal to If-Range date
|
|
117
|
+
return Date.parse(response.get('Last-Modified')) <= Date.parse(ifRange);
|
|
118
|
+
}
|
|
125
119
|
/**
|
|
126
120
|
* @function parseRanges
|
|
127
121
|
* @description Parse ranges.
|
|
@@ -130,9 +124,10 @@ function isPreconditionFailure({ request, response }) {
|
|
|
130
124
|
*/
|
|
131
125
|
function parseRanges(context, stats) {
|
|
132
126
|
const { size } = stats;
|
|
127
|
+
const { request, response } = context;
|
|
133
128
|
// Range support.
|
|
134
|
-
if (/^bytes$/i.test(
|
|
135
|
-
const range =
|
|
129
|
+
if (/^bytes$/i.test(response.get('Accept-Ranges'))) {
|
|
130
|
+
const range = request.get('Range');
|
|
136
131
|
// Range fresh.
|
|
137
132
|
if (range && isRangeFresh(context)) {
|
|
138
133
|
// Parse range -1 -2 or [].
|
|
@@ -145,7 +140,7 @@ function parseRanges(context, stats) {
|
|
|
145
140
|
// Ranges ok, support multiple ranges.
|
|
146
141
|
if (parsed.type === 'bytes') {
|
|
147
142
|
// Set 206 status.
|
|
148
|
-
|
|
143
|
+
response.status = 206;
|
|
149
144
|
const { length } = parsed;
|
|
150
145
|
// Multiple ranges.
|
|
151
146
|
if (length > 1) {
|
|
@@ -158,11 +153,11 @@ function parseRanges(context, stats) {
|
|
|
158
153
|
// Range boundary.
|
|
159
154
|
const boundary = `${hash.generate(32)}`;
|
|
160
155
|
// Multipart Content-Type.
|
|
161
|
-
const contentType = `Content-Type: ${
|
|
156
|
+
const contentType = `Content-Type: ${response.type}`;
|
|
162
157
|
// Range suffix.
|
|
163
158
|
const suffix = buffer.Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
164
159
|
// Override Content-Type.
|
|
165
|
-
|
|
160
|
+
response.type = `multipart/byteranges; boundary="${boundary}"`;
|
|
166
161
|
// Map ranges.
|
|
167
162
|
for (const [index, { start, end }] of entries) {
|
|
168
163
|
const length = end - start + 1;
|
|
@@ -177,7 +172,7 @@ function parseRanges(context, stats) {
|
|
|
177
172
|
// Compute Content-Length.
|
|
178
173
|
contentLength += suffix.length;
|
|
179
174
|
// Set Content-Length.
|
|
180
|
-
|
|
175
|
+
response.length = contentLength;
|
|
181
176
|
// The last add suffix boundary.
|
|
182
177
|
ranges[length - 1].suffix = suffix;
|
|
183
178
|
// Return ranges.
|
|
@@ -186,9 +181,9 @@ function parseRanges(context, stats) {
|
|
|
186
181
|
const [{ start, end }] = parsed;
|
|
187
182
|
const length = end - start + 1;
|
|
188
183
|
// Set Content-Length.
|
|
189
|
-
|
|
184
|
+
response.length = length;
|
|
190
185
|
// Set Content-Range.
|
|
191
|
-
|
|
186
|
+
response.set('Content-Range', `bytes ${start}-${end}/${size}`);
|
|
192
187
|
// Return ranges.
|
|
193
188
|
return [{ offset: start, length }];
|
|
194
189
|
}
|
|
@@ -196,12 +191,12 @@ function parseRanges(context, stats) {
|
|
|
196
191
|
}
|
|
197
192
|
}
|
|
198
193
|
// Set Content-Length.
|
|
199
|
-
|
|
194
|
+
response.length = size;
|
|
200
195
|
// Return ranges.
|
|
201
196
|
return [{ offset: 0, length: size }];
|
|
202
197
|
}
|
|
203
198
|
|
|
204
199
|
exports.decodeURI = decodeURI;
|
|
205
200
|
exports.isConditionalGET = isConditionalGET;
|
|
206
|
-
exports.
|
|
201
|
+
exports.isPreconditionFailed = isPreconditionFailed;
|
|
207
202
|
exports.parseRanges = parseRanges;
|
package/cjs/utils/path.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @package koa-files
|
|
3
3
|
* @license MIT
|
|
4
|
-
* @version 3.
|
|
4
|
+
* @version 3.2.0
|
|
5
5
|
* @author nuintun <nuintun@qq.com>
|
|
6
6
|
* @description A static files serving middleware for koa.
|
|
7
7
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -11,10 +11,10 @@ import { Buffer } from 'buffer';
|
|
|
11
11
|
import { Readable } from 'stream';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* @module
|
|
14
|
+
* @module ReadStream
|
|
15
15
|
*/
|
|
16
16
|
const DISPOSE_EVENT = Symbol('dispose');
|
|
17
|
-
class
|
|
17
|
+
class ReadStream extends Readable {
|
|
18
18
|
/**
|
|
19
19
|
* @constructor
|
|
20
20
|
* @param path The file path.
|
|
@@ -216,4 +216,4 @@ class FileReadStream extends Readable {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
export {
|
|
219
|
+
export { ReadStream };
|
package/esm/Service.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @package koa-files
|
|
3
3
|
* @license MIT
|
|
4
|
-
* @version 3.
|
|
4
|
+
* @version 3.2.0
|
|
5
5
|
* @author nuintun <nuintun@qq.com>
|
|
6
6
|
* @description A static files serving middleware for koa.
|
|
7
7
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import createETag from 'etag';
|
|
11
11
|
import fs from 'fs';
|
|
12
|
+
import { ReadStream } from './ReadStream.js';
|
|
12
13
|
import { stat } from './utils/fs.js';
|
|
13
14
|
import { resolve, extname, join } from 'path';
|
|
14
|
-
import { FileReadStream } from './utils/stream.js';
|
|
15
15
|
import { unixify, isOutRoot, hasTrailingSlash } from './utils/path.js';
|
|
16
|
-
import { decodeURI, isConditionalGET,
|
|
16
|
+
import { decodeURI, isConditionalGET, isPreconditionFailed, parseRanges } from './utils/http.js';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @module Service
|
|
@@ -58,46 +58,47 @@ class Service {
|
|
|
58
58
|
* @param path The file path.
|
|
59
59
|
* @param stats The file stats.
|
|
60
60
|
*/
|
|
61
|
-
setupHeaders(
|
|
61
|
+
async setupHeaders({ response }, path, stats) {
|
|
62
62
|
const { options } = this;
|
|
63
|
-
const { headers
|
|
63
|
+
const { headers } = options;
|
|
64
64
|
// Set status.
|
|
65
|
-
|
|
65
|
+
response.status = 200;
|
|
66
66
|
// Set Content-Type.
|
|
67
|
-
|
|
67
|
+
response.type = extname(path);
|
|
68
|
+
// Set headers.
|
|
69
|
+
if (headers) {
|
|
70
|
+
if (isFunction(headers)) {
|
|
71
|
+
const fields = await headers(path, stats);
|
|
72
|
+
if (fields) {
|
|
73
|
+
response.set(fields);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
response.set(headers);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
68
79
|
// Accept-Ranges.
|
|
69
80
|
if (options.acceptRanges === false) {
|
|
70
81
|
// Set Accept-Ranges to none tell client not support.
|
|
71
|
-
|
|
82
|
+
response.set('Accept-Ranges', 'none');
|
|
72
83
|
} else {
|
|
73
84
|
// Set Accept-Ranges.
|
|
74
|
-
|
|
85
|
+
response.set('Accept-Ranges', 'bytes');
|
|
75
86
|
}
|
|
76
87
|
// ETag.
|
|
77
|
-
if (etag === false) {
|
|
88
|
+
if (options.etag === false) {
|
|
78
89
|
// Remove ETag.
|
|
79
|
-
|
|
80
|
-
} else {
|
|
81
|
-
|
|
90
|
+
response.remove('ETag');
|
|
91
|
+
} else if (!response.get('ETag')) {
|
|
92
|
+
// Set weak ETag.
|
|
93
|
+
response.set('ETag', createETag(stats));
|
|
82
94
|
}
|
|
83
95
|
// Last-Modified.
|
|
84
96
|
if (options.lastModified === false) {
|
|
85
97
|
// Remove Last-Modified.
|
|
86
|
-
|
|
87
|
-
} else {
|
|
88
|
-
// Set
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
// Set headers.
|
|
92
|
-
if (headers) {
|
|
93
|
-
if (isFunction(headers)) {
|
|
94
|
-
const fields = headers(path, stats);
|
|
95
|
-
if (fields) {
|
|
96
|
-
context.set(fields);
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
context.set(headers);
|
|
100
|
-
}
|
|
98
|
+
response.remove('Last-Modified');
|
|
99
|
+
} else if (!response.get('Last-Modified')) {
|
|
100
|
+
// Set last modified from mtime.
|
|
101
|
+
response.set('Last-Modified', stats.mtime.toUTCString());
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
@@ -107,17 +108,21 @@ class Service {
|
|
|
107
108
|
* @param context The koa context.
|
|
108
109
|
*/
|
|
109
110
|
async respond(context) {
|
|
110
|
-
const {
|
|
111
|
+
const { request } = context;
|
|
112
|
+
const { method } = request;
|
|
111
113
|
// Only support GET and HEAD (405).
|
|
112
|
-
if (
|
|
114
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
113
115
|
return false;
|
|
114
116
|
}
|
|
115
117
|
// Get pathname of file.
|
|
116
|
-
const pathname = decodeURI(
|
|
118
|
+
const pathname = decodeURI(request.path);
|
|
117
119
|
// Pathname decode failed or includes null byte(s).
|
|
118
120
|
if (pathname === -1 || pathname.includes('\0')) {
|
|
119
121
|
return context.throw(400);
|
|
120
122
|
}
|
|
123
|
+
// Get service root.
|
|
124
|
+
const { root } = this;
|
|
125
|
+
// Get file path.
|
|
121
126
|
const path = unixify(join(root, pathname));
|
|
122
127
|
// Malicious path (403).
|
|
123
128
|
if (isOutRoot(path, root)) {
|
|
@@ -142,30 +147,32 @@ class Service {
|
|
|
142
147
|
) {
|
|
143
148
|
return false;
|
|
144
149
|
}
|
|
150
|
+
// Koa response.
|
|
151
|
+
const { response } = context;
|
|
145
152
|
// Setup headers.
|
|
146
|
-
this.setupHeaders(context, path, stats);
|
|
153
|
+
await this.setupHeaders(context, path, stats);
|
|
147
154
|
// Conditional get support.
|
|
148
155
|
if (isConditionalGET(context)) {
|
|
149
|
-
// Request precondition
|
|
150
|
-
if (
|
|
156
|
+
// Request precondition failed.
|
|
157
|
+
if (isPreconditionFailed(context)) {
|
|
151
158
|
return context.throw(412);
|
|
152
159
|
}
|
|
153
160
|
// Request fresh (304).
|
|
154
|
-
if (
|
|
161
|
+
if (request.fresh) {
|
|
155
162
|
// Set status.
|
|
156
|
-
|
|
163
|
+
response.status = 304;
|
|
157
164
|
// Set body null.
|
|
158
|
-
|
|
165
|
+
response.body = null;
|
|
159
166
|
// File found.
|
|
160
167
|
return true;
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
170
|
// Head request.
|
|
164
|
-
if (
|
|
171
|
+
if (method === 'HEAD') {
|
|
165
172
|
// Set Content-Length.
|
|
166
|
-
|
|
173
|
+
response.length = stats.size;
|
|
167
174
|
// Set body null
|
|
168
|
-
|
|
175
|
+
response.body = null;
|
|
169
176
|
// File found.
|
|
170
177
|
return true;
|
|
171
178
|
}
|
|
@@ -174,7 +181,7 @@ class Service {
|
|
|
174
181
|
// 416
|
|
175
182
|
if (ranges === -1) {
|
|
176
183
|
// Set Content-Range.
|
|
177
|
-
|
|
184
|
+
response.set('Content-Range', `bytes */${stats.size}`);
|
|
178
185
|
// Unsatisfiable 416.
|
|
179
186
|
return context.throw(416);
|
|
180
187
|
}
|
|
@@ -183,7 +190,7 @@ class Service {
|
|
|
183
190
|
return context.throw(400);
|
|
184
191
|
}
|
|
185
192
|
// Set response body.
|
|
186
|
-
|
|
193
|
+
response.body = new ReadStream(path, ranges, options);
|
|
187
194
|
// File found.
|
|
188
195
|
return true;
|
|
189
196
|
}
|
package/esm/index.js
CHANGED
package/esm/utils/fs.js
CHANGED
package/esm/utils/hash.js
CHANGED
package/esm/utils/http.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @package koa-files
|
|
3
3
|
* @license MIT
|
|
4
|
-
* @version 3.
|
|
4
|
+
* @version 3.2.0
|
|
5
5
|
* @author nuintun <nuintun@qq.com>
|
|
6
6
|
* @description A static files serving middleware for koa.
|
|
7
7
|
* @see https://github.com/nuintun/koa-files#readme
|
|
@@ -14,23 +14,8 @@ import parseRange from 'range-parser';
|
|
|
14
14
|
/**
|
|
15
15
|
* @module http
|
|
16
16
|
*/
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
* @function isETag
|
|
20
|
-
* @description Check if etag is valid.
|
|
21
|
-
* @param value The value to check.
|
|
22
|
-
*/
|
|
23
|
-
function isETag(value) {
|
|
24
|
-
return /^(?:W\/)?"[\s\S]+"$/.test(value);
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* @function parseTokens
|
|
28
|
-
* @description Parse HTTP tokens.
|
|
29
|
-
* @param value The tokens value string.
|
|
30
|
-
*/
|
|
31
|
-
function parseTokens(value) {
|
|
32
|
-
return value.trim().split(TOKEN_SPLIT_REGEX);
|
|
33
|
-
}
|
|
17
|
+
const SPLIT_ETAG_RE = /\s*,\s*/;
|
|
18
|
+
const SINGLE_ETAG_RE = /^(?:W\/)?"[ !#-\x7E\x80-\xFF]+"$/;
|
|
34
19
|
/**
|
|
35
20
|
* @function decodeURI
|
|
36
21
|
* @description Decode URI component.
|
|
@@ -43,45 +28,12 @@ function decodeURI(URI) {
|
|
|
43
28
|
return -1;
|
|
44
29
|
}
|
|
45
30
|
}
|
|
46
|
-
/**
|
|
47
|
-
* @function isRangeFresh
|
|
48
|
-
* @description Check if request range fresh.
|
|
49
|
-
* @param context Koa context.
|
|
50
|
-
*/
|
|
51
|
-
function isRangeFresh(context) {
|
|
52
|
-
const { request, response } = context;
|
|
53
|
-
const ifRange = request.get('If-Range');
|
|
54
|
-
// No If-Range.
|
|
55
|
-
if (!ifRange) {
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
// If-Range as etag.
|
|
59
|
-
if (isETag(ifRange)) {
|
|
60
|
-
const etag = response.get('ETag');
|
|
61
|
-
return !!(etag && isETagFresh(ifRange, etag));
|
|
62
|
-
}
|
|
63
|
-
// If-Range as modified date.
|
|
64
|
-
const lastModified = response.get('Last-Modified');
|
|
65
|
-
return Date.parse(lastModified) <= Date.parse(ifRange);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* @function isETagFresh
|
|
69
|
-
* @description Check if etag is fresh.
|
|
70
|
-
* @param match The match value.
|
|
71
|
-
* @param etag The etag value.
|
|
72
|
-
*/
|
|
73
|
-
function isETagFresh(match, etag) {
|
|
74
|
-
return parseTokens(match).some(match => {
|
|
75
|
-
return match === etag || match === 'W/' + etag || 'W/' + match === etag;
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
31
|
/**
|
|
79
32
|
* @function isConditionalGET
|
|
80
33
|
* @description Check if request is conditional GET.
|
|
81
34
|
* @param context The koa context.
|
|
82
35
|
*/
|
|
83
|
-
function isConditionalGET(
|
|
84
|
-
const { request } = context;
|
|
36
|
+
function isConditionalGET({ request }) {
|
|
85
37
|
return !!(
|
|
86
38
|
request.get('If-Match') ||
|
|
87
39
|
request.get('If-None-Match') ||
|
|
@@ -90,30 +42,72 @@ function isConditionalGET(context) {
|
|
|
90
42
|
);
|
|
91
43
|
}
|
|
92
44
|
/**
|
|
93
|
-
* @function
|
|
94
|
-
* @
|
|
45
|
+
* @function isETagMatch
|
|
46
|
+
* @see https://httpwg.org/specs/rfc9110.html
|
|
47
|
+
* @param match The match value.
|
|
48
|
+
* @param etag The etag value.
|
|
49
|
+
* @param isIfMatch The flag of if-match.
|
|
50
|
+
*/
|
|
51
|
+
function isETagMatch(match, etag, isIfMatch) {
|
|
52
|
+
// Trim etag.
|
|
53
|
+
etag = etag.trim();
|
|
54
|
+
// Weak tags cannot be matched and return false.
|
|
55
|
+
if (!etag || etag.startsWith('W/')) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// Trim match.
|
|
59
|
+
match = match.trim();
|
|
60
|
+
// Check If-Match.
|
|
61
|
+
if (isIfMatch) {
|
|
62
|
+
if (match === '*') {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// If-Match maybe a list of etag.
|
|
66
|
+
return match.split(SPLIT_ETAG_RE).includes(etag);
|
|
67
|
+
}
|
|
68
|
+
// Check If-Range.
|
|
69
|
+
return match === etag;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* @function isPreconditionFailed
|
|
73
|
+
* @description Check if request precondition failed.
|
|
95
74
|
* @param context The koa context.
|
|
96
75
|
*/
|
|
97
|
-
function
|
|
76
|
+
function isPreconditionFailed({ request, response }) {
|
|
98
77
|
// If-Match.
|
|
99
|
-
const
|
|
100
|
-
// Check
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
const etag = response.get('ETag');
|
|
104
|
-
return !etag || match === '*' || !isETagFresh(match, etag);
|
|
78
|
+
const ifMatch = request.get('If-Match');
|
|
79
|
+
// Check If-Match.
|
|
80
|
+
if (ifMatch) {
|
|
81
|
+
return !isETagMatch(ifMatch, response.get('ETag'), true);
|
|
105
82
|
}
|
|
106
83
|
// If-Unmodified-Since.
|
|
107
84
|
const unmodifiedSince = Date.parse(request.get('If-Unmodified-Since'));
|
|
108
|
-
// Check
|
|
85
|
+
// Check If-Unmodified-Since.
|
|
109
86
|
if (!Number.isNaN(unmodifiedSince)) {
|
|
110
|
-
// Last-Modified.
|
|
111
87
|
const lastModified = Date.parse(response.get('Last-Modified'));
|
|
112
88
|
return Number.isNaN(lastModified) || lastModified > unmodifiedSince;
|
|
113
89
|
}
|
|
114
90
|
// Check precondition passed.
|
|
115
91
|
return false;
|
|
116
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* @function isRangeFresh
|
|
95
|
+
* @description Check if request range fresh.
|
|
96
|
+
* @param context Koa context.
|
|
97
|
+
*/
|
|
98
|
+
function isRangeFresh({ request, response }) {
|
|
99
|
+
const ifRange = request.get('If-Range');
|
|
100
|
+
// No If-Range.
|
|
101
|
+
if (!ifRange) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// If-Range as modified date failed.
|
|
105
|
+
if (SINGLE_ETAG_RE.test(ifRange)) {
|
|
106
|
+
return isETagMatch(ifRange, response.get('ETag'));
|
|
107
|
+
}
|
|
108
|
+
// Check if Last-Modified is valid and equal to If-Range date
|
|
109
|
+
return Date.parse(response.get('Last-Modified')) <= Date.parse(ifRange);
|
|
110
|
+
}
|
|
117
111
|
/**
|
|
118
112
|
* @function parseRanges
|
|
119
113
|
* @description Parse ranges.
|
|
@@ -122,9 +116,10 @@ function isPreconditionFailure({ request, response }) {
|
|
|
122
116
|
*/
|
|
123
117
|
function parseRanges(context, stats) {
|
|
124
118
|
const { size } = stats;
|
|
119
|
+
const { request, response } = context;
|
|
125
120
|
// Range support.
|
|
126
|
-
if (/^bytes$/i.test(
|
|
127
|
-
const range =
|
|
121
|
+
if (/^bytes$/i.test(response.get('Accept-Ranges'))) {
|
|
122
|
+
const range = request.get('Range');
|
|
128
123
|
// Range fresh.
|
|
129
124
|
if (range && isRangeFresh(context)) {
|
|
130
125
|
// Parse range -1 -2 or [].
|
|
@@ -137,7 +132,7 @@ function parseRanges(context, stats) {
|
|
|
137
132
|
// Ranges ok, support multiple ranges.
|
|
138
133
|
if (parsed.type === 'bytes') {
|
|
139
134
|
// Set 206 status.
|
|
140
|
-
|
|
135
|
+
response.status = 206;
|
|
141
136
|
const { length } = parsed;
|
|
142
137
|
// Multiple ranges.
|
|
143
138
|
if (length > 1) {
|
|
@@ -150,11 +145,11 @@ function parseRanges(context, stats) {
|
|
|
150
145
|
// Range boundary.
|
|
151
146
|
const boundary = `${generate(32)}`;
|
|
152
147
|
// Multipart Content-Type.
|
|
153
|
-
const contentType = `Content-Type: ${
|
|
148
|
+
const contentType = `Content-Type: ${response.type}`;
|
|
154
149
|
// Range suffix.
|
|
155
150
|
const suffix = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
156
151
|
// Override Content-Type.
|
|
157
|
-
|
|
152
|
+
response.type = `multipart/byteranges; boundary="${boundary}"`;
|
|
158
153
|
// Map ranges.
|
|
159
154
|
for (const [index, { start, end }] of entries) {
|
|
160
155
|
const length = end - start + 1;
|
|
@@ -169,7 +164,7 @@ function parseRanges(context, stats) {
|
|
|
169
164
|
// Compute Content-Length.
|
|
170
165
|
contentLength += suffix.length;
|
|
171
166
|
// Set Content-Length.
|
|
172
|
-
|
|
167
|
+
response.length = contentLength;
|
|
173
168
|
// The last add suffix boundary.
|
|
174
169
|
ranges[length - 1].suffix = suffix;
|
|
175
170
|
// Return ranges.
|
|
@@ -178,9 +173,9 @@ function parseRanges(context, stats) {
|
|
|
178
173
|
const [{ start, end }] = parsed;
|
|
179
174
|
const length = end - start + 1;
|
|
180
175
|
// Set Content-Length.
|
|
181
|
-
|
|
176
|
+
response.length = length;
|
|
182
177
|
// Set Content-Range.
|
|
183
|
-
|
|
178
|
+
response.set('Content-Range', `bytes ${start}-${end}/${size}`);
|
|
184
179
|
// Return ranges.
|
|
185
180
|
return [{ offset: start, length }];
|
|
186
181
|
}
|
|
@@ -188,9 +183,9 @@ function parseRanges(context, stats) {
|
|
|
188
183
|
}
|
|
189
184
|
}
|
|
190
185
|
// Set Content-Length.
|
|
191
|
-
|
|
186
|
+
response.length = size;
|
|
192
187
|
// Return ranges.
|
|
193
188
|
return [{ offset: 0, length: size }];
|
|
194
189
|
}
|
|
195
190
|
|
|
196
|
-
export { decodeURI, isConditionalGET,
|
|
191
|
+
export { decodeURI, isConditionalGET, isPreconditionFailed, parseRanges };
|
package/esm/utils/path.js
CHANGED
package/package.json
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @module
|
|
2
|
+
* @module ReadStream
|
|
3
3
|
*/
|
|
4
4
|
import { PathLike } from 'fs';
|
|
5
|
-
import {
|
|
6
|
-
import { FileSystem } from './fs';
|
|
5
|
+
import { Range } from './utils/http';
|
|
6
|
+
import { FileSystem } from './utils/fs';
|
|
7
7
|
import { Readable, ReadableOptions } from 'stream';
|
|
8
|
-
export interface Range {
|
|
9
|
-
offset: number;
|
|
10
|
-
length: number;
|
|
11
|
-
prefix?: Buffer;
|
|
12
|
-
suffix?: Buffer;
|
|
13
|
-
}
|
|
14
8
|
export interface Options extends Pick<ReadableOptions, 'highWaterMark'> {
|
|
15
9
|
fs: FileSystem;
|
|
16
10
|
}
|
|
17
11
|
interface Callback {
|
|
18
12
|
(error?: Error | null): void;
|
|
19
13
|
}
|
|
20
|
-
export declare class
|
|
14
|
+
export declare class ReadStream extends Readable {
|
|
21
15
|
private readonly fs;
|
|
22
16
|
private readonly path;
|
|
23
17
|
private readonly ranges;
|
package/types/Service.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ interface IgnoreFunction {
|
|
|
10
10
|
interface Headers {
|
|
11
11
|
[key: string]: string | string[];
|
|
12
12
|
}
|
|
13
|
-
interface
|
|
14
|
-
(path: string, stats: Stats): Headers | void;
|
|
13
|
+
interface HeadersFunction {
|
|
14
|
+
(path: string, stats: Stats): Promise<Headers | void> | Headers | void;
|
|
15
15
|
}
|
|
16
16
|
export interface Options {
|
|
17
17
|
fs?: FileSystem;
|
|
@@ -20,7 +20,7 @@ export interface Options {
|
|
|
20
20
|
lastModified?: boolean;
|
|
21
21
|
highWaterMark?: number;
|
|
22
22
|
ignore?: IgnoreFunction;
|
|
23
|
-
headers?: Headers |
|
|
23
|
+
headers?: Headers | HeadersFunction;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* @class Service
|
package/types/utils/http.d.ts
CHANGED
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Stats } from 'fs';
|
|
5
5
|
import { Context } from 'koa';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { Buffer } from 'buffer';
|
|
7
|
+
export interface Range {
|
|
8
|
+
offset: number;
|
|
9
|
+
length: number;
|
|
10
|
+
prefix?: Buffer;
|
|
11
|
+
suffix?: Buffer;
|
|
12
|
+
}
|
|
8
13
|
/**
|
|
9
14
|
* @function decodeURI
|
|
10
15
|
* @description Decode URI component.
|
|
@@ -16,18 +21,17 @@ export declare function decodeURI(URI: string): string | -1;
|
|
|
16
21
|
* @description Check if request is conditional GET.
|
|
17
22
|
* @param context The koa context.
|
|
18
23
|
*/
|
|
19
|
-
export declare function isConditionalGET(
|
|
24
|
+
export declare function isConditionalGET({ request }: Context): boolean;
|
|
20
25
|
/**
|
|
21
|
-
* @function
|
|
22
|
-
* @description Check if request precondition
|
|
26
|
+
* @function isPreconditionFailed
|
|
27
|
+
* @description Check if request precondition failed.
|
|
23
28
|
* @param context The koa context.
|
|
24
29
|
*/
|
|
25
|
-
export declare function
|
|
30
|
+
export declare function isPreconditionFailed({ request, response }: Context): boolean;
|
|
26
31
|
/**
|
|
27
32
|
* @function parseRanges
|
|
28
33
|
* @description Parse ranges.
|
|
29
34
|
* @param context The koa context.
|
|
30
35
|
* @param stats The file stats.
|
|
31
36
|
*/
|
|
32
|
-
export declare function parseRanges(context: Context, stats: Stats):
|
|
33
|
-
export {};
|
|
37
|
+
export declare function parseRanges(context: Context, stats: Stats): Range[] | -1 | -2;
|