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