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