formstream 1.3.1 → 1.5.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2012 - 2014 fengmk2 <fengmk2@gmail.com>
4
+ Copyright (c) 2014-present node-modules and other contributors.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # formstream
2
2
 
3
3
  [![NPM version][npm-image]][npm-url]
4
- [![Test coverage][coveralls-image]][coveralls-url]
4
+ [![CI](https://github.com/node-modules/formstream/actions/workflows/ci.yml/badge.svg)](https://github.com/node-modules/formstream/actions/workflows/ci.yml)
5
+ [![Test coverage][codecov-image]][codecov-url]
5
6
  [![npm download][download-image]][download-url]
6
7
 
7
8
  [npm-image]: https://img.shields.io/npm/v/formstream.svg?style=flat-square
8
9
  [npm-url]: https://npmjs.org/package/formstream
9
- [coveralls-image]: https://img.shields.io/coveralls/node-modules/formstream.svg?style=flat-square
10
- [coveralls-url]: https://coveralls.io/r/node-modules/formstream?branch=master
10
+ [codecov-image]: https://codecov.io/github/node-modules/formstream/coverage.svg?branch=master
11
+ [codecov-url]: https://codecov.io/github/node-modules/formstream?branch=master
11
12
  [download-image]: https://img.shields.io/npm/dm/formstream.svg?style=flat-square
12
13
  [download-url]: https://npmjs.org/package/formstream
13
14
 
@@ -16,7 +17,7 @@ A [multipart/form-data](http://tools.ietf.org/html/rfc2388) encoded stream, help
16
17
  ## Install
17
18
 
18
19
  ```bash
19
- $ npm install formstream
20
+ npm install formstream
20
21
  ```
21
22
 
22
23
  ## Quick Start
@@ -61,19 +62,44 @@ var formstream = require('formstream');
61
62
 
62
63
  var filepath = './logo.png';
63
64
  fs.stat(filepath, function (err, stat) {
64
- formstream().field('status', 'share picture')
65
- .field('access_token', 'your access token')
66
- .file('pic', filepath, 'logo.png', stat.size)
67
- .pipe(process.stdout); // your request stream
65
+ formstream()
66
+ .field('status', 'share picture')
67
+ .field('access_token', 'your access token')
68
+ .file('pic', filepath, 'logo.png', stat.size)
69
+ .pipe(process.stdout); // your request stream
70
+ });
71
+ ```
72
+
73
+ ### Set min chunk buffer size
74
+
75
+ Some web servers have a limit on the number of chunks, and you can set `minChunkSize` to ensure the size of chunk sent to the server.
76
+
77
+ ```js
78
+ var fs = require('fs');
79
+ var FormStream = require('formstream');
80
+
81
+ var filepath = './big-file.zip';
82
+ fs.stat(filepath, function (err, stat) {
83
+ new FormStream({
84
+ // send >= 2MB chunk buffer size to the server
85
+ minChunkSize: 1024 * 1024 * 2,
86
+ }).field('status', 'share file')
87
+ .field('access_token', 'your access token')
88
+ .file('file', filepath, 'big-file.zip', stat.size)
89
+ .pipe(process.stdout); // your request stream
68
90
  });
69
91
  ```
70
92
 
71
93
  ## API Doc
72
94
 
73
- ### formstream()
95
+ ### formstream([options])
74
96
 
75
97
  Create a form instance.
76
98
 
99
+ #### Arguments
100
+
101
+ - **options.minChunkSize** Number - min chunk size to emit data event
102
+
77
103
  #### Returns
78
104
 
79
105
  Form - form instance
@@ -143,7 +169,7 @@ Get headers for the request.
143
169
 
144
170
  #### Arguments
145
171
 
146
- - ***headers*** Object - Additional headers
172
+ - **headers** Object - Additional headers
147
173
 
148
174
  #### Example
149
175
 
@@ -176,26 +202,16 @@ See [Node.js Documentation](http://nodejs.org/api/stream.html#stream_event_end)
176
202
 
177
203
  ## License
178
204
 
179
- (The MIT License)
180
-
181
- Copyright (c) 2012 - 2014 fengmk2 &lt;fengmk2@gmail.com&gt;
182
- Copyright(c) node-modules and other contributors.
183
-
184
- Permission is hereby granted, free of charge, to any person obtaining
185
- a copy of this software and associated documentation files (the
186
- 'Software'), to deal in the Software without restriction, including
187
- without limitation the rights to use, copy, modify, merge, publish,
188
- distribute, sublicense, and/or sell copies of the Software, and to
189
- permit persons to whom the Software is furnished to do so, subject to
190
- the following conditions:
191
-
192
- The above copyright notice and this permission notice shall be
193
- included in all copies or substantial portions of the Software.
194
-
195
- THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
196
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
197
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
198
- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
199
- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
200
- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
201
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
205
+ [MIT](LICENSE)
206
+
207
+ <!-- GITCONTRIBUTOR_START -->
208
+
209
+ ## Contributors
210
+
211
+ |[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/288288?v=4" width="100px;"/><br/><sub><b>xingrz</b></sub>](https://github.com/xingrz)<br/>|[<img src="https://avatars.githubusercontent.com/u/32174276?v=4" width="100px;"/><br/><sub><b>semantic-release-bot</b></sub>](https://github.com/semantic-release-bot)<br/>|[<img src="https://avatars.githubusercontent.com/u/13151189?v=4" width="100px;"/><br/><sub><b>fjc0k</b></sub>](https://github.com/fjc0k)<br/>|[<img src="https://avatars.githubusercontent.com/u/18096247?v=4" width="100px;"/><br/><sub><b>mrspeiser</b></sub>](https://github.com/mrspeiser)<br/>|[<img src="https://avatars.githubusercontent.com/u/985607?v=4" width="100px;"/><br/><sub><b>dead-horse</b></sub>](https://github.com/dead-horse)<br/>|
212
+ | :---: | :---: | :---: | :---: | :---: | :---: |
213
+ [<img src="https://avatars.githubusercontent.com/u/7326406?v=4" width="100px;"/><br/><sub><b>shaozj</b></sub>](https://github.com/shaozj)<br/>
214
+
215
+ This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Wed May 15 2024 00:34:12 GMT+0800`.
216
+
217
+ <!-- GITCONTRIBUTOR_END -->
package/lib/formstream.js CHANGED
@@ -1,28 +1,39 @@
1
1
  /**
2
- * Data format:
2
+ * Form Data format:
3
3
  *
4
4
 
5
+ ```txt
5
6
  --FormStreamBoundary1349886663601\r\n
6
7
  Content-Disposition: form-data; name="foo"\r\n
7
8
  \r\n
8
9
  <FIELD-CONTENT>\r\n
9
10
  --FormStreamBoundary1349886663601\r\n
11
+ Content-Disposition: form-data; name="data"\r\n
12
+ Content-Type: application/json\r\n
13
+ \r\n
14
+ <JSON-FORMAT-CONTENT>\r\n
15
+ --FormStreamBoundary1349886663601\r\n
10
16
  Content-Disposition: form-data; name="file"; filename="formstream.test.js"\r\n
11
17
  Content-Type: application/javascript\r\n
12
18
  \r\n
13
- <FILE-CONTENT>\r\n
19
+ <FILE-CONTENT-CHUNK-1>
20
+ ...
21
+ <FILE-CONTENT-CHUNK-N>
22
+ \r\n
14
23
  --FormStreamBoundary1349886663601\r\n
15
24
  Content-Disposition: form-data; name="pic"; filename="fawave.png"\r\n
16
25
  Content-Type: image/png\r\n
17
26
  \r\n
18
27
  <IMAGE-CONTENT>\r\n
19
28
  --FormStreamBoundary1349886663601--
29
+ ```
20
30
 
21
31
  *
22
32
  */
23
33
 
24
34
  'use strict';
25
35
 
36
+ var debug = require('util').debuglog('formstream');
26
37
  var Stream = require('stream');
27
38
  var parseStream = require('pause-stream');
28
39
  var util = require('util');
@@ -30,14 +41,15 @@ var mime = require('mime');
30
41
  var path = require('path');
31
42
  var fs = require('fs');
32
43
  var destroy = require('destroy');
44
+ var hex = require('node-hex');
33
45
 
34
46
  var PADDING = '--';
35
47
  var NEW_LINE = '\r\n';
36
48
  var NEW_LINE_BUFFER = Buffer.from(NEW_LINE);
37
49
 
38
- function FormStream() {
50
+ function FormStream(options) {
39
51
  if (!(this instanceof FormStream)) {
40
- return new FormStream();
52
+ return new FormStream(options);
41
53
  }
42
54
 
43
55
  FormStream.super_.call(this);
@@ -49,6 +61,10 @@ function FormStream() {
49
61
  this._contentLength = 0;
50
62
  this._isAllStreamSizeKnown = true;
51
63
  this._knownStreamSize = 0;
64
+ this._minChunkSize = options && options.minChunkSize || 0;
65
+
66
+ this.isFormStream = true;
67
+ debug('start boundary\n%s', this._boundary);
52
68
  }
53
69
 
54
70
  util.inherits(FormStream, Stream);
@@ -76,12 +92,12 @@ FormStream.prototype.setTotalStreamSize = function (size) {
76
92
 
77
93
  for (var i = 0; i < this._streams.length; i++) {
78
94
  size += this._streams[i][0].length;
79
- size += NEW_LINE_BUFFER.length; // stream field end pedding size
95
+ size += NEW_LINE_BUFFER.length; // stream field end padding size
80
96
  }
81
97
 
82
98
  this._knownStreamSize = size;
83
99
  this._isAllStreamSizeKnown = true;
84
-
100
+ debug('set total size: %s', size);
85
101
  return this;
86
102
  };
87
103
 
@@ -92,7 +108,6 @@ FormStream.prototype.headers = function (options) {
92
108
 
93
109
  // calculate total stream size
94
110
  this._contentLength += this._knownStreamSize;
95
-
96
111
  // calculate length of end padding
97
112
  this._contentLength += this._endData.length;
98
113
 
@@ -106,19 +121,20 @@ FormStream.prototype.headers = function (options) {
106
121
  }
107
122
  }
108
123
 
124
+ debug('headers: %j', headers);
109
125
  return headers;
110
126
  };
111
127
 
112
128
  FormStream.prototype.file = function (name, filepath, filename, filesize) {
113
- var mimeType = mime.getType(filepath);
114
-
115
129
  if (typeof filename === 'number' && !filesize) {
116
130
  filesize = filename;
117
131
  filename = path.basename(filepath);
118
- } else if (!filename) {
132
+ }
133
+ if (!filename) {
119
134
  filename = path.basename(filepath);
120
135
  }
121
136
 
137
+ var mimeType = mime.getType(filename);
122
138
  var stream = fs.createReadStream(filepath);
123
139
 
124
140
  return this.stream(name, stream, filename, mimeType, filesize);
@@ -128,9 +144,10 @@ FormStream.prototype.file = function (name, filepath, filename, filesize) {
128
144
  * Add a form field
129
145
  * @param {String} name field name
130
146
  * @param {String|Buffer} value field value
147
+ * @param {String} [mimeType] field mimeType
131
148
  * @return {this}
132
149
  */
133
- FormStream.prototype.field = function (name, value) {
150
+ FormStream.prototype.field = function (name, value, mimeType) {
134
151
  if (!Buffer.isBuffer(value)) {
135
152
  // field(String, Number)
136
153
  // https://github.com/qiniu/nodejs-sdk/issues/123
@@ -139,7 +156,7 @@ FormStream.prototype.field = function (name, value) {
139
156
  }
140
157
  value = Buffer.from(value);
141
158
  }
142
- return this.buffer(name, value);
159
+ return this.buffer(name, value, null, mimeType);
143
160
  };
144
161
 
145
162
  FormStream.prototype.stream = function (name, stream, filename, mimeType, size) {
@@ -190,38 +207,37 @@ FormStream.prototype.buffer = function (name, buffer, filename, mimeType) {
190
207
 
191
208
  var leading = this._leading(disposition, mimeType);
192
209
 
193
- this._buffers.push([leading, buffer]);
194
-
195
210
  // plus buffer length to total content-length
196
- this._contentLength += leading.length;
197
- this._contentLength += buffer.length;
198
- this._contentLength += NEW_LINE_BUFFER.length;
211
+ var bufferSize = leading.length + buffer.length + NEW_LINE_BUFFER.length;
212
+ this._buffers.push(Buffer.concat([leading, buffer, NEW_LINE_BUFFER], bufferSize));
213
+ this._contentLength += bufferSize;
199
214
 
200
215
  process.nextTick(this.resume.bind(this));
201
-
216
+ if (debug.enabled) {
217
+ debug('new buffer field, content size: %d\n%s%s',
218
+ buffer.length, leading.toString(), hex(buffer));
219
+ }
202
220
  return this;
203
221
  };
204
222
 
205
223
  FormStream.prototype._leading = function (disposition, type) {
206
224
  var leading = [PADDING + this._boundary];
207
225
 
208
- var disps = [];
226
+ var dispositions = [];
209
227
 
210
228
  if (disposition) {
211
229
  for (var k in disposition) {
212
- disps.push(k + '="' + disposition[k] + '"');
230
+ dispositions.push(k + '="' + disposition[k] + '"');
213
231
  }
214
232
  }
215
233
 
216
- leading.push('Content-Disposition: form-data; ' + disps.join('; '));
217
-
234
+ leading.push('Content-Disposition: form-data; ' + dispositions.join('; '));
218
235
  if (type) {
219
236
  leading.push('Content-Type: ' + type);
220
237
  }
221
238
 
222
239
  leading.push('');
223
240
  leading.push('');
224
-
225
241
  return Buffer.from(leading.join(NEW_LINE));
226
242
  };
227
243
 
@@ -231,26 +247,101 @@ FormStream.prototype._emitBuffers = function () {
231
247
  }
232
248
 
233
249
  for (var i = 0; i < this._buffers.length; i++) {
234
- var item = this._buffers[i];
235
- this.emit('data', item[0]); // part leading
236
- this.emit('data', item[1]); // part content
237
- this.emit('data', NEW_LINE_BUFFER);
250
+ this.emit('data', this._buffers[i]);
238
251
  }
239
-
240
252
  this._buffers = [];
241
253
  };
242
254
 
243
255
  FormStream.prototype._emitStream = function (item) {
244
256
  var self = this;
245
- // item: [ fieldData, stream ]
246
- self.emit('data', item[0]);
257
+ // item: [ leading, stream ]
258
+ var streamSize = 0;
259
+ var chunkCount = 0;
260
+ const leading = item[0];
261
+ self.emit('data', leading);
262
+ chunkCount++;
263
+ if (debug.enabled) {
264
+ debug('new stream, chunk index %d\n%s', chunkCount, leading.toString());
265
+ }
247
266
 
248
267
  var stream = item[1];
249
268
  stream.on('data', function (data) {
250
269
  self.emit('data', data);
270
+ streamSize += leading.length;
271
+ chunkCount++;
272
+ if (debug.enabled) {
273
+ if (data.length > 512) {
274
+ debug('stream chunk, size %d, chunk index %d, stream size %d\n%s...... only show 512 bytes ......',
275
+ data.length, chunkCount, streamSize, hex(data.slice(0, 512)));
276
+ } else {
277
+ debug('stream chunk, size %d, chunk index %d, stream size %d\n%s',
278
+ data.length, chunkCount, streamSize, hex(data));
279
+ }
280
+ }
251
281
  });
252
282
  stream.on('end', function () {
253
283
  self.emit('data', NEW_LINE_BUFFER);
284
+ chunkCount++;
285
+ debug('stream end, chunk index %d, stream size %d', chunkCount, streamSize);
286
+ return process.nextTick(self.drain.bind(self));
287
+ });
288
+ stream.resume();
289
+ };
290
+
291
+ FormStream.prototype._emitStreamWithChunkSize = function (item, minChunkSize) {
292
+ var self = this;
293
+ // item: [ leading, stream ]
294
+ var streamSize = 0;
295
+ var chunkCount = 0;
296
+ var bufferSize = 0;
297
+ var buffers = [];
298
+ const leading = item[0];
299
+ buffers.push(leading);
300
+ bufferSize += leading.length;
301
+ if (debug.enabled) {
302
+ debug('new stream, with min chunk size: %d\n%s', minChunkSize, leading.toString());
303
+ }
304
+
305
+ var stream = item[1];
306
+ stream.on('data', function (data) {
307
+ if (typeof data === 'string') {
308
+ data = Buffer.from(data, 'utf-8');
309
+ }
310
+ buffers.push(data);
311
+ bufferSize += data.length;
312
+ streamSize += data.length;
313
+ debug('got stream data size %d, buffer size %d, stream size %d',
314
+ data.length, bufferSize, streamSize);
315
+ if (bufferSize >= minChunkSize) {
316
+ const chunk = Buffer.concat(buffers, bufferSize);
317
+ buffers = [];
318
+ bufferSize = 0;
319
+ self.emit('data', chunk);
320
+ chunkCount++;
321
+ if (debug.enabled) {
322
+ if (chunk.length > 512) {
323
+ debug('stream chunk, size %d, chunk index %d, stream size %d\n%s...... only show 512 bytes ......',
324
+ chunk.length, chunkCount, streamSize, hex(chunk.slice(0, 512)));
325
+ } else {
326
+ debug('stream chunk, size %d, chunk index %d, stream size %d\n%s',
327
+ chunk.length, chunkCount, streamSize, hex(chunk));
328
+ }
329
+ }
330
+ }
331
+ });
332
+ stream.on('end', function () {
333
+ buffers.push(NEW_LINE_BUFFER);
334
+ bufferSize += NEW_LINE_BUFFER.length;
335
+ const chunk = Buffer.concat(buffers, bufferSize);
336
+ self.emit('data', chunk);
337
+ chunkCount++;
338
+ if (chunk.length > 512) {
339
+ debug('stream end, size %d, chunk index %d, stream size %d\n%s...... only show 512 bytes ......',
340
+ chunk.length, chunkCount, streamSize, hex(chunk.slice(0, 512)));
341
+ } else {
342
+ debug('stream end, size %d, chunk index %d, stream size %d\n%s',
343
+ chunk.length, chunkCount, streamSize, hex(chunk));
344
+ }
254
345
  return process.nextTick(self.drain.bind(self));
255
346
  });
256
347
  stream.resume();
@@ -262,14 +353,22 @@ FormStream.prototype._emitEnd = function () {
262
353
  // --{boundary}--\r\n
263
354
  this.emit('data', this._endData);
264
355
  this.emit('end');
356
+ if (debug.enabled) {
357
+ debug('end boundary\n%s', this._endData.toString());
358
+ }
265
359
  };
266
360
 
267
361
  FormStream.prototype.drain = function () {
362
+ // debug('drain');
268
363
  this._emitBuffers();
269
364
 
270
365
  var item = this._streams.shift();
271
366
  if (item) {
272
- this._emitStream(item);
367
+ if (this._minChunkSize && this._minChunkSize > 0) {
368
+ this._emitStreamWithChunkSize(item, this._minChunkSize);
369
+ } else {
370
+ this._emitStream(item);
371
+ }
273
372
  } else {
274
373
  this._emitEnd();
275
374
  }
@@ -278,6 +377,7 @@ FormStream.prototype.drain = function () {
278
377
  };
279
378
 
280
379
  FormStream.prototype.resume = function () {
380
+ // debug('resume');
281
381
  this.paused = false;
282
382
 
283
383
  if (!this._draining) {
@@ -290,4 +390,5 @@ FormStream.prototype.resume = function () {
290
390
 
291
391
  FormStream.prototype.close = FormStream.prototype.destroy = function () {
292
392
  this.emit('destroy');
393
+ // debug('destroy or close');
293
394
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formstream",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "A multipart/form-data encoded stream, helper for file upload.",
5
5
  "main": "lib/formstream.js",
6
6
  "types": "types/formstream.d.ts",
@@ -11,9 +11,10 @@
11
11
  "scripts": {
12
12
  "test": "egg-bin test",
13
13
  "cov": "egg-bin cov",
14
- "ci": "npm run lint && npm run tsd && npm run cov",
14
+ "ci": "npm run lint && npm run tsd && npm run cov && NODE_DEBUG=formstream npm run cov",
15
15
  "lint": "jshint .",
16
- "tsd": "tsd"
16
+ "tsd": "tsd",
17
+ "contributor": "git-contributor"
17
18
  },
18
19
  "repository": {
19
20
  "type": "git",
@@ -31,6 +32,7 @@
31
32
  "dependencies": {
32
33
  "destroy": "^1.0.4",
33
34
  "mime": "^2.5.2",
35
+ "node-hex": "^1.0.1",
34
36
  "pause-stream": "~0.0.11"
35
37
  },
36
38
  "devDependencies": {
@@ -38,6 +40,7 @@
38
40
  "connect-multiparty": "1",
39
41
  "egg-bin": "^5.6.1",
40
42
  "express": "^4.16.4",
43
+ "git-contributor": "^2.1.5",
41
44
  "jshint": "^2.13.6",
42
45
  "pedding": "1",
43
46
  "should": "4",
@@ -7,7 +7,7 @@ declare class FormStream extends Stream {
7
7
  * @param name Name of field
8
8
  * @param value Value of field
9
9
  */
10
- field(name: string, value: string): this
10
+ field(name: string, value: string, contentType?: string): this
11
11
 
12
12
  /**
13
13
  * Add a local file to be uploaded to the form.
@@ -64,9 +64,14 @@ declare class FormStream extends Stream {
64
64
  headers(additionalHeaders?: Record<string, any>): Record<string, any>
65
65
  }
66
66
 
67
+ interface FormStreamOptions {
68
+ /** min chunk size to emit data event */
69
+ minChunkSize?: number;
70
+ }
71
+
67
72
  declare const formStream: {
68
- new (): FormStream
69
- (): FormStream
73
+ new (options?: FormStreamOptions): FormStream
74
+ (options?: FormStreamOptions): FormStream
70
75
  }
71
76
 
72
77
  interface formStream extends FormStream {}