form-data2.js 5.95 KB
var CRLF, FormData, Promise, combinedStream2, debug, mime, ofTypes, path, stream, uuid, _;

path = require("path");

stream = require("stream");

uuid = require("uuid");

mime = require("mime");

combinedStream2 = require("combined-stream2");

Promise = require("bluebird");

_ = require("lodash");

debug = require("debug")("form-data2");

CRLF = "\r\n";

ofTypes = function(obj, types) {
  var match, type, _i, _len;
  match = false;
  for (_i = 0, _len = types.length; _i < _len; _i++) {
    type = types[_i];
    match = match || obj instanceof type;
  }
  return match;
};

module.exports = FormData = (function() {
  function FormData() {
    this._firstHeader = false;
    this._closingHeaderAppended = false;
    this._boundary = "----" + uuid.v4();
    this._headers = {
      "content-type": "multipart/form-data; boundary=" + this._boundary
    };
    this._stream = combinedStream2.create();
  }

  FormData.prototype._getStreamMetadata = function(source, options) {
    var contentLength, contentType, filename, fullPath, _ref, _ref1, _ref10, _ref11, _ref12, _ref13, _ref14, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9;
    debug("obtaining metadata for source: %s", source.toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r"));
    fullPath = (_ref = (_ref1 = options.filename) != null ? _ref1 : (_ref2 = source.client) != null ? (_ref3 = _ref2._httpMessage) != null ? _ref3.path : void 0 : void 0) != null ? _ref : source.path;
    if (fullPath != null) {
      filename = path.basename(fullPath);
      contentType = (_ref4 = (_ref5 = options.contentType) != null ? _ref5 : (_ref6 = source.headers) != null ? _ref6["content-type"] : void 0) != null ? _ref4 : mime.lookup(filename);
      contentLength = (_ref7 = (_ref8 = options.knownLength) != null ? _ref8 : options.contentLength) != null ? _ref7 : (_ref9 = source.headers) != null ? _ref9["content-length"] : void 0;
    } else {
      contentType = (_ref10 = options.contentType) != null ? _ref10 : (_ref11 = source.headers) != null ? _ref11["content-type"] : void 0;
      contentLength = (_ref12 = (_ref13 = options.knownLength) != null ? _ref13 : options.contentLength) != null ? _ref12 : (_ref14 = source.headers) != null ? _ref14["content-length"] : void 0;
    }
    return {
      filename: filename,
      contentType: contentType,
      contentLength: contentLength
    };
  };

  FormData.prototype._generateHeaderFields = function(name, metadata) {
    var escapedFilename, headerFields;
    debug("generating headers for: %s", metadata);
    headerFields = [];
    if (metadata.filename != null) {
      escapedFilename = metadata.filename.replace('"', '\\"');
      headerFields.push("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + escapedFilename + "\"");
    } else {
      headerFields.push("Content-Disposition: form-data; name=\"" + name + "\"");
    }
    if (metadata.contentType != null) {
      headerFields.push("Content-Type: " + metadata.contentType);
    }
    debug("generated headers: %s", headerFields);
    return headerFields.join(CRLF);
  };

  FormData.prototype._appendHeader = function(name, metadata) {
    var headerFields, leadingCRLF;
    if (this._firstHeader === false) {
      debug("appending header");
      leadingCRLF = "";
      this._firstHeader = true;
    } else {
      debug("appending first header");
      leadingCRLF = CRLF;
    }
    headerFields = this._generateHeaderFields(name, metadata);
    return this._stream.append(new Buffer(leadingCRLF + ("--" + this._boundary) + CRLF + headerFields + CRLF + CRLF));
  };

  FormData.prototype._appendClosingHeader = function() {
    debug("appending closing header");
    return this._stream.append(new Buffer(CRLF + ("--" + this._boundary + "--")));
  };

  FormData.prototype.append = function(name, source, options) {
    var metadata;
    if (options == null) {
      options = {};
    }
    debug("appending source");
    if (this._closingHeaderAppended) {
      throw new Error("The stream has already been prepared for usage; you either piped it or generated the HTTP headers. No new sources can be appended anymore.");
    }
    if (!ofTypes(source, [stream.Readable, stream.Duplex, stream.Transform, Buffer, Function]) && typeof source !== "string") {
      throw new Error("The provided value must be either a readable stream, a Buffer, a callback providing either of those, or a string.");
    }
    if (typeof source === "string") {
      source = new Buffer(source);
      if (options.contentType == null) {
        options.contentType = "text/plain";
      }
    }
    metadata = this._getStreamMetadata(source, options);
    this._appendHeader(name, metadata);
    return this._stream.append(source, options);
  };

  FormData.prototype.done = function() {
    debug("called 'done'");
    if (!this._closingHeaderAppended) {
      this._closingHeaderAppended = true;
      return this._appendClosingHeader();
    }
  };

  FormData.prototype.getBoundary = function() {
    return this._boundary;
  };

  FormData.prototype.getHeaders = function(callback) {
    this.done();
    return Promise["try"]((function(_this) {
      return function() {
        return _this._stream.getCombinedStreamLength();
      };
    })(this)).then(function(length) {
      debug("total combined stream length: %s", length);
      return Promise.resolve({
        "content-length": length
      });
    })["catch"](function(err) {
      debug("WARN: could not get total combined stream length");
      return Promise.resolve({
        "transfer-encoding": "chunked"
      });
    }).then((function(_this) {
      return function(sizeHeaders) {
        return Promise.resolve(_.extend(sizeHeaders, _this._headers));
      };
    })(this)).nodeify(callback);
  };

  FormData.prototype.getLength = function(callback) {
    return this._stream.getCombinedStreamLength(callback);
  };

  FormData.prototype.pipe = function(target) {
    this.done();
    debug("piping underlying combined-stream2 to target writable");
    return this._stream.pipe(target);
  };

  return FormData;

})();