You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							294 lines
						
					
					
						
							6.9 KiB
						
					
					
				
			
		
		
	
	
							294 lines
						
					
					
						
							6.9 KiB
						
					
					
				| /**
 | |
|  * Data format:
 | |
|  *
 | |
| 
 | |
| --FormStreamBoundary1349886663601\r\n
 | |
| Content-Disposition: form-data; name="foo"\r\n
 | |
| \r\n
 | |
| <FIELD-CONTENT>\r\n
 | |
| --FormStreamBoundary1349886663601\r\n
 | |
| Content-Disposition: form-data; name="file"; filename="formstream.test.js"\r\n
 | |
| Content-Type: application/javascript\r\n
 | |
| \r\n
 | |
| <FILE-CONTENT>\r\n
 | |
| --FormStreamBoundary1349886663601\r\n
 | |
| Content-Disposition: form-data; name="pic"; filename="fawave.png"\r\n
 | |
| Content-Type: image/png\r\n
 | |
| \r\n
 | |
| <IMAGE-CONTENT>\r\n
 | |
| --FormStreamBoundary1349886663601--
 | |
| 
 | |
|  *
 | |
|  */
 | |
| 
 | |
| 'use strict';
 | |
| 
 | |
| var Stream = require('stream');
 | |
| var parseStream = require('pause-stream');
 | |
| var util = require('util');
 | |
| var mime = require('mime');
 | |
| var path = require('path');
 | |
| var fs = require('fs');
 | |
| var destroy = require('destroy');
 | |
| 
 | |
| var PADDING = '--';
 | |
| var NEW_LINE = '\r\n';
 | |
| var NEW_LINE_BUFFER = new Buffer(NEW_LINE);
 | |
| 
 | |
| function FormStream() {
 | |
|   if (!(this instanceof FormStream)) {
 | |
|     return new FormStream();
 | |
|   }
 | |
| 
 | |
|   FormStream.super_.call(this);
 | |
| 
 | |
|   this._boundary = this._generateBoundary();
 | |
|   this._streams = [];
 | |
|   this._buffers = [];
 | |
|   this._endData = new Buffer(PADDING + this._boundary + PADDING + NEW_LINE);
 | |
|   this._contentLength = 0;
 | |
|   this._isAllStreamSizeKnown = true;
 | |
|   this._knownStreamSize = 0;
 | |
| }
 | |
| 
 | |
| util.inherits(FormStream, Stream);
 | |
| module.exports = FormStream;
 | |
| 
 | |
| FormStream.prototype._generateBoundary = function() {
 | |
|   // https://github.com/felixge/node-form-data/blob/master/lib/form_data.js#L162
 | |
|   // This generates a 50 character boundary similar to those used by Firefox.
 | |
|   // They are optimized for boyer-moore parsing.
 | |
|   var boundary = '--------------------------';
 | |
|   for (var i = 0; i < 24; i++) {
 | |
|     boundary += Math.floor(Math.random() * 10).toString(16);
 | |
|   }
 | |
| 
 | |
|   return boundary;
 | |
| };
 | |
| 
 | |
| FormStream.prototype.setTotalStreamSize = function (size) {
 | |
|   // this method should not make any sense if the length of each stream is known.
 | |
|   if (this._isAllStreamSizeKnown) {
 | |
|     return this;
 | |
|   }
 | |
| 
 | |
|   size = size || 0;
 | |
| 
 | |
|   for (var i = 0; i < this._streams.length; i++) {
 | |
|     size += this._streams[i][0].length;
 | |
|     size += NEW_LINE_BUFFER.length; // stream field end pedding size
 | |
|   }
 | |
| 
 | |
|   this._knownStreamSize = size;
 | |
|   this._isAllStreamSizeKnown = true;
 | |
| 
 | |
|   return this;
 | |
| };
 | |
| 
 | |
| FormStream.prototype.headers = function (options) {
 | |
|   var headers = {
 | |
|     'Content-Type': 'multipart/form-data; boundary=' + this._boundary
 | |
|   };
 | |
| 
 | |
|   // calculate total stream size
 | |
|   this._contentLength += this._knownStreamSize;
 | |
| 
 | |
|   // calculate length of end padding
 | |
|   this._contentLength += this._endData.length;
 | |
| 
 | |
|   if (this._isAllStreamSizeKnown) {
 | |
|     headers['Content-Length'] = String(this._contentLength);
 | |
|   }
 | |
| 
 | |
|   if (options) {
 | |
|     for (var k in options) {
 | |
|       headers[k] = options[k];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return headers;
 | |
| };
 | |
| 
 | |
| FormStream.prototype.file = function (name, filepath, filename, filesize) {
 | |
|   var mimeType = mime.getType(filepath);
 | |
| 
 | |
|   if (typeof filename === 'number' && !filesize) {
 | |
|     filesize = filename;
 | |
|     filename = path.basename(filepath);
 | |
|   } else if (!filename) {
 | |
|     filename = path.basename(filepath);
 | |
|   }
 | |
| 
 | |
|   var stream = fs.createReadStream(filepath);
 | |
| 
 | |
|   return this.stream(name, stream, filename, mimeType, filesize);
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Add a form field
 | |
|  * @param  {String} name field name
 | |
|  * @param  {String|Buffer} value field value
 | |
|  * @return {this}
 | |
|  */
 | |
| FormStream.prototype.field = function (name, value) {
 | |
|   if (!Buffer.isBuffer(value)) {
 | |
|     // field(String, Number)
 | |
|     // https://github.com/qiniu/nodejs-sdk/issues/123
 | |
|     if (typeof value === 'number') {
 | |
|       value = String(value);
 | |
|     }
 | |
|     value = new Buffer(value);
 | |
|   }
 | |
|   return this.buffer(name, value);
 | |
| };
 | |
| 
 | |
| FormStream.prototype.stream = function (name, stream, filename, mimeType, size) {
 | |
|   if (typeof mimeType === 'number' && !size) {
 | |
|     size = mimeType;
 | |
|     mimeType = mime.getType(filename);
 | |
|   } else if (!mimeType) {
 | |
|     mimeType = mime.getType(filename);
 | |
|   }
 | |
| 
 | |
|   stream.once('error', this.emit.bind(this, 'error'));
 | |
|   // if form stream destroy, also destroy the source stream
 | |
|   this.once('destroy', function () {
 | |
|     destroy(stream);
 | |
|   });
 | |
| 
 | |
|   var leading = this._leading({ name: name, filename: filename }, mimeType);
 | |
| 
 | |
|   var ps = parseStream().pause();
 | |
|   stream.pipe(ps);
 | |
| 
 | |
|   this._streams.push([leading, ps]);
 | |
| 
 | |
|   // if the size of this stream is known, plus the total content-length;
 | |
|   // otherwise, content-length is unknown.
 | |
|   if (typeof size === 'number') {
 | |
|     this._knownStreamSize += leading.length;
 | |
|     this._knownStreamSize += size;
 | |
|     this._knownStreamSize += NEW_LINE_BUFFER.length;
 | |
|   } else {
 | |
|     this._isAllStreamSizeKnown = false;
 | |
|   }
 | |
| 
 | |
|   process.nextTick(this.resume.bind(this));
 | |
| 
 | |
|   return this;
 | |
| };
 | |
| 
 | |
| FormStream.prototype.buffer = function (name, buffer, filename, mimeType) {
 | |
|   if (filename && !mimeType) {
 | |
|     mimeType = mime.getType(filename);
 | |
|   }
 | |
| 
 | |
|   var disposition = { name: name };
 | |
|   if (filename) {
 | |
|     disposition.filename = filename;
 | |
|   }
 | |
| 
 | |
|   var leading = this._leading(disposition, mimeType);
 | |
| 
 | |
|   this._buffers.push([leading, buffer]);
 | |
| 
 | |
|   // plus buffer length to total content-length
 | |
|   this._contentLength += leading.length;
 | |
|   this._contentLength += buffer.length;
 | |
|   this._contentLength += NEW_LINE_BUFFER.length;
 | |
| 
 | |
|   process.nextTick(this.resume.bind(this));
 | |
| 
 | |
|   return this;
 | |
| };
 | |
| 
 | |
| FormStream.prototype._leading = function (disposition, type) {
 | |
|   var leading = [PADDING + this._boundary];
 | |
| 
 | |
|   var disps = [];
 | |
| 
 | |
|   if (disposition) {
 | |
|     for (var k in disposition) {
 | |
|       disps.push(k + '="' + disposition[k] + '"');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   leading.push('Content-Disposition: form-data; ' + disps.join('; '));
 | |
| 
 | |
|   if (type) {
 | |
|     leading.push('Content-Type: ' + type);
 | |
|   }
 | |
| 
 | |
|   leading.push('');
 | |
|   leading.push('');
 | |
| 
 | |
|   return new Buffer(leading.join(NEW_LINE));
 | |
| };
 | |
| 
 | |
| FormStream.prototype._emitBuffers = function () {
 | |
|   if (!this._buffers.length) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   for (var i = 0; i < this._buffers.length; i++) {
 | |
|     var item = this._buffers[i];
 | |
|     this.emit('data', item[0]); // part leading
 | |
|     this.emit('data', item[1]); // part content
 | |
|     this.emit('data', NEW_LINE_BUFFER);
 | |
|   }
 | |
| 
 | |
|   this._buffers = [];
 | |
| };
 | |
| 
 | |
| FormStream.prototype._emitStream = function (item) {
 | |
|   var self = this;
 | |
|   // item: [ fieldData, stream ]
 | |
|   self.emit('data', item[0]);
 | |
| 
 | |
|   var stream = item[1];
 | |
|   stream.on('data', function (data) {
 | |
|     self.emit('data', data);
 | |
|   });
 | |
|   stream.on('end', function () {
 | |
|     self.emit('data', NEW_LINE_BUFFER);
 | |
|     return process.nextTick(self.drain.bind(self));
 | |
|   });
 | |
|   stream.resume();
 | |
| };
 | |
| 
 | |
| FormStream.prototype._emitEnd = function () {
 | |
|   // ending format:
 | |
|   //
 | |
|   // --{boundary}--\r\n
 | |
|   this.emit('data', this._endData);
 | |
|   this.emit('end');
 | |
| };
 | |
| 
 | |
| FormStream.prototype.drain = function () {
 | |
|   this._emitBuffers();
 | |
| 
 | |
|   var item = this._streams.shift();
 | |
|   if (item) {
 | |
|     this._emitStream(item);
 | |
|   } else {
 | |
|     this._emitEnd();
 | |
|   }
 | |
| 
 | |
|   return this;
 | |
| };
 | |
| 
 | |
| FormStream.prototype.resume = function () {
 | |
|   this.paused = false;
 | |
| 
 | |
|   if (!this._draining) {
 | |
|     this._draining = true;
 | |
|     this.drain();
 | |
|   }
 | |
| 
 | |
|   return this;
 | |
| };
 | |
| 
 | |
| FormStream.prototype.close = FormStream.prototype.destroy = function () {
 | |
|   this.emit('destroy');
 | |
| };
 |