koa-files 3.0.7 → 3.1.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/README.md CHANGED
@@ -89,6 +89,11 @@ export default function server(root: string, options?: Options): Middleware;
89
89
  - Use the file system's last modified value.
90
90
  - Can be overridden by the `headers`.
91
91
 
92
+ ##### `highWaterMark`
93
+
94
+ - Defaults to `node:stream.Readable.highWaterMark`.
95
+ - Set the high water mark for the read stream.
96
+
92
97
  ##### `ignore`
93
98
 
94
99
  - Defaults to `undefined`.
package/cjs/Service.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
@@ -10,10 +10,9 @@
10
10
  'use strict';
11
11
 
12
12
  const createETag = require('etag');
13
- const destroy = require('destroy');
14
- const stream = require('stream');
15
13
  const fs = require('./utils/fs.cjs');
16
14
  const path$1 = require('path');
15
+ const stream = require('./utils/stream.cjs');
17
16
  const path = require('./utils/path.cjs');
18
17
  const http = require('./utils/http.cjs');
19
18
 
@@ -22,7 +21,6 @@ function _interopDefault(e) {
22
21
  }
23
22
 
24
23
  const createETag__default = /*#__PURE__*/ _interopDefault(createETag);
25
- const destroy__default = /*#__PURE__*/ _interopDefault(destroy);
26
24
 
27
25
  /**
28
26
  * @module Service
@@ -46,6 +44,18 @@ class Service {
46
44
  * @param options The file service options.
47
45
  */
48
46
  constructor(root, options) {
47
+ Object.defineProperty(this, 'root', {
48
+ enumerable: true,
49
+ configurable: true,
50
+ writable: true,
51
+ value: void 0
52
+ });
53
+ Object.defineProperty(this, 'options', {
54
+ enumerable: true,
55
+ configurable: true,
56
+ writable: true,
57
+ value: void 0
58
+ });
49
59
  this.options = options;
50
60
  this.root = path.unixify(path$1.resolve(root));
51
61
  }
@@ -109,81 +119,6 @@ class Service {
109
119
  }
110
120
  }
111
121
  }
112
- /**
113
- * @private
114
- * @method read
115
- * @description Read file with range.
116
- * @param path The file path to read.
117
- * @param range The range to read.
118
- * @param stream The destination stream.
119
- * @param end The end flag after pipe to stream.
120
- */
121
- read(path, range, stream, 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 with ranges.
170
- * @param path The file path to send.
171
- * @param ranges The ranges to send.
172
- * @param stream The destination stream.
173
- */
174
- async send(path, ranges, stream) {
175
- // Ranges length.
176
- let { length } = ranges;
177
- // Write ranges to stream.
178
- for (const range of ranges) {
179
- // Write range.
180
- const passed = await this.read(path, range, stream, --length === 0);
181
- // If not passed, break.
182
- if (!passed) {
183
- break;
184
- }
185
- }
186
- }
187
122
  /**
188
123
  * @public
189
124
  * @method respond
@@ -264,14 +199,10 @@ class Service {
264
199
  if (ranges === -2) {
265
200
  return context.throw(400);
266
201
  }
267
- // Set stream body, highWaterMark 64kb.
268
- const stream$1 = new stream.PassThrough({
269
- highWaterMark: 65536
270
- });
271
- // Send file with ranges.
272
- this.send(path$2, ranges, stream$1);
202
+ // Ranges length.
203
+ const { fs: fs$1, highWaterMark } = this.options;
273
204
  // Set response body.
274
- context.body = stream$1;
205
+ context.body = new stream.FileReadStream(path$2, ranges, { fs: fs$1, highWaterMark });
275
206
  // File found.
276
207
  return true;
277
208
  }
package/cjs/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
package/cjs/utils/fs.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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,6 +9,7 @@
9
9
 
10
10
  'use strict';
11
11
 
12
+ const buffer = require('buffer');
12
13
  const hash = require('./hash.cjs');
13
14
  const parseRange = require('range-parser');
14
15
 
@@ -175,42 +176,44 @@ function parseRanges(context, stats) {
175
176
  let contentLength = 0;
176
177
  // Ranges.
177
178
  const ranges = [];
179
+ // Parsed entries.
180
+ const entries = parsed.entries();
178
181
  // Range boundary.
179
182
  const boundary = `<${hash.generate()}>`;
180
- // Range suffix.
181
- const suffix = `\r\n--${boundary}--\r\n`;
182
183
  // Multipart Content-Type.
183
184
  const contentType = `Content-Type: ${context.type}`;
185
+ // Range suffix.
186
+ const suffix = buffer.Buffer.from(`\r\n--${boundary}--\r\n`);
184
187
  // Override Content-Type.
185
188
  context.type = `multipart/byteranges; boundary=${boundary}`;
186
189
  // Map ranges.
187
- for (let index = 0; index < length; index++) {
188
- const { start, end } = parsed[index];
189
- // The first prefix boundary no \r\n.
190
+ for (const [index, { start, end }] of entries) {
191
+ const length = end - start + 1;
190
192
  const head = index > 0 ? '\r\n' : '';
191
193
  const contentRange = `Content-Range: bytes ${start}-${end}/${size}`;
192
- const prefix = `${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`;
194
+ const prefix = buffer.Buffer.from(`${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`);
193
195
  // Compute Content-Length
194
- contentLength += end - start + 1 + Buffer.byteLength(prefix);
196
+ contentLength += length + prefix.length;
195
197
  // Cache range.
196
- ranges.push({ start, end, prefix });
198
+ ranges.push({ offset: start, length, prefix });
197
199
  }
198
- // The last add suffix boundary.
199
- ranges[length - 1].suffix = suffix;
200
200
  // Compute Content-Length.
201
- contentLength += Buffer.byteLength(suffix);
201
+ contentLength += suffix.length;
202
202
  // Set Content-Length.
203
203
  context.length = contentLength;
204
+ // The last add suffix boundary.
205
+ ranges[length - 1].suffix = suffix;
204
206
  // Return ranges.
205
207
  return ranges;
206
208
  } else {
207
209
  const [{ start, end }] = parsed;
210
+ const length = end - start + 1;
208
211
  // Set Content-Length.
209
- context.length = end - start + 1;
212
+ context.length = length;
210
213
  // Set Content-Range.
211
214
  context.set('Content-Range', `bytes ${start}-${end}/${size}`);
212
215
  // Return ranges.
213
- return parsed;
216
+ return [{ offset: start, length }];
214
217
  }
215
218
  }
216
219
  }
@@ -218,7 +221,7 @@ function parseRanges(context, stats) {
218
221
  // Set Content-Length.
219
222
  context.length = size;
220
223
  // Return ranges.
221
- return [{ start: 0, end: Math.max(size - 1) }];
224
+ return [{ offset: 0, length: size }];
222
225
  }
223
226
 
224
227
  exports.decodeURI = decodeURI;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @package koa-files
3
+ * @license MIT
4
+ * @version 3.1.0
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 buffer = require('buffer');
13
+ const stream = require('stream');
14
+
15
+ /**
16
+ * @module stream
17
+ */
18
+ class FileReadStream extends stream.Readable {
19
+ /**
20
+ * @constructor
21
+ * @param path The file path.
22
+ * @param ranges The ranges to read.
23
+ * @param options The stream options.
24
+ */
25
+ constructor(path, ranges, options) {
26
+ const { fs, encoding, highWaterMark } = options;
27
+ super({ encoding, highWaterMark });
28
+ Object.defineProperty(this, 'fd', {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: void 0
33
+ });
34
+ Object.defineProperty(this, 'fs', {
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ value: void 0
39
+ });
40
+ Object.defineProperty(this, 'path', {
41
+ enumerable: true,
42
+ configurable: true,
43
+ writable: true,
44
+ value: void 0
45
+ });
46
+ Object.defineProperty(this, 'ranges', {
47
+ enumerable: true,
48
+ configurable: true,
49
+ writable: true,
50
+ value: void 0
51
+ });
52
+ Object.defineProperty(this, 'bytesRead', {
53
+ enumerable: true,
54
+ configurable: true,
55
+ writable: true,
56
+ value: 0
57
+ });
58
+ Object.defineProperty(this, 'reading', {
59
+ enumerable: true,
60
+ configurable: true,
61
+ writable: true,
62
+ value: false
63
+ });
64
+ Object.defineProperty(this, 'currentRangeIndex', {
65
+ enumerable: true,
66
+ configurable: true,
67
+ writable: true,
68
+ value: 0
69
+ });
70
+ Object.defineProperty(this, 'readState', {
71
+ enumerable: true,
72
+ configurable: true,
73
+ writable: true,
74
+ value: 0 /* ReadState.PREFIX */
75
+ });
76
+ this.fs = fs;
77
+ this.path = path;
78
+ this.ranges = ranges;
79
+ }
80
+ /**
81
+ * @override
82
+ * @method _construct
83
+ * @param callback The callback.
84
+ */
85
+ _construct(callback) {
86
+ this.fs.open(this.path, 'r', (openError, fd) => {
87
+ if (openError === null) {
88
+ this.fd = fd;
89
+ }
90
+ callback(openError);
91
+ });
92
+ }
93
+ /**
94
+ * @private
95
+ * @method getRange
96
+ */
97
+ getRange() {
98
+ return this.ranges[this.currentRangeIndex];
99
+ }
100
+ /**
101
+ * @private
102
+ * @method getPadding
103
+ * @param range The current range.
104
+ */
105
+ getPadding(range) {
106
+ switch (this.readState) {
107
+ case 0 /* ReadState.PREFIX */:
108
+ return range.prefix;
109
+ case 2 /* ReadState.SUFFIX */:
110
+ return range.suffix;
111
+ }
112
+ }
113
+ /**
114
+ * @private
115
+ * @method readFilePadding
116
+ * @param fd The file descriptor.
117
+ * @param range The current range.
118
+ * @param size The number of bytes to read.
119
+ */
120
+ readFilePadding(fd, range, size) {
121
+ let bytesRead = 0;
122
+ const padding = this.getPadding(range);
123
+ const hasRangePadding = padding != null;
124
+ const length = hasRangePadding ? padding.length : 0;
125
+ if (hasRangePadding && length > 0) {
126
+ const begin = this.bytesRead;
127
+ bytesRead = Math.min(size, length - begin);
128
+ const end = begin + bytesRead;
129
+ this.push(padding.subarray(begin, end));
130
+ this.bytesRead = end;
131
+ }
132
+ if (!hasRangePadding || this.bytesRead >= length) {
133
+ this.bytesRead = 0;
134
+ const hasBytesRead = bytesRead > 0;
135
+ switch (this.readState) {
136
+ case 0 /* ReadState.PREFIX */:
137
+ this.readState = 1 /* ReadState.RANGE */;
138
+ if (!hasBytesRead) {
139
+ this.readFileRange(fd, range, size);
140
+ }
141
+ break;
142
+ case 2 /* ReadState.SUFFIX */:
143
+ this.currentRangeIndex++;
144
+ this.readState = 0 /* ReadState.PREFIX */;
145
+ if (!hasBytesRead) {
146
+ const range = this.getRange();
147
+ if (range == null) {
148
+ this.push(null);
149
+ } else {
150
+ this.readFilePadding(fd, range, size);
151
+ }
152
+ }
153
+ break;
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * @private
159
+ * @method readFileRange
160
+ * @param fd The file descriptor.
161
+ * @param range The current range.
162
+ * @param size The number of bytes to read.
163
+ */
164
+ readFileRange(fd, range, size) {
165
+ this.reading = true;
166
+ const { bytesRead } = this;
167
+ const position = range.offset + bytesRead;
168
+ const buffer$1 = buffer.Buffer.alloc(Math.min(size, range.length - bytesRead));
169
+ this.fs.read(fd, buffer$1, 0, buffer$1.length, position, (readError, bytesRead, buffer) => {
170
+ if (readError === null) {
171
+ if (bytesRead > 0) {
172
+ this.push(buffer.subarray(0, bytesRead));
173
+ this.bytesRead += bytesRead;
174
+ } else {
175
+ this.bytesRead = 0;
176
+ this.readState = 2 /* ReadState.SUFFIX */;
177
+ this.readFilePadding(fd, range, size);
178
+ }
179
+ } else {
180
+ this.destroy(readError);
181
+ }
182
+ this.reading = false;
183
+ });
184
+ }
185
+ /**
186
+ * @override
187
+ * @method _read
188
+ * @param size The number of bytes to read.
189
+ */
190
+ _read(size) {
191
+ if (!this.reading) {
192
+ const { fd } = this;
193
+ const range = this.getRange();
194
+ if (fd != null && range != null) {
195
+ switch (this.readState) {
196
+ case 0 /* ReadState.PREFIX */:
197
+ case 2 /* ReadState.SUFFIX */:
198
+ this.readFilePadding(fd, range, size);
199
+ break;
200
+ case 1 /* ReadState.RANGE */:
201
+ this.readFileRange(fd, range, size);
202
+ break;
203
+ default:
204
+ throw new Error(`invalid read state: ${this.readState}`);
205
+ }
206
+ } else {
207
+ this.push(null);
208
+ }
209
+ }
210
+ }
211
+ /**
212
+ * @override
213
+ * @method _destroy
214
+ * @param error The error.
215
+ * @param callback The callback.
216
+ */
217
+ _destroy(error, callback) {
218
+ const { fd } = this;
219
+ if (fd != null) {
220
+ this.fs.close(fd, closeError => {
221
+ callback(error !== null && error !== void 0 ? error : closeError);
222
+ });
223
+ } else {
224
+ callback(error);
225
+ }
226
+ }
227
+ }
228
+
229
+ exports.FileReadStream = FileReadStream;
package/esm/Service.js CHANGED
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
8
8
  */
9
9
 
10
10
  import createETag from 'etag';
11
- import destroy from 'destroy';
12
- import { PassThrough } from 'stream';
13
11
  import { stat } from './utils/fs.js';
14
12
  import { resolve, extname, join } from 'path';
13
+ import { FileReadStream } from './utils/stream.js';
15
14
  import { unixify, isOutRoot, hasTrailingSlash } from './utils/path.js';
16
15
  import { decodeURI, isConditionalGET, isPreconditionFailure, parseRanges } from './utils/http.js';
17
16
 
@@ -37,6 +36,18 @@ class Service {
37
36
  * @param options The file service options.
38
37
  */
39
38
  constructor(root, options) {
39
+ Object.defineProperty(this, 'root', {
40
+ enumerable: true,
41
+ configurable: true,
42
+ writable: true,
43
+ value: void 0
44
+ });
45
+ Object.defineProperty(this, 'options', {
46
+ enumerable: true,
47
+ configurable: true,
48
+ writable: true,
49
+ value: void 0
50
+ });
40
51
  this.options = options;
41
52
  this.root = unixify(resolve(root));
42
53
  }
@@ -100,81 +111,6 @@ class Service {
100
111
  }
101
112
  }
102
113
  }
103
- /**
104
- * @private
105
- * @method read
106
- * @description Read file with range.
107
- * @param path The file path to read.
108
- * @param range The range to read.
109
- * @param stream The destination stream.
110
- * @param end The end flag after pipe to stream.
111
- */
112
- read(path, range, stream, end) {
113
- const { fs } = this.options;
114
- return new Promise(resolve => {
115
- // Range prefix and suffix.
116
- const { prefix, suffix } = range;
117
- // Create file stream reader.
118
- const reader = fs.createReadStream(path, range);
119
- // File read stream open.
120
- if (prefix) {
121
- reader.once('open', () => {
122
- // Write prefix boundary.
123
- stream.write(prefix);
124
- });
125
- }
126
- // File read stream end.
127
- if (suffix) {
128
- reader.once('end', () => {
129
- // Push suffix boundary.
130
- stream.write(suffix);
131
- });
132
- }
133
- // File read stream error.
134
- reader.once('error', () => {
135
- // End stream.
136
- stream.end();
137
- // Unpipe.
138
- reader.unpipe();
139
- // Destroy.
140
- destroy(reader);
141
- // Resolve.
142
- resolve(false);
143
- });
144
- // File read stream close.
145
- reader.once('close', () => {
146
- // Unpipe.
147
- reader.unpipe();
148
- // Destroy.
149
- destroy(reader);
150
- // Resolve.
151
- resolve(true);
152
- });
153
- // Write data to buffer.
154
- reader.pipe(stream, { end });
155
- });
156
- }
157
- /**
158
- * @private
159
- * @method send
160
- * @description Send file with ranges.
161
- * @param path The file path to send.
162
- * @param ranges The ranges to send.
163
- * @param stream The destination stream.
164
- */
165
- async send(path, ranges, stream) {
166
- // Ranges length.
167
- let { length } = ranges;
168
- // Write ranges to stream.
169
- for (const range of ranges) {
170
- // Write range.
171
- const passed = await this.read(path, range, stream, --length === 0);
172
- // If not passed, break.
173
- if (!passed) {
174
- break;
175
- }
176
- }
177
- }
178
114
  /**
179
115
  * @public
180
116
  * @method respond
@@ -255,14 +191,10 @@ class Service {
255
191
  if (ranges === -2) {
256
192
  return context.throw(400);
257
193
  }
258
- // Set stream body, highWaterMark 64kb.
259
- const stream = new PassThrough({
260
- highWaterMark: 65536
261
- });
262
- // Send file with ranges.
263
- this.send(path, ranges, stream);
194
+ // Ranges length.
195
+ const { fs, highWaterMark } = this.options;
264
196
  // Set response body.
265
- context.body = stream;
197
+ context.body = new FileReadStream(path, ranges, { fs, highWaterMark });
266
198
  // File found.
267
199
  return true;
268
200
  }
package/esm/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
package/esm/utils/fs.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
package/esm/utils/hash.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
package/esm/utils/http.js CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
8
8
  */
9
9
 
10
+ import { Buffer } from 'buffer';
10
11
  import { generate } from './hash.js';
11
12
  import parseRange from 'range-parser';
12
13
 
@@ -167,42 +168,44 @@ function parseRanges(context, stats) {
167
168
  let contentLength = 0;
168
169
  // Ranges.
169
170
  const ranges = [];
171
+ // Parsed entries.
172
+ const entries = parsed.entries();
170
173
  // Range boundary.
171
174
  const boundary = `<${generate()}>`;
172
- // Range suffix.
173
- const suffix = `\r\n--${boundary}--\r\n`;
174
175
  // Multipart Content-Type.
175
176
  const contentType = `Content-Type: ${context.type}`;
177
+ // Range suffix.
178
+ const suffix = Buffer.from(`\r\n--${boundary}--\r\n`);
176
179
  // Override Content-Type.
177
180
  context.type = `multipart/byteranges; boundary=${boundary}`;
178
181
  // Map ranges.
179
- for (let index = 0; index < length; index++) {
180
- const { start, end } = parsed[index];
181
- // The first prefix boundary no \r\n.
182
+ for (const [index, { start, end }] of entries) {
183
+ const length = end - start + 1;
182
184
  const head = index > 0 ? '\r\n' : '';
183
185
  const contentRange = `Content-Range: bytes ${start}-${end}/${size}`;
184
- const prefix = `${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`;
186
+ const prefix = Buffer.from(`${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`);
185
187
  // Compute Content-Length
186
- contentLength += end - start + 1 + Buffer.byteLength(prefix);
188
+ contentLength += length + prefix.length;
187
189
  // Cache range.
188
- ranges.push({ start, end, prefix });
190
+ ranges.push({ offset: start, length, prefix });
189
191
  }
190
- // The last add suffix boundary.
191
- ranges[length - 1].suffix = suffix;
192
192
  // Compute Content-Length.
193
- contentLength += Buffer.byteLength(suffix);
193
+ contentLength += suffix.length;
194
194
  // Set Content-Length.
195
195
  context.length = contentLength;
196
+ // The last add suffix boundary.
197
+ ranges[length - 1].suffix = suffix;
196
198
  // Return ranges.
197
199
  return ranges;
198
200
  } else {
199
201
  const [{ start, end }] = parsed;
202
+ const length = end - start + 1;
200
203
  // Set Content-Length.
201
- context.length = end - start + 1;
204
+ context.length = length;
202
205
  // Set Content-Range.
203
206
  context.set('Content-Range', `bytes ${start}-${end}/${size}`);
204
207
  // Return ranges.
205
- return parsed;
208
+ return [{ offset: start, length }];
206
209
  }
207
210
  }
208
211
  }
@@ -210,7 +213,7 @@ function parseRanges(context, stats) {
210
213
  // Set Content-Length.
211
214
  context.length = size;
212
215
  // Return ranges.
213
- return [{ start: 0, end: Math.max(size - 1) }];
216
+ return [{ offset: 0, length: size }];
214
217
  }
215
218
 
216
219
  export { decodeURI, isConditionalGET, isPreconditionFailure, parseRanges };
package/esm/utils/path.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @package koa-files
3
3
  * @license MIT
4
- * @version 3.0.7
4
+ * @version 3.1.0
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
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @package koa-files
3
+ * @license MIT
4
+ * @version 3.1.0
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 { Buffer } from 'buffer';
11
+ import { Readable } from 'stream';
12
+
13
+ /**
14
+ * @module stream
15
+ */
16
+ class FileReadStream extends Readable {
17
+ /**
18
+ * @constructor
19
+ * @param path The file path.
20
+ * @param ranges The ranges to read.
21
+ * @param options The stream options.
22
+ */
23
+ constructor(path, ranges, options) {
24
+ const { fs, encoding, highWaterMark } = options;
25
+ super({ encoding, highWaterMark });
26
+ Object.defineProperty(this, 'fd', {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: void 0
31
+ });
32
+ Object.defineProperty(this, 'fs', {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: void 0
37
+ });
38
+ Object.defineProperty(this, 'path', {
39
+ enumerable: true,
40
+ configurable: true,
41
+ writable: true,
42
+ value: void 0
43
+ });
44
+ Object.defineProperty(this, 'ranges', {
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ value: void 0
49
+ });
50
+ Object.defineProperty(this, 'bytesRead', {
51
+ enumerable: true,
52
+ configurable: true,
53
+ writable: true,
54
+ value: 0
55
+ });
56
+ Object.defineProperty(this, 'reading', {
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ value: false
61
+ });
62
+ Object.defineProperty(this, 'currentRangeIndex', {
63
+ enumerable: true,
64
+ configurable: true,
65
+ writable: true,
66
+ value: 0
67
+ });
68
+ Object.defineProperty(this, 'readState', {
69
+ enumerable: true,
70
+ configurable: true,
71
+ writable: true,
72
+ value: 0 /* ReadState.PREFIX */
73
+ });
74
+ this.fs = fs;
75
+ this.path = path;
76
+ this.ranges = ranges;
77
+ }
78
+ /**
79
+ * @override
80
+ * @method _construct
81
+ * @param callback The callback.
82
+ */
83
+ _construct(callback) {
84
+ this.fs.open(this.path, 'r', (openError, fd) => {
85
+ if (openError === null) {
86
+ this.fd = fd;
87
+ }
88
+ callback(openError);
89
+ });
90
+ }
91
+ /**
92
+ * @private
93
+ * @method getRange
94
+ */
95
+ getRange() {
96
+ return this.ranges[this.currentRangeIndex];
97
+ }
98
+ /**
99
+ * @private
100
+ * @method getPadding
101
+ * @param range The current range.
102
+ */
103
+ getPadding(range) {
104
+ switch (this.readState) {
105
+ case 0 /* ReadState.PREFIX */:
106
+ return range.prefix;
107
+ case 2 /* ReadState.SUFFIX */:
108
+ return range.suffix;
109
+ }
110
+ }
111
+ /**
112
+ * @private
113
+ * @method readFilePadding
114
+ * @param fd The file descriptor.
115
+ * @param range The current range.
116
+ * @param size The number of bytes to read.
117
+ */
118
+ readFilePadding(fd, range, size) {
119
+ let bytesRead = 0;
120
+ const padding = this.getPadding(range);
121
+ const hasRangePadding = padding != null;
122
+ const length = hasRangePadding ? padding.length : 0;
123
+ if (hasRangePadding && length > 0) {
124
+ const begin = this.bytesRead;
125
+ bytesRead = Math.min(size, length - begin);
126
+ const end = begin + bytesRead;
127
+ this.push(padding.subarray(begin, end));
128
+ this.bytesRead = end;
129
+ }
130
+ if (!hasRangePadding || this.bytesRead >= length) {
131
+ this.bytesRead = 0;
132
+ const hasBytesRead = bytesRead > 0;
133
+ switch (this.readState) {
134
+ case 0 /* ReadState.PREFIX */:
135
+ this.readState = 1 /* ReadState.RANGE */;
136
+ if (!hasBytesRead) {
137
+ this.readFileRange(fd, range, size);
138
+ }
139
+ break;
140
+ case 2 /* ReadState.SUFFIX */:
141
+ this.currentRangeIndex++;
142
+ this.readState = 0 /* ReadState.PREFIX */;
143
+ if (!hasBytesRead) {
144
+ const range = this.getRange();
145
+ if (range == null) {
146
+ this.push(null);
147
+ } else {
148
+ this.readFilePadding(fd, range, size);
149
+ }
150
+ }
151
+ break;
152
+ }
153
+ }
154
+ }
155
+ /**
156
+ * @private
157
+ * @method readFileRange
158
+ * @param fd The file descriptor.
159
+ * @param range The current range.
160
+ * @param size The number of bytes to read.
161
+ */
162
+ readFileRange(fd, range, size) {
163
+ this.reading = true;
164
+ const { bytesRead } = this;
165
+ const position = range.offset + bytesRead;
166
+ const buffer = Buffer.alloc(Math.min(size, range.length - bytesRead));
167
+ this.fs.read(fd, buffer, 0, buffer.length, position, (readError, bytesRead, buffer) => {
168
+ if (readError === null) {
169
+ if (bytesRead > 0) {
170
+ this.push(buffer.subarray(0, bytesRead));
171
+ this.bytesRead += bytesRead;
172
+ } else {
173
+ this.bytesRead = 0;
174
+ this.readState = 2 /* ReadState.SUFFIX */;
175
+ this.readFilePadding(fd, range, size);
176
+ }
177
+ } else {
178
+ this.destroy(readError);
179
+ }
180
+ this.reading = false;
181
+ });
182
+ }
183
+ /**
184
+ * @override
185
+ * @method _read
186
+ * @param size The number of bytes to read.
187
+ */
188
+ _read(size) {
189
+ if (!this.reading) {
190
+ const { fd } = this;
191
+ const range = this.getRange();
192
+ if (fd != null && range != null) {
193
+ switch (this.readState) {
194
+ case 0 /* ReadState.PREFIX */:
195
+ case 2 /* ReadState.SUFFIX */:
196
+ this.readFilePadding(fd, range, size);
197
+ break;
198
+ case 1 /* ReadState.RANGE */:
199
+ this.readFileRange(fd, range, size);
200
+ break;
201
+ default:
202
+ throw new Error(`invalid read state: ${this.readState}`);
203
+ }
204
+ } else {
205
+ this.push(null);
206
+ }
207
+ }
208
+ }
209
+ /**
210
+ * @override
211
+ * @method _destroy
212
+ * @param error The error.
213
+ * @param callback The callback.
214
+ */
215
+ _destroy(error, callback) {
216
+ const { fd } = this;
217
+ if (fd != null) {
218
+ this.fs.close(fd, closeError => {
219
+ callback(error !== null && error !== void 0 ? error : closeError);
220
+ });
221
+ } else {
222
+ callback(error);
223
+ }
224
+ }
225
+ }
226
+
227
+ export { FileReadStream };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koa-files",
3
- "version": "3.0.7",
3
+ "version": "3.1.0",
4
4
  "description": "A static files serving middleware for koa.",
5
5
  "type": "module",
6
6
  "main": "cjs/index.cjs",
@@ -54,7 +54,6 @@
54
54
  "build:types": "tsc --declaration --emitDeclarationOnly --declarationDir types && node tools/fix-types.js"
55
55
  },
56
56
  "dependencies": {
57
- "destroy": "^1.2.0",
58
57
  "etag": "^1.8.1",
59
58
  "range-parser": "^1.2.1",
60
59
  "tslib": "^2.7.0"
@@ -18,6 +18,7 @@ export interface Options {
18
18
  etag?: boolean;
19
19
  acceptRanges?: boolean;
20
20
  lastModified?: boolean;
21
+ highWaterMark?: number;
21
22
  ignore?: IgnoreFunction;
22
23
  headers?: Headers | HeaderFunction;
23
24
  }
@@ -50,25 +51,6 @@ export default class Service {
50
51
  * @param stats The file stats.
51
52
  */
52
53
  private setupHeaders;
53
- /**
54
- * @private
55
- * @method read
56
- * @description Read file with range.
57
- * @param path The file path to read.
58
- * @param range The range to read.
59
- * @param stream The destination stream.
60
- * @param end The end flag after pipe to stream.
61
- */
62
- private read;
63
- /**
64
- * @private
65
- * @method send
66
- * @description Send file with ranges.
67
- * @param path The file path to send.
68
- * @param ranges The ranges to send.
69
- * @param stream The destination stream.
70
- */
71
- private send;
72
54
  /**
73
55
  * @public
74
56
  * @method respond
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * @module fs
3
3
  */
4
- import { ReadStream, Stats } from 'fs';
5
- type PathLike = string | Buffer | URL;
4
+ import fs, { Stats } from 'fs';
5
+ export interface FileSystem {
6
+ stat: typeof fs.stat;
7
+ open: typeof fs.open;
8
+ read: typeof fs.read;
9
+ close: typeof fs.close;
10
+ }
6
11
  /**
7
12
  * @function stat
8
13
  * @description Get file stats.
@@ -10,14 +15,3 @@ type PathLike = string | Buffer | URL;
10
15
  * @param path The file path.
11
16
  */
12
17
  export declare function stat(fs: FileSystem, path: string): Promise<Stats | null>;
13
- export interface FileSystem {
14
- stat(path: PathLike, callback: (error: Error | null, stats: Stats) => void): void;
15
- createReadStream(
16
- path: PathLike,
17
- options?: {
18
- start?: number;
19
- end?: number;
20
- }
21
- ): ReadStream;
22
- }
23
- export {};
@@ -3,12 +3,7 @@
3
3
  */
4
4
  import { Stats } from 'fs';
5
5
  import { Context } from 'koa';
6
- export interface Range {
7
- start: number;
8
- end?: number;
9
- prefix?: string;
10
- suffix?: string;
11
- }
6
+ import { Range } from './stream';
12
7
  type Ranges = Range[] | -1 | -2;
13
8
  /**
14
9
  * @function decodeURI
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @module stream
3
+ */
4
+ import { PathLike } from 'fs';
5
+ import { Buffer } from 'buffer';
6
+ import { FileSystem } from './fs';
7
+ import { Readable, ReadableOptions } from 'stream';
8
+ export interface Options extends Pick<ReadableOptions, 'encoding' | 'highWaterMark'> {
9
+ fs: FileSystem;
10
+ }
11
+ export interface Range {
12
+ offset: number;
13
+ length: number;
14
+ prefix?: Buffer;
15
+ suffix?: Buffer;
16
+ }
17
+ interface Callback {
18
+ (error?: Error | null): void;
19
+ }
20
+ export declare class FileReadStream extends Readable {
21
+ private fd?;
22
+ private fs;
23
+ private path;
24
+ private ranges;
25
+ private bytesRead;
26
+ private reading;
27
+ private currentRangeIndex;
28
+ private readState;
29
+ /**
30
+ * @constructor
31
+ * @param path The file path.
32
+ * @param ranges The ranges to read.
33
+ * @param options The stream options.
34
+ */
35
+ constructor(path: PathLike, ranges: Range[], options: Options);
36
+ /**
37
+ * @override
38
+ * @method _construct
39
+ * @param callback The callback.
40
+ */
41
+ _construct(callback: Callback): void;
42
+ /**
43
+ * @private
44
+ * @method getRange
45
+ */
46
+ private getRange;
47
+ /**
48
+ * @private
49
+ * @method getPadding
50
+ * @param range The current range.
51
+ */
52
+ private getPadding;
53
+ /**
54
+ * @private
55
+ * @method readFilePadding
56
+ * @param fd The file descriptor.
57
+ * @param range The current range.
58
+ * @param size The number of bytes to read.
59
+ */
60
+ private readFilePadding;
61
+ /**
62
+ * @private
63
+ * @method readFileRange
64
+ * @param fd The file descriptor.
65
+ * @param range The current range.
66
+ * @param size The number of bytes to read.
67
+ */
68
+ private readFileRange;
69
+ /**
70
+ * @override
71
+ * @method _read
72
+ * @param size The number of bytes to read.
73
+ */
74
+ _read(size: number): void;
75
+ /**
76
+ * @override
77
+ * @method _destroy
78
+ * @param error The error.
79
+ * @param callback The callback.
80
+ */
81
+ _destroy(error: Error | null, callback: Callback): void;
82
+ }
83
+ export {};