Coverage

100%
131
131
0

formstream.js

100%
131
131
0
LineHitsSource
1/*!
2 * formstream - lib/formstream.js
3 * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>
4 * MIT Licensed
5 *
6 * Data format:
7 *
8
9--FormStreamBoundary1349886663601\r\n
10Content-Disposition: form-data; name="foo"\r\n
11\r\n
12<FIELD-CONTENT>\r\n
13--FormStreamBoundary1349886663601\r\n
14Content-Disposition: form-data; name="file"; filename="formstream.test.js"\r\n
15Content-Type: application/javascript\r\n
16\r\n
17<FILE-CONTENT>\r\n
18--FormStreamBoundary1349886663601\r\n
19Content-Disposition: form-data; name="pic"; filename="fawave.png"\r\n
20Content-Type: image/png\r\n
21\r\n
22<IMAGE-CONTENT>\r\n
23--FormStreamBoundary1349886663601--
24
25 *
26 */
27
281"use strict";
29
30/**
31 * Module dependencies.
32 */
331require('buffer-concat');
341var Stream = require('stream');
351var parseStream = require('pause-stream');
361var util = require('util');
371var mime = require('mime');
381var path = require('path');
391var fs = require('fs');
40
411var PADDING = '--';
421var NEW_LINE = '\r\n';
431var NEW_LINE_BUFFER = new Buffer(NEW_LINE);
44
451function FormStream() {
4626 if (!(this instanceof FormStream)) {
4713 return new FormStream();
48 }
49
5013 FormStream.super_.call(this);
5113 this._boundary = this._generateBoundary();
5213 this._streams = [];
5313 this._fields = [];
5413 this._buffers = [];
5513 this._endData = new Buffer(PADDING + this._boundary + PADDING + NEW_LINE);
56}
57
581util.inherits(FormStream, Stream);
591module.exports = FormStream;
60
611FormStream.prototype._generateBoundary = function() {
62 // https://github.com/felixge/node-form-data/blob/master/lib/form_data.js#L162
63 // This generates a 50 character boundary similar to those used by Firefox.
64 // They are optimized for boyer-moore parsing.
6513 var boundary = '--------------------------';
6613 for (var i = 0; i < 24; i++) {
67312 boundary += Math.floor(Math.random() * 10).toString(16);
68 }
69
7013 return boundary;
71};
72
73/**
74 * Set total stream size.
75 *
76 * You know total stream data size and you want to set `Content-Length` in headers.
77 */
781FormStream.prototype.setTotalStreamSize = function (size) {
797 size = size || 0;
80 // plus fileds data size
817 this._formatFields();
827 if (this._fieldsData) {
837 size += this._fieldsData.length;
84 }
85
86 // plus stream field data size
877 for (var i = 0; i < this._streams.length; i++) {
886 var item = this._streams[i];
896 size += item[0].length;
906 size += NEW_LINE_BUFFER.length; // stream field end pedding size
91 }
92
93 // plus buffers size
947 for (var i = 0; i < this._buffers.length; i++) {
951 var item = this._buffers[i];
961 size += item[0].length;
971 size += item[1].length;
981 size += NEW_LINE_BUFFER.length;
99 }
100
101 // end padding data size
1027 size += this._endData.length;
1037 this._contentLength = size;
104};
105
1061FormStream.prototype.headers = function (options) {
10713 var headers = {
108 'Content-Type': 'multipart/form-data; boundary=' + this._boundary
109 };
11013 if (typeof this._contentLength === 'number') {
1117 headers['Content-Length'] = String(this._contentLength);
112 }
11313 if (options) {
1142 for (var k in options) {
1152 headers[k] = options[k];
116 }
117 }
11813 return headers;
119};
120
1211FormStream.prototype.file = function (name, filepath, filename) {
12211 var mimeType = mime.lookup(filepath);
12311 if (!filename) {
12411 filename = path.basename(filepath);
125 }
12611 this.stream(name, fs.createReadStream(filepath), filename, mimeType);
127};
128
1291FormStream.prototype.field = function (name, value) {
13034 this._fields.push([name, value]);
13134 process.nextTick(this.resume.bind(this));
132};
133
1341FormStream.prototype.stream = function (name, stream, filename, mimeType) {
13513 mimeType = mimeType || mime.lookup(filename);
13613 stream.on('error', this.emit.bind(this, 'error'));
13713 var ps = parseStream().pause();
13813 stream.pipe(ps);
13913 this._streams.push([
140 this._formatStreamField(name, filename, mimeType),
141 ps
142 ]);
14313 process.nextTick(this.resume.bind(this));
144};
145
1461FormStream.prototype.buffer = function (name, buffer, filename, mimeType) {
1473 mimeType = mimeType || mime.lookup(filename);
1483 this._buffers.push([
149 this._formatStreamField(name, filename, mimeType),
150 buffer
151 ]);
1523 process.nextTick(this.resume.bind(this));
153};
154
1551FormStream.prototype._emitEnd = function () {
156 // ending format:
157 //
158 // --{boundary}--\r\n
15911 this.emit('data', this._endData);
16011 this.emit('end');
161};
162
1631FormStream.prototype._formatFields = function () {
16431 if (!this._fields.length) {
16519 return;
166 }
167
16812 var lines = '';
16912 for (var i = 0; i < this._fields.length; i++) {
17034 var field = this._fields[i];
17134 lines += PADDING + this._boundary + NEW_LINE;
17234 lines += 'Content-Disposition: form-data; name="' + field[0] + '"' + NEW_LINE;
17334 lines += NEW_LINE;
17434 lines += field[1];
17534 lines += NEW_LINE;
176 }
17712 this._fieldsData = new Buffer(lines);
17812 this._fields = [];
179};
180
1811FormStream.prototype._emitFields = function () {
18224 this._formatFields();
18324 if (this._fieldsData) {
18412 var data = this._fieldsData;
18512 this._fieldsData = null;
18612 this.emit('data', data);
187 }
188};
189
1901FormStream.prototype._emitBuffers = function () {
19124 if (!this._buffers.length) {
19222 return;
193 }
1942 for (var i = 0; i < this._buffers.length; i++) {
1953 var item = this._buffers[i];
1963 this.emit('data', item[0]);
1973 this.emit('data', item[1]);
1983 this.emit('data', NEW_LINE_BUFFER);
199 }
2002 this._buffers = [];
201};
202
2031FormStream.prototype._formatStreamField = function (name, filename, mimeType) {
20416 var data = PADDING + this._boundary + NEW_LINE;
20516 data += 'Content-Disposition: form-data; name="' + name +'"; filename="' + filename + '"' + NEW_LINE;
20616 data += 'Content-Type: ' + mimeType + NEW_LINE;
20716 data += NEW_LINE;
20816 return new Buffer(data);
209};
210
2111FormStream.prototype._emitStream = function (item) {
21213 var self = this;
213 // item: [ fieldData, stream ]
21413 self.emit('data', item[0]);
215
21613 var stream = item[1];
21713 stream.on('data', function (data) {
21822 self.emit('data', data);
219 });
22013 stream.on('end', function () {
22112 self.emit('data', NEW_LINE_BUFFER);
22212 return process.nextTick(self.drain.bind(self));
223 });
22413 stream.resume();
225};
226
2271FormStream.prototype.drain = function () {
22824 this._emitFields();
22924 this._emitBuffers();
23024 var item = this._streams.shift();
23124 if (item) {
23213 this._emitStream(item);
233 } else {
234 // end
23511 this._emitEnd();
236 }
23724 return this;
238};
239
2401FormStream.prototype.resume = function () {
24150 this.paused = false;
24250 if (!this._draining) {
24312 this._draining = true;
24412 this.drain();
245 }
24650 return this;
247};