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/README.md CHANGED
@@ -16,48 +16,93 @@ $ npm install koa-files
16
16
 
17
17
  ## API
18
18
 
19
- ```js
20
- import Koa from 'koa';
21
- import files from 'koa-files';
19
+ ```ts
20
+ import { Middleware } from 'koa';
21
+ import { createReadStream, stat, Stats } from 'fs';
22
22
 
23
- const app = new Koa();
23
+ interface IgnoreFunction {
24
+ (path: string): boolean;
25
+ }
24
26
 
25
- // Static files server
26
- app.use(files(root, options));
27
+ interface Headers {
28
+ [key: string]: string | string[];
29
+ }
30
+
31
+ interface HeaderFunction {
32
+ (path: string, stats: Stats): Headers | void;
33
+ }
34
+
35
+ interface FileSystem {
36
+ readonly stat: typeof stat;
37
+ readonly createReadStream: typeof createReadStream;
38
+ }
39
+
40
+ export interface Options {
41
+ etag?: boolean;
42
+ fs?: FileSystem;
43
+ defer?: boolean;
44
+ acceptRanges?: boolean;
45
+ lastModified?: boolean;
46
+ ignore?: IgnoreFunction;
47
+ headers?: Headers | HeaderFunction;
48
+ }
49
+
50
+ export default function server(root: string, options?: Options): Middleware;
27
51
  ```
28
52
 
29
- - `root` root directory string. nothing above this root directory can be served.
30
- - `options` options object.
53
+ ### root
54
+
55
+ - Root directory string.
56
+ - Nothing above this root directory can be served.
31
57
 
32
58
  ### Options
33
59
 
34
- #### acceptRanges?: `boolean`
60
+ ##### `fs`
61
+
62
+ - Defaults to `node:fs`.
63
+ - The file system to used.
35
64
 
36
- - Enable or disable accepting ranged requests. Disabling this will not send Accept-Ranges and ignore the contents of the Range request header. defaults to `true`.
65
+ ##### `headers`
37
66
 
38
- #### cacheControl?: `string`
67
+ - Defaults to `undefined`.
68
+ - Set headers to be sent.
69
+ - See docs: [Headers in MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers).
39
70
 
40
- - Set Cache-Control response header, defaults to `undefined`, see docs: [Cache-Control in MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).
71
+ ##### `acceptRanges`
41
72
 
42
- #### etag?: `boolean`
73
+ - Defaults to `true`.
74
+ - Enable or disable accepting ranged requests.
75
+ - Disabling this will not send Accept-Ranges and ignore the contents of the Range request header.
76
+ - Can be overridden by the `headers`.
43
77
 
44
- - Enable or disable etag generation, defaults to `true`.
78
+ ##### `etag`
45
79
 
46
- #### lastModified?: `boolean`
80
+ - Defaults to `true`.
81
+ - Enable or disable etag generation.
82
+ - Use weak etag internally.
83
+ - Can be overridden by the `headers`.
47
84
 
48
- - Enable or disable Last-Modified header, defaults to true. Uses the file system's last modified value. defaults to `true`.
85
+ ##### `lastModified`
49
86
 
50
- #### ignore?: `(path: string) => boolean`
87
+ - Defaults to `true`.
88
+ - Enable or disable Last-Modified header.
89
+ - Use the file system's last modified value.
90
+ - Can be overridden by the `headers`.
51
91
 
52
- - Set ignore rules. defaults to `undefined`.
92
+ ##### `ignore`
53
93
 
54
- #### defer?: `boolean`
94
+ - Defaults to `undefined`.
95
+ - Function that determines if a file should be ignored.
55
96
 
56
- - If true, serves after `await next()`, allowing any downstream middleware to respond first. defaults to `false`.
97
+ ##### `defer`
98
+
99
+ - Defaults to `false`.
100
+ - If true, serves after `await next()`.
101
+ - Allowing any downstream middleware to respond first.
57
102
 
58
103
  ## Example
59
104
 
60
- ```js
105
+ ```ts
61
106
  /**
62
107
  * @module server
63
108
  * @license MIT
@@ -70,6 +115,15 @@ import files from 'koa-files';
70
115
  const app = new Koa();
71
116
  const port = process.env.PORT || 80;
72
117
 
118
+ // Static files server
119
+ app.use(
120
+ files('tests', {
121
+ headers: {
122
+ 'Cache-Control': 'public, max-age=31557600'
123
+ }
124
+ })
125
+ );
126
+
73
127
  /**
74
128
  * @function httpError
75
129
  * @param {NodeJS.ErrnoException} error
@@ -79,14 +133,15 @@ function httpError(error) {
79
133
  return /^(EOF|EPIPE|ECANCELED|ECONNRESET|ECONNABORTED)$/i.test(error.code);
80
134
  }
81
135
 
82
- // Static files server
83
- app.use(files('tests', { cacheControl: 'public, max-age=31557600' }));
84
-
85
136
  // Listen error event
86
- app.on('error', error => !httpError(error) && console.error(error));
137
+ app.on('error', error => {
138
+ !httpError(error) && console.error(error);
139
+ });
87
140
 
88
141
  // Start server
89
- app.listen(port, () => console.log(`> server running at: 127.0.0.1:${port}`));
142
+ app.listen(port, () => {
143
+ console.log(`> server running at: 127.0.0.1:${port}`);
144
+ });
90
145
  ```
91
146
 
92
147
  ## Features
@@ -0,0 +1,280 @@
1
+ /**
2
+ * @package koa-files
3
+ * @license MIT
4
+ * @version 3.0.1
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 createETag = require('etag');
13
+ const destroy = require('destroy');
14
+ const stream = require('stream');
15
+ const fs = require('./utils/fs.cjs');
16
+ const path$1 = require('path');
17
+ const path = require('./utils/path.cjs');
18
+ const http = require('./utils/http.cjs');
19
+
20
+ function _interopDefault(e) {
21
+ return e && e.__esModule ? e : { default: e };
22
+ }
23
+
24
+ const createETag__default = /*#__PURE__*/ _interopDefault(createETag);
25
+ const destroy__default = /*#__PURE__*/ _interopDefault(destroy);
26
+
27
+ /**
28
+ * @module Service
29
+ */
30
+ /**
31
+ * @function isFunction
32
+ * @description Check if value is function.
33
+ * @param value The value to check.
34
+ */
35
+ function isFunction(value) {
36
+ return typeof value === 'function';
37
+ }
38
+ /**
39
+ * @class Service
40
+ */
41
+ class Service {
42
+ /**
43
+ * @constructor
44
+ * @description Create file service.
45
+ * @param root File service root.
46
+ * @param options File service options.
47
+ */
48
+ constructor(root, options) {
49
+ this.options = options;
50
+ this.root = path.unixify(path$1.resolve(root));
51
+ }
52
+ /**
53
+ * @private
54
+ * @method isIgnore
55
+ * @description Check if path is ignore.
56
+ * @param path File path.
57
+ */
58
+ isIgnore(path) {
59
+ const { ignore } = this.options;
60
+ return (isFunction(ignore) ? ignore(path) : false) === true;
61
+ }
62
+ /**
63
+ * @private
64
+ * @method setupHeaders
65
+ * @description Setup headers
66
+ * @param context Koa context
67
+ * @param path File path
68
+ * @param stats File stats
69
+ */
70
+ setupHeaders(context, path, stats) {
71
+ const { options } = this;
72
+ const { headers, etag } = options;
73
+ // Set status.
74
+ context.status = 200;
75
+ // Set Content-Type.
76
+ context.type = path$1.extname(path);
77
+ // Accept-Ranges.
78
+ if (options.acceptRanges === false) {
79
+ // Set Accept-Ranges to none tell client not support.
80
+ context.set('Accept-Ranges', 'none');
81
+ } else {
82
+ // Set Accept-Ranges.
83
+ context.set('Accept-Ranges', 'bytes');
84
+ }
85
+ // ETag.
86
+ if (etag === false) {
87
+ // Remove ETag.
88
+ context.remove('ETag');
89
+ } else {
90
+ context.set('ETag', createETag__default.default(stats));
91
+ }
92
+ // Last-Modified.
93
+ if (options.lastModified === false) {
94
+ // Remove Last-Modified.
95
+ context.remove('Last-Modified');
96
+ } else {
97
+ // Set mtime utc string.
98
+ context.set('Last-Modified', stats.mtime.toUTCString());
99
+ }
100
+ // Set headers.
101
+ if (headers) {
102
+ if (isFunction(headers)) {
103
+ const fields = headers(path, stats);
104
+ if (fields) {
105
+ context.set(fields);
106
+ }
107
+ } else {
108
+ context.set(headers);
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * @private
114
+ * @method write
115
+ * @description Write file to stream.
116
+ * @param stream Destination stream.
117
+ * @param path The file path to read.
118
+ * @param range The range to read.
119
+ * @param end Is destory destination stream after read complete.
120
+ */
121
+ write(stream, path, range, end) {
122
+ const { fs } = this.options;
123
+ return new Promise(resolve => {
124
+ // Range prefix and suffix.
125
+ const { prefix, suffix } = range;
126
+ // Create file stream reader.
127
+ const reader = fs.createReadStream(path, range);
128
+ // File read stream open.
129
+ if (prefix) {
130
+ reader.once('open', () => {
131
+ // Write prefix boundary.
132
+ stream.write(prefix);
133
+ });
134
+ }
135
+ // File read stream end.
136
+ if (suffix) {
137
+ reader.once('end', () => {
138
+ // Push suffix boundary.
139
+ stream.write(suffix);
140
+ });
141
+ }
142
+ // File read stream error.
143
+ reader.once('error', () => {
144
+ // End stream.
145
+ stream.end();
146
+ // Unpipe.
147
+ reader.unpipe();
148
+ // Destroy.
149
+ destroy__default.default(reader);
150
+ // Resolve.
151
+ resolve(false);
152
+ });
153
+ // File read stream close.
154
+ reader.once('close', () => {
155
+ // Unpipe.
156
+ reader.unpipe();
157
+ // Destroy.
158
+ destroy__default.default(reader);
159
+ // Resolve.
160
+ resolve(true);
161
+ });
162
+ // Write data to buffer.
163
+ reader.pipe(stream, { end });
164
+ });
165
+ }
166
+ /**
167
+ * @private
168
+ * @method send
169
+ * @description Send file.
170
+ * @param context Koa context.
171
+ * @param path File path.
172
+ * @param ranges Read ranges.
173
+ */
174
+ async send(context, path, ranges) {
175
+ // Set stream body, highWaterMark 64kb.
176
+ const stream$1 = new stream.PassThrough({
177
+ highWaterMark: 65536
178
+ });
179
+ // Set response body.
180
+ context.body = stream$1;
181
+ // Ranges length.
182
+ let { length } = ranges;
183
+ // Write ranges to stream.
184
+ for (const range of ranges) {
185
+ // Write range.
186
+ const passed = await this.write(stream$1, path, range, --length === 0);
187
+ // If not passed, break.
188
+ if (!passed) {
189
+ break;
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * @public
195
+ * @method response
196
+ * @description Response to koa context.
197
+ * @param context Koa context.
198
+ */
199
+ async response(context) {
200
+ const { root } = this;
201
+ // Only support GET and HEAD (405).
202
+ if (context.method !== 'GET' && context.method !== 'HEAD') {
203
+ return false;
204
+ }
205
+ // Get path of file
206
+ const pathname = http.decodeURI(context.path);
207
+ const path$2 = pathname === -1 ? pathname : path.unixify(path$1.join(root, pathname));
208
+ // Path -1 or null byte(s)
209
+ if (path$2 === -1 || path$2.includes('\0')) {
210
+ return context.throw(400);
211
+ }
212
+ // Malicious path (403).
213
+ if (path.isOutRoot(path$2, root)) {
214
+ return false;
215
+ }
216
+ // Is ignore path or file (403).
217
+ if (this.isIgnore(path$2)) {
218
+ return false;
219
+ }
220
+ // File stats.
221
+ const stats = await fs.stat(this.options.fs, path$2);
222
+ // Check file stats.
223
+ if (
224
+ // File not exist (404 | 500).
225
+ stats == null ||
226
+ // Is directory (403).
227
+ stats.isDirectory() ||
228
+ // Not a directory but has trailing slash (404).
229
+ path.hasTrailingSlash(path$2)
230
+ ) {
231
+ return false;
232
+ }
233
+ // Setup headers.
234
+ this.setupHeaders(context, path$2, stats);
235
+ // Conditional get support.
236
+ if (http.isConditionalGET(context)) {
237
+ // Request precondition failure.
238
+ if (http.isPreconditionFailure(context)) {
239
+ return context.throw(412);
240
+ }
241
+ // Request fresh (304).
242
+ if (context.fresh) {
243
+ // Set status.
244
+ context.status = 304;
245
+ // Set body null.
246
+ context.body = null;
247
+ // File found.
248
+ return true;
249
+ }
250
+ }
251
+ // Head request.
252
+ if (context.method === 'HEAD') {
253
+ // Set Content-Length.
254
+ context.length = stats.size;
255
+ // Set body null
256
+ context.body = null;
257
+ // File found.
258
+ return true;
259
+ }
260
+ // Parsed ranges.
261
+ const ranges = http.parseRanges(context, stats);
262
+ // 416
263
+ if (ranges === -1) {
264
+ // Set Content-Range.
265
+ context.set('Content-Range', `bytes */${stats.size}`);
266
+ // Unsatisfiable 416.
267
+ return context.throw(416);
268
+ }
269
+ // 400.
270
+ if (ranges === -2) {
271
+ return context.throw(400);
272
+ }
273
+ // Send file.
274
+ this.send(context, path$2, ranges);
275
+ // File found.
276
+ return true;
277
+ }
278
+ }
279
+
280
+ module.exports = Service;
package/cjs/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 2.3.8
4
+ * @version 3.0.1
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,12 +9,17 @@
9
9
 
10
10
  'use strict';
11
11
 
12
- const Files = require('./Files.cjs');
12
+ const fs = require('fs');
13
+ const Service = require('./Service.cjs');
14
+
15
+ function _interopDefault(e) {
16
+ return e && e.__esModule ? e : { default: e };
17
+ }
18
+
19
+ const fs__default = /*#__PURE__*/ _interopDefault(fs);
13
20
 
14
21
  /**
15
22
  * @module index
16
- * @license MIT
17
- * @author nuintun
18
23
  */
19
24
  /**
20
25
  * @function server
@@ -22,15 +27,16 @@ const Files = require('./Files.cjs');
22
27
  * @param {Options} options
23
28
  */
24
29
  function server(root, options) {
25
- const files = new Files(root, options);
26
- if (options && options.defer) {
30
+ const config = Object.assign({ fs: fs__default.default }, options);
31
+ const service = new Service(root, config);
32
+ if (config.defer) {
27
33
  return async (context, next) => {
28
34
  await next();
29
- await files.response(context);
35
+ await service.response(context);
30
36
  };
31
37
  }
32
38
  return async (context, next) => {
33
- if (!(await files.response(context))) {
39
+ if (!(await service.response(context))) {
34
40
  await next();
35
41
  }
36
42
  };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @package koa-files
3
+ * @license MIT
4
+ * @version 3.0.1
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
+ /**
13
+ * @module fs
14
+ */
15
+ /**
16
+ * @function stat
17
+ * @description Get file stats.
18
+ * @param fs The file system to used.
19
+ * @param path The file path.
20
+ */
21
+ function stat(fs, path) {
22
+ return new Promise(resolve => {
23
+ fs.stat(path, (error, stats) => {
24
+ resolve(error ? null : stats);
25
+ });
26
+ });
27
+ }
28
+
29
+ exports.stat = stat;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @package koa-files
3
+ * @license MIT
4
+ * @version 3.0.1
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 crypto = require('crypto');
13
+
14
+ /**
15
+ * @module hash
16
+ */
17
+ // prettier-ignore
18
+ const CHARS = [
19
+ // 0-9
20
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
21
+ // A-M
22
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
23
+ // N-Z
24
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
25
+ // a-m
26
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
27
+ // n-z
28
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
29
+ ];
30
+ /**
31
+ * @function generate
32
+ * @description Generate a hash.
33
+ * @param length The length of hash.
34
+ */
35
+ function generate(length = 32) {
36
+ let hash = '';
37
+ const randomValues = crypto.getRandomValues(new Uint8Array(length));
38
+ // Create hash.
39
+ for (const value of randomValues) {
40
+ hash += CHARS[value % 62];
41
+ }
42
+ // Return hash.
43
+ return hash;
44
+ }
45
+
46
+ exports.generate = generate;