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