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