koa-files 2.3.7 → 2.3.8

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