koa-files 2.3.8 → 3.0.1

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/Files.cjs DELETED
@@ -1,389 +0,0 @@
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/utils.cjs DELETED
@@ -1,150 +0,0 @@
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 fs = require('fs');
13
- const path = require('path');
14
-
15
- function _interopDefault(e) {
16
- return e && e.__esModule ? e : { default: e };
17
- }
18
-
19
- const fs__default = /*#__PURE__*/ _interopDefault(fs);
20
-
21
- /**
22
- * @module utils
23
- * @license MIT
24
- * @author nuintun
25
- */
26
- const CHARS = Array.from('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
27
- /**
28
- * @function isOutRoot
29
- * @description Check if path is out of root
30
- * @param path The path to check
31
- * @param root The root path
32
- */
33
- function isOutRoot(path$1, root) {
34
- path$1 = path.relative(root, path$1);
35
- return /\.\.(?:[\\/]|$)/.test(path$1) || path.isAbsolute(path$1);
36
- }
37
- /**
38
- * @function unixify
39
- * @description Convert path to unix style
40
- * @param path The path to convert
41
- */
42
- function unixify(path) {
43
- return path.replace(/\\/g, '/');
44
- }
45
- /**
46
- * @function decodeURI
47
- * @description Decode URI component
48
- * @param URI The URI to decode
49
- */
50
- function decodeURI(URI) {
51
- try {
52
- return decodeURIComponent(URI);
53
- } catch (error) {
54
- return -1;
55
- }
56
- }
57
- /**
58
- * @function boundaryGenerator
59
- * @description Generate a boundary
60
- */
61
- function boundaryGenerator() {
62
- let boundary = '';
63
- // Create boundary
64
- for (let i = 0; i < 38; i++) {
65
- boundary += CHARS[Math.floor(Math.random() * 62)];
66
- }
67
- // Return boundary
68
- return boundary;
69
- }
70
- /**
71
- * @function fstat
72
- * @description Get file stats
73
- * @param path The file path
74
- */
75
- function fstat(path) {
76
- return new Promise((resolve, reject) => {
77
- fs__default.default.stat(path, (error, stats) => {
78
- error ? reject(error) : resolve(stats);
79
- });
80
- });
81
- }
82
- /**
83
- * @function hasTrailingSlash
84
- * @description Check if path has trailing slash
85
- * @param path The path to check
86
- */
87
- function hasTrailingSlash(path) {
88
- return /\/$/.test(path);
89
- }
90
- /**
91
- * @function parseTokens
92
- * @description Parse HTTP tokens
93
- * @param value The tokens value string
94
- */
95
- function parseTokens(value) {
96
- let end = 0;
97
- let start = 0;
98
- let tokens = [];
99
- // gather tokens
100
- for (let i = 0, length = value.length; i < length; i++) {
101
- switch (value.charCodeAt(i)) {
102
- case 0x20:
103
- // ' '
104
- if (start === end) {
105
- start = end = i + 1;
106
- }
107
- break;
108
- case 0x2c:
109
- // ','
110
- tokens.push(value.substring(start, end));
111
- start = end = i + 1;
112
- break;
113
- default:
114
- end = i + 1;
115
- break;
116
- }
117
- }
118
- // final token
119
- tokens.push(value.substring(start, end));
120
- return tokens;
121
- }
122
- /**
123
- * @function isETag
124
- * @description Check if etag is valid
125
- * @param value The value to check
126
- */
127
- function isETag(value) {
128
- return /^(?:W\/)?"[\s\S]+"$/.test(value);
129
- }
130
- /**
131
- * @function isETagFresh
132
- * @description Check if etag is fresh
133
- * @param match The match value
134
- * @param etag The etag value
135
- */
136
- function isETagFresh(match, etag) {
137
- return parseTokens(match).some(match => {
138
- return match === etag || match === 'W/' + etag || 'W/' + match === etag;
139
- });
140
- }
141
-
142
- exports.boundaryGenerator = boundaryGenerator;
143
- exports.decodeURI = decodeURI;
144
- exports.fstat = fstat;
145
- exports.hasTrailingSlash = hasTrailingSlash;
146
- exports.isETag = isETag;
147
- exports.isETagFresh = isETagFresh;
148
- exports.isOutRoot = isOutRoot;
149
- exports.parseTokens = parseTokens;
150
- exports.unixify = unixify;