koa-files 2.3.6 → 2.3.7
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/Send.cjs +330 -331
- package/cjs/index.cjs +11 -11
- package/cjs/utils.cjs +51 -52
- package/esm/Send.js +325 -324
- package/esm/index.js +11 -11
- package/esm/utils.js +49 -48
- package/package.json +15 -14
- package/types/Send.d.ts +73 -73
- package/types/index.d.ts +1 -1
package/esm/Send.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @package koa-files
|
|
3
3
|
* @license MIT
|
|
4
|
-
* @version 2.3.
|
|
4
|
+
* @version 2.3.7
|
|
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
|
|
@@ -24,343 +24,344 @@ import { unixify, isETagFresh, isETag, boundaryGenerator, decodeURI, isOutRoot,
|
|
|
24
24
|
* @class Send
|
|
25
25
|
*/
|
|
26
26
|
class Send {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* @method isConditionalGET
|
|
38
|
-
* @param {Context} context
|
|
39
|
-
* @returns {boolean}
|
|
40
|
-
*/
|
|
41
|
-
isConditionalGET(context) {
|
|
42
|
-
const { request } = context;
|
|
43
|
-
return !!(
|
|
44
|
-
request.get('If-Match') ||
|
|
45
|
-
request.get('If-None-Match') ||
|
|
46
|
-
request.get('If-Modified-Since') ||
|
|
47
|
-
request.get('if-Unmodified-Since')
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* @method isPreconditionFailure
|
|
52
|
-
* @param {Context} context
|
|
53
|
-
* @returns {boolean}
|
|
54
|
-
*/
|
|
55
|
-
isPreconditionFailure(context) {
|
|
56
|
-
const { request, response } = context;
|
|
57
|
-
// If-Match
|
|
58
|
-
const match = request.get('If-Match');
|
|
59
|
-
if (match) {
|
|
60
|
-
const etag = response.get('ETag');
|
|
61
|
-
return !etag || (match !== '*' && !isETagFresh(match, etag));
|
|
27
|
+
/**
|
|
28
|
+
* @constructor
|
|
29
|
+
* @param {string} root
|
|
30
|
+
* @param {Options} options
|
|
31
|
+
*/
|
|
32
|
+
constructor(root, options = {}) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
this.root = unixify(resolve(root));
|
|
62
35
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @method isConditionalGET
|
|
38
|
+
* @param {Context} context
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
isConditionalGET(context) {
|
|
42
|
+
const { request } = context;
|
|
43
|
+
return !!(request.get('If-Match') ||
|
|
44
|
+
request.get('If-None-Match') ||
|
|
45
|
+
request.get('If-Modified-Since') ||
|
|
46
|
+
request.get('if-Unmodified-Since'));
|
|
68
47
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
// If-Range as etag
|
|
84
|
-
if (isETag(ifRange)) {
|
|
85
|
-
const etag = response.get('ETag');
|
|
86
|
-
return !!(etag && isETagFresh(ifRange, etag));
|
|
87
|
-
}
|
|
88
|
-
// If-Range as modified date
|
|
89
|
-
const lastModified = response.get('Last-Modified');
|
|
90
|
-
return Date.parse(lastModified) <= Date.parse(ifRange);
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* @method isIgnore
|
|
94
|
-
* @param {string} path
|
|
95
|
-
* @returns {boolean}
|
|
96
|
-
*/
|
|
97
|
-
isIgnore(path) {
|
|
98
|
-
const { ignore } = this.options;
|
|
99
|
-
return (typeof ignore === 'function' ? ignore(path) : false) === true;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* @method parseRange
|
|
103
|
-
* @param {Context} context
|
|
104
|
-
* @param {Stats} stats
|
|
105
|
-
* @returns {Ranges}
|
|
106
|
-
*/
|
|
107
|
-
parseRange(context, stats) {
|
|
108
|
-
const { size } = stats;
|
|
109
|
-
const { request } = context;
|
|
110
|
-
// Content-Length
|
|
111
|
-
let contentLength = size;
|
|
112
|
-
// Ranges
|
|
113
|
-
const ranges = [];
|
|
114
|
-
// Range support
|
|
115
|
-
if (this.options.acceptRanges !== false) {
|
|
116
|
-
const range = request.get('Range');
|
|
117
|
-
// Range fresh
|
|
118
|
-
if (range && this.isRangeFresh(context)) {
|
|
119
|
-
// Parse range -1 -2 or []
|
|
120
|
-
const parsed = parseRange(size, range, { combine: true });
|
|
121
|
-
// -1 signals an unsatisfiable range
|
|
122
|
-
// -2 signals a malformed header string
|
|
123
|
-
if (parsed === -1 || parsed === -2) {
|
|
124
|
-
return parsed;
|
|
48
|
+
/**
|
|
49
|
+
* @method isPreconditionFailure
|
|
50
|
+
* @param {Context} context
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
isPreconditionFailure(context) {
|
|
54
|
+
const { request, response } = context;
|
|
55
|
+
// If-Match
|
|
56
|
+
const match = request.get('If-Match');
|
|
57
|
+
if (match) {
|
|
58
|
+
const etag = response.get('ETag');
|
|
59
|
+
return !etag || (match !== '*' && !isETagFresh(match, etag));
|
|
125
60
|
}
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (parsed.length > 1) {
|
|
132
|
-
// Reset content-length
|
|
133
|
-
contentLength = 0;
|
|
134
|
-
// Range boundary
|
|
135
|
-
const boundary = `<${boundaryGenerator()}>`;
|
|
136
|
-
const suffix = `\r\n--${boundary}--\r\n`;
|
|
137
|
-
const contentType = `Content-Type: ${context.type}`;
|
|
138
|
-
context.type = `multipart/byteranges; boundary=${boundary}`;
|
|
139
|
-
// Map ranges
|
|
140
|
-
parsed.forEach(({ start, end }, index) => {
|
|
141
|
-
// The first prefix boundary no \r\n
|
|
142
|
-
const prefixHead = index > 0 ? '\r\n' : '';
|
|
143
|
-
const contentRange = `Content-Range: bytes ${start}-${end}/${size}`;
|
|
144
|
-
const prefix = `${prefixHead}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`;
|
|
145
|
-
// Compute content-length
|
|
146
|
-
contentLength += end - start + 1 + Buffer.byteLength(prefix);
|
|
147
|
-
// Cache range
|
|
148
|
-
ranges.push({ start, end, prefix });
|
|
149
|
-
});
|
|
150
|
-
// The last add suffix boundary
|
|
151
|
-
ranges[ranges.length - 1].suffix = suffix;
|
|
152
|
-
// Compute content-length
|
|
153
|
-
contentLength += Buffer.byteLength(suffix);
|
|
154
|
-
} else {
|
|
155
|
-
const { start, end } = parsed[0];
|
|
156
|
-
context.set('Content-Range', `bytes ${start}-${end}/${size}`);
|
|
157
|
-
// Compute content-length
|
|
158
|
-
contentLength = end - start + 1;
|
|
159
|
-
// Cache range
|
|
160
|
-
ranges.push({ start, end });
|
|
161
|
-
}
|
|
61
|
+
// If-Unmodified-Since
|
|
62
|
+
const unmodifiedSince = Date.parse(request.get('If-Unmodified-Since'));
|
|
63
|
+
if (!isNaN(unmodifiedSince)) {
|
|
64
|
+
const lastModified = Date.parse(response.get('Last-Modified'));
|
|
65
|
+
return isNaN(lastModified) || lastModified > unmodifiedSince;
|
|
162
66
|
}
|
|
163
|
-
|
|
67
|
+
return false;
|
|
164
68
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Set ETag
|
|
186
|
-
context.set('ETag', etag(stats));
|
|
69
|
+
/**
|
|
70
|
+
* @method isRangeFresh
|
|
71
|
+
* @param {Context} context
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
isRangeFresh(context) {
|
|
75
|
+
const { request, response } = context;
|
|
76
|
+
const ifRange = request.get('If-Range');
|
|
77
|
+
// No If-Range
|
|
78
|
+
if (!ifRange) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
// If-Range as etag
|
|
82
|
+
if (isETag(ifRange)) {
|
|
83
|
+
const etag = response.get('ETag');
|
|
84
|
+
return !!(etag && isETagFresh(ifRange, etag));
|
|
85
|
+
}
|
|
86
|
+
// If-Range as modified date
|
|
87
|
+
const lastModified = response.get('Last-Modified');
|
|
88
|
+
return Date.parse(lastModified) <= Date.parse(ifRange);
|
|
187
89
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
90
|
+
/**
|
|
91
|
+
* @method isIgnore
|
|
92
|
+
* @param {string} path
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
isIgnore(path) {
|
|
96
|
+
const { ignore } = this.options;
|
|
97
|
+
return (typeof ignore === 'function' ? ignore(path) : false) === true;
|
|
192
98
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
99
|
+
/**
|
|
100
|
+
* @method parseRange
|
|
101
|
+
* @param {Context} context
|
|
102
|
+
* @param {Stats} stats
|
|
103
|
+
* @returns {Ranges}
|
|
104
|
+
*/
|
|
105
|
+
parseRange(context, stats) {
|
|
106
|
+
const { size } = stats;
|
|
107
|
+
const { request } = context;
|
|
108
|
+
// Content-Length
|
|
109
|
+
let contentLength = size;
|
|
110
|
+
// Ranges
|
|
111
|
+
const ranges = [];
|
|
112
|
+
// Range support
|
|
113
|
+
if (this.options.acceptRanges !== false) {
|
|
114
|
+
const range = request.get('Range');
|
|
115
|
+
// Range fresh
|
|
116
|
+
if (range && this.isRangeFresh(context)) {
|
|
117
|
+
// Parse range -1 -2 or []
|
|
118
|
+
const parsed = parseRange(size, range, { combine: true });
|
|
119
|
+
// -1 signals an unsatisfiable range
|
|
120
|
+
// -2 signals a malformed header string
|
|
121
|
+
if (parsed === -1 || parsed === -2) {
|
|
122
|
+
return parsed;
|
|
123
|
+
}
|
|
124
|
+
// Ranges ok, support multiple ranges
|
|
125
|
+
if (parsed.type === 'bytes') {
|
|
126
|
+
// Set 206 status
|
|
127
|
+
context.status = 206;
|
|
128
|
+
// Multiple ranges
|
|
129
|
+
if (parsed.length > 1) {
|
|
130
|
+
// Reset content-length
|
|
131
|
+
contentLength = 0;
|
|
132
|
+
// Range boundary
|
|
133
|
+
const boundary = `<${boundaryGenerator()}>`;
|
|
134
|
+
const suffix = `\r\n--${boundary}--\r\n`;
|
|
135
|
+
const contentType = `Content-Type: ${context.type}`;
|
|
136
|
+
context.type = `multipart/byteranges; boundary=${boundary}`;
|
|
137
|
+
// Map ranges
|
|
138
|
+
parsed.forEach(({ start, end }, index) => {
|
|
139
|
+
// The first prefix boundary no \r\n
|
|
140
|
+
const prefixHead = index > 0 ? '\r\n' : '';
|
|
141
|
+
const contentRange = `Content-Range: bytes ${start}-${end}/${size}`;
|
|
142
|
+
const prefix = `${prefixHead}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`;
|
|
143
|
+
// Compute content-length
|
|
144
|
+
contentLength += end - start + 1 + Buffer.byteLength(prefix);
|
|
145
|
+
// Cache range
|
|
146
|
+
ranges.push({ start, end, prefix });
|
|
147
|
+
});
|
|
148
|
+
// The last add suffix boundary
|
|
149
|
+
ranges[ranges.length - 1].suffix = suffix;
|
|
150
|
+
// Compute content-length
|
|
151
|
+
contentLength += Buffer.byteLength(suffix);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const { start, end } = parsed[0];
|
|
155
|
+
context.set('Content-Range', `bytes ${start}-${end}/${size}`);
|
|
156
|
+
// Compute content-length
|
|
157
|
+
contentLength = end - start + 1;
|
|
158
|
+
// Cache range
|
|
159
|
+
ranges.push({ start, end });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Set Content-Length
|
|
165
|
+
context.length = contentLength;
|
|
166
|
+
return ranges.length ? ranges : [{ start: 0 }];
|
|
197
167
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
168
|
+
/**
|
|
169
|
+
* @method setupHeaders
|
|
170
|
+
* @param {Context} context
|
|
171
|
+
* @param {string} path
|
|
172
|
+
* @param {Stats} stats
|
|
173
|
+
*/
|
|
174
|
+
setupHeaders(context, path, stats) {
|
|
175
|
+
const { options } = this;
|
|
176
|
+
const { toString } = Object.prototype;
|
|
177
|
+
const { acceptRanges, cacheControl, lastModified } = options;
|
|
178
|
+
// Set status
|
|
179
|
+
context.status = 200;
|
|
180
|
+
// Set Content-Type
|
|
181
|
+
context.type = extname(path);
|
|
182
|
+
// ETag
|
|
183
|
+
if (options.etag !== false) {
|
|
184
|
+
// Set ETag
|
|
185
|
+
context.set('ETag', etag(stats));
|
|
186
|
+
}
|
|
187
|
+
// Accept-Ranges
|
|
188
|
+
if (acceptRanges !== false) {
|
|
189
|
+
// Set Accept-Ranges
|
|
190
|
+
context.set('Accept-Ranges', 'bytes');
|
|
191
|
+
}
|
|
192
|
+
// Last-Modified
|
|
193
|
+
if (lastModified !== false) {
|
|
194
|
+
// Set mtime utc string
|
|
195
|
+
context.set('Last-Modified', stats.mtime.toUTCString());
|
|
196
|
+
}
|
|
197
|
+
// Cache-Control
|
|
198
|
+
if (cacheControl && toString.call(cacheControl) === '[object String]') {
|
|
199
|
+
// Set Cache-Control
|
|
200
|
+
context.set('Cache-Control', cacheControl);
|
|
201
|
+
}
|
|
202
202
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
203
|
+
/**
|
|
204
|
+
* @method read
|
|
205
|
+
* @param {string} path
|
|
206
|
+
* @param {Range} range
|
|
207
|
+
* @param {PassThrough} buffer
|
|
208
|
+
* @param {boolean} end
|
|
209
|
+
* @returns {Promise<true>}
|
|
210
|
+
*/
|
|
211
|
+
read(path, range, buffer, end) {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
// Create file stream
|
|
214
|
+
const file = fs.createReadStream(path, range);
|
|
215
|
+
// File read stream open
|
|
216
|
+
if (range.prefix) {
|
|
217
|
+
file.once('open', () => {
|
|
218
|
+
// Write prefix boundary
|
|
219
|
+
buffer.write(range.prefix);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// File read stream error
|
|
223
|
+
file.once('error', error => {
|
|
224
|
+
// Unpipe
|
|
225
|
+
file.unpipe(buffer);
|
|
226
|
+
// Destroy file stream
|
|
227
|
+
destroy(file);
|
|
228
|
+
// Reject
|
|
229
|
+
reject(error);
|
|
230
|
+
});
|
|
231
|
+
// File read stream end
|
|
232
|
+
if (range.suffix) {
|
|
233
|
+
file.once('end', () => {
|
|
234
|
+
// Push suffix boundary
|
|
235
|
+
buffer.write(range.suffix);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// File read stream close
|
|
239
|
+
file.once('close', () => {
|
|
240
|
+
// Unpipe
|
|
241
|
+
file.unpipe(buffer);
|
|
242
|
+
// Destroy file stream
|
|
243
|
+
destroy(file);
|
|
244
|
+
// Resolve
|
|
245
|
+
resolve(true);
|
|
246
|
+
});
|
|
247
|
+
// Write data to buffer
|
|
248
|
+
file.pipe(buffer, { end });
|
|
237
249
|
});
|
|
238
|
-
}
|
|
239
|
-
// File read stream close
|
|
240
|
-
file.once('close', () => {
|
|
241
|
-
// Unpipe
|
|
242
|
-
file.unpipe(buffer);
|
|
243
|
-
// Destroy file stream
|
|
244
|
-
destroy(file);
|
|
245
|
-
// Resolve
|
|
246
|
-
resolve(true);
|
|
247
|
-
});
|
|
248
|
-
// Write data to buffer
|
|
249
|
-
file.pipe(buffer, { end });
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* @method send
|
|
254
|
-
* @param {Context} context
|
|
255
|
-
* @param {string} path
|
|
256
|
-
* @param {Range[]} ranges
|
|
257
|
-
*/
|
|
258
|
-
async send(context, path, ranges) {
|
|
259
|
-
// Ranges count
|
|
260
|
-
let count = ranges.length;
|
|
261
|
-
// Set stream body, highWaterMark 64kb
|
|
262
|
-
const stream = new PassThrough({ highWaterMark: 65536 });
|
|
263
|
-
// Set response body
|
|
264
|
-
context.body = stream;
|
|
265
|
-
// Read file ranges
|
|
266
|
-
try {
|
|
267
|
-
for (const range of ranges) {
|
|
268
|
-
await this.read(path, range, stream, --count === 0);
|
|
269
|
-
}
|
|
270
|
-
} catch (error) {
|
|
271
|
-
// End stream when read exception
|
|
272
|
-
stream.end();
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* @method response
|
|
277
|
-
* @param {Context} context
|
|
278
|
-
* @returns {Promise<boolean>}
|
|
279
|
-
*/
|
|
280
|
-
async response(context) {
|
|
281
|
-
const { root } = this;
|
|
282
|
-
// Only support GET and HEAD (405)
|
|
283
|
-
if (context.method !== 'GET' && context.method !== 'HEAD') {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
// Get path of file
|
|
287
|
-
const pathname = decodeURI(context.path);
|
|
288
|
-
const path = pathname === -1 ? pathname : unixify(join(root, pathname));
|
|
289
|
-
// Path -1 or null byte(s)
|
|
290
|
-
if (path === -1 || path.includes('\0')) {
|
|
291
|
-
return context.throw(400);
|
|
292
|
-
}
|
|
293
|
-
// Malicious path (403)
|
|
294
|
-
if (isOutRoot(path, root)) {
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
// Is ignore path or file (403)
|
|
298
|
-
if (this.isIgnore(path)) {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
// File stats
|
|
302
|
-
let stats;
|
|
303
|
-
// Get file stats
|
|
304
|
-
try {
|
|
305
|
-
stats = await fstat(path);
|
|
306
|
-
} catch (error) {
|
|
307
|
-
// 404 | 500
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
// File not exist (404 | 500)
|
|
311
|
-
if (!stats) {
|
|
312
|
-
return false;
|
|
313
250
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
251
|
+
/**
|
|
252
|
+
* @method send
|
|
253
|
+
* @param {Context} context
|
|
254
|
+
* @param {string} path
|
|
255
|
+
* @param {Range[]} ranges
|
|
256
|
+
*/
|
|
257
|
+
async send(context, path, ranges) {
|
|
258
|
+
// Ranges count
|
|
259
|
+
let count = ranges.length;
|
|
260
|
+
// Set stream body, highWaterMark 64kb
|
|
261
|
+
const stream = new PassThrough({ highWaterMark: 65536 });
|
|
262
|
+
// Set response body
|
|
263
|
+
context.body = stream;
|
|
264
|
+
// Read file ranges
|
|
265
|
+
try {
|
|
266
|
+
for (const range of ranges) {
|
|
267
|
+
await this.read(path, range, stream, --count === 0);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
// End stream when read exception
|
|
272
|
+
stream.end();
|
|
273
|
+
}
|
|
321
274
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
275
|
+
/**
|
|
276
|
+
* @method response
|
|
277
|
+
* @param {Context} context
|
|
278
|
+
* @returns {Promise<boolean>}
|
|
279
|
+
*/
|
|
280
|
+
async response(context) {
|
|
281
|
+
const { root } = this;
|
|
282
|
+
// Only support GET and HEAD (405)
|
|
283
|
+
if (context.method !== 'GET' && context.method !== 'HEAD') {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
// Get path of file
|
|
287
|
+
const pathname = decodeURI(context.path);
|
|
288
|
+
const path = pathname === -1 ? pathname : unixify(join(root, pathname));
|
|
289
|
+
// Path -1 or null byte(s)
|
|
290
|
+
if (path === -1 || path.includes('\0')) {
|
|
291
|
+
return context.throw(400);
|
|
292
|
+
}
|
|
293
|
+
// Malicious path (403)
|
|
294
|
+
if (isOutRoot(path, root)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
// Is ignore path or file (403)
|
|
298
|
+
if (this.isIgnore(path)) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
// File stats
|
|
302
|
+
let stats;
|
|
303
|
+
// Get file stats
|
|
304
|
+
try {
|
|
305
|
+
stats = await fstat(path);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
// 404 | 500
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
// File not exist (404 | 500)
|
|
312
|
+
if (!stats) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
// Is directory (403)
|
|
316
|
+
if (stats.isDirectory()) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
// Not a directory but has trailing slash (404)
|
|
320
|
+
if (hasTrailingSlash(path)) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
// Setup headers
|
|
324
|
+
this.setupHeaders(context, path, stats);
|
|
325
|
+
// Conditional get support
|
|
326
|
+
if (this.isConditionalGET(context)) {
|
|
327
|
+
// Request precondition failure
|
|
328
|
+
if (this.isPreconditionFailure(context)) {
|
|
329
|
+
return context.throw(412);
|
|
330
|
+
}
|
|
331
|
+
// Request fresh (304)
|
|
332
|
+
if (context.fresh) {
|
|
333
|
+
// Set status
|
|
334
|
+
context.status = 304;
|
|
335
|
+
// Set body null
|
|
336
|
+
context.body = null;
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Head request
|
|
341
|
+
if (context.method === 'HEAD') {
|
|
342
|
+
// Set content-length
|
|
343
|
+
context.length = stats.size;
|
|
344
|
+
// Set body null
|
|
345
|
+
context.body = null;
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
// Parsed ranges
|
|
349
|
+
const ranges = this.parseRange(context, stats);
|
|
350
|
+
// 416
|
|
351
|
+
if (ranges === -1) {
|
|
352
|
+
// Set content-range
|
|
353
|
+
context.set('Content-Range', `bytes */${stats.size}`);
|
|
354
|
+
// Unsatisfiable 416
|
|
355
|
+
return context.throw(416);
|
|
356
|
+
}
|
|
357
|
+
// 400
|
|
358
|
+
if (ranges === -2) {
|
|
359
|
+
return context.throw(400);
|
|
360
|
+
}
|
|
361
|
+
// Send file
|
|
362
|
+
this.send(context, path, ranges);
|
|
336
363
|
return true;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
// Head request
|
|
340
|
-
if (context.method === 'HEAD') {
|
|
341
|
-
// Set content-length
|
|
342
|
-
context.length = stats.size;
|
|
343
|
-
// Set body null
|
|
344
|
-
context.body = null;
|
|
345
|
-
return true;
|
|
346
|
-
}
|
|
347
|
-
// Parsed ranges
|
|
348
|
-
const ranges = this.parseRange(context, stats);
|
|
349
|
-
// 416
|
|
350
|
-
if (ranges === -1) {
|
|
351
|
-
// Set content-range
|
|
352
|
-
context.set('Content-Range', `bytes */${stats.size}`);
|
|
353
|
-
// Unsatisfiable 416
|
|
354
|
-
return context.throw(416);
|
|
355
|
-
}
|
|
356
|
-
// 400
|
|
357
|
-
if (ranges === -2) {
|
|
358
|
-
return context.throw(400);
|
|
359
364
|
}
|
|
360
|
-
// Send file
|
|
361
|
-
this.send(context, path, ranges);
|
|
362
|
-
return true;
|
|
363
|
-
}
|
|
364
365
|
}
|
|
365
366
|
|
|
366
367
|
export { Send as default };
|