Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
form_data.js502 linesDownload Raw Back to lib
1var CombinedStream = require('combined-stream');2var util = require('util');3var path = require('path');4var http = require('http');5var https = require('https');6var parseUrl = require('url').parse;7var fs = require('fs');8var Stream = require('stream').Stream;9var mime = require('mime-types');10var asynckit = require('asynckit');11var populate = require('./populate.js');12 13// Public API14module.exports = FormData;15 16// make it a Stream17util.inherits(FormData, CombinedStream);18 19/**20 * Create readable "multipart/form-data" streams.21 * Can be used to submit forms22 * and file uploads to other web applications.23 *24 * @constructor25 * @param {Object} options - Properties to be added/overriden for FormData and CombinedStream26 */27function FormData(options) {28  if (!(this instanceof FormData)) {29    return new FormData(options);30  }31 32  this._overheadLength = 0;33  this._valueLength = 0;34  this._valuesToMeasure = [];35 36  CombinedStream.call(this);37 38  options = options || {};39  for (var option in options) {40    this[option] = options[option];41  }42}43 44FormData.LINE_BREAK = '\r\n';45FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream';46 47FormData.prototype.append = function(field, value, options) {48 49  options = options || {};50 51  // allow filename as single option52  if (typeof options == 'string') {53    options = {filename: options};54  }55 56  var append = CombinedStream.prototype.append.bind(this);57 58  // all that streamy business can't handle numbers59  if (typeof value == 'number') {60    value = '' + value;61  }62 63  // https://github.com/felixge/node-form-data/issues/3864  if (util.isArray(value)) {65    // Please convert your array into string66    // the way web server expects it67    this._error(new Error('Arrays are not supported.'));68    return;69  }70 71  var header = this._multiPartHeader(field, value, options);72  var footer = this._multiPartFooter();73 74  append(header);75  append(value);76  append(footer);77 78  // pass along options.knownLength79  this._trackLength(header, value, options);80};81 82FormData.prototype._trackLength = function(header, value, options) {83  var valueLength = 0;84 85  // used w/ getLengthSync(), when length is known.86  // e.g. for streaming directly from a remote server,87  // w/ a known file a size, and not wanting to wait for88  // incoming file to finish to get its size.89  if (options.knownLength != null) {90    valueLength += +options.knownLength;91  } else if (Buffer.isBuffer(value)) {92    valueLength = value.length;93  } else if (typeof value === 'string') {94    valueLength = Buffer.byteLength(value);95  }96 97  this._valueLength += valueLength;98 99  // @check why add CRLF? does this account for custom/multiple CRLFs?100  this._overheadLength +=101    Buffer.byteLength(header) +102    FormData.LINE_BREAK.length;103 104  // empty or either doesn't have path or not an http response or not a stream105  if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) && !(value instanceof Stream))) {106    return;107  }108 109  // no need to bother with the length110  if (!options.knownLength) {111    this._valuesToMeasure.push(value);112  }113};114 115FormData.prototype._lengthRetriever = function(value, callback) {116 117  if (value.hasOwnProperty('fd')) {118 119    // take read range into a account120    // `end` = Infinity –> read file till the end121    //122    // TODO: Looks like there is bug in Node fs.createReadStream123    // it doesn't respect `end` options without `start` options124    // Fix it when node fixes it.125    // https://github.com/joyent/node/issues/7819126    if (value.end != undefined && value.end != Infinity && value.start != undefined) {127 128      // when end specified129      // no need to calculate range130      // inclusive, starts with 0131      callback(null, value.end + 1 - (value.start ? value.start : 0));132 133    // not that fast snoopy134    } else {135      // still need to fetch file size from fs136      fs.stat(value.path, function(err, stat) {137 138        var fileSize;139 140        if (err) {141          callback(err);142          return;143        }144 145        // update final size based on the range options146        fileSize = stat.size - (value.start ? value.start : 0);147        callback(null, fileSize);148      });149    }150 151  // or http response152  } else if (value.hasOwnProperty('httpVersion')) {153    callback(null, +value.headers['content-length']);154 155  // or request stream http://github.com/mikeal/request156  } else if (value.hasOwnProperty('httpModule')) {157    // wait till response come back158    value.on('response', function(response) {159      value.pause();160      callback(null, +response.headers['content-length']);161    });162    value.resume();163 164  // something else165  } else {166    callback('Unknown stream');167  }168};169 170FormData.prototype._multiPartHeader = function(field, value, options) {171  // custom header specified (as string)?172  // it becomes responsible for boundary173  // (e.g. to handle extra CRLFs on .NET servers)174  if (typeof options.header == 'string') {175    return options.header;176  }177 178  var contentDisposition = this._getContentDisposition(value, options);179  var contentType = this._getContentType(value, options);180 181  var contents = '';182  var headers  = {183    // add custom disposition as third element or keep it two elements if not184    'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []),185    // if no content type. allow it to be empty array186    'Content-Type': [].concat(contentType || [])187  };188 189  // allow custom headers.190  if (typeof options.header == 'object') {191    populate(headers, options.header);192  }193 194  var header;195  for (var prop in headers) {196    if (!headers.hasOwnProperty(prop)) continue;197    header = headers[prop];198 199    // skip nullish headers.200    if (header == null) {201      continue;202    }203 204    // convert all headers to arrays.205    if (!Array.isArray(header)) {206      header = [header];207    }208 209    // add non-empty headers.210    if (header.length) {211      contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK;212    }213  }214 215  return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK;216};217 218FormData.prototype._getContentDisposition = function(value, options) {219 220  var filename221    , contentDisposition222    ;223 224  if (typeof options.filepath === 'string') {225    // custom filepath for relative paths226    filename = path.normalize(options.filepath).replace(/\\/g, '/');227  } else if (options.filename || value.name || value.path) {228    // custom filename take precedence229    // formidable and the browser add a name property230    // fs- and request- streams have path property231    filename = path.basename(options.filename || value.name || value.path);232  } else if (value.readable && value.hasOwnProperty('httpVersion')) {233    // or try http response234    filename = path.basename(value.client._httpMessage.path || '');235  }236 237  if (filename) {238    contentDisposition = 'filename="' + filename + '"';239  }240 241  return contentDisposition;242};243 244FormData.prototype._getContentType = function(value, options) {245 246  // use custom content-type above all247  var contentType = options.contentType;248 249  // or try `name` from formidable, browser250  if (!contentType && value.name) {251    contentType = mime.lookup(value.name);252  }253 254  // or try `path` from fs-, request- streams255  if (!contentType && value.path) {256    contentType = mime.lookup(value.path);257  }258 259  // or if it's http-reponse260  if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) {261    contentType = value.headers['content-type'];262  }263 264  // or guess it from the filepath or filename265  if (!contentType && (options.filepath || options.filename)) {266    contentType = mime.lookup(options.filepath || options.filename);267  }268 269  // fallback to the default content type if `value` is not simple value270  if (!contentType && typeof value == 'object') {271    contentType = FormData.DEFAULT_CONTENT_TYPE;272  }273 274  return contentType;275};276 277FormData.prototype._multiPartFooter = function() {278  return function(next) {279    var footer = FormData.LINE_BREAK;280 281    var lastPart = (this._streams.length === 0);282    if (lastPart) {283      footer += this._lastBoundary();284    }285 286    next(footer);287  }.bind(this);288};289 290FormData.prototype._lastBoundary = function() {291  return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK;292};293 294FormData.prototype.getHeaders = function(userHeaders) {295  var header;296  var formHeaders = {297    'content-type': 'multipart/form-data; boundary=' + this.getBoundary()298  };299 300  for (header in userHeaders) {301    if (userHeaders.hasOwnProperty(header)) {302      formHeaders[header.toLowerCase()] = userHeaders[header];303    }304  }305 306  return formHeaders;307};308 309FormData.prototype.setBoundary = function(boundary) {310  this._boundary = boundary;311};312 313FormData.prototype.getBoundary = function() {314  if (!this._boundary) {315    this._generateBoundary();316  }317 318  return this._boundary;319};320 321FormData.prototype.getBuffer = function() {322  var dataBuffer = new Buffer.alloc( 0 );323  var boundary = this.getBoundary();324 325  // Create the form content. Add Line breaks to the end of data.326  for (var i = 0, len = this._streams.length; i < len; i++) {327    if (typeof this._streams[i] !== 'function') {328 329      // Add content to the buffer.330      if(Buffer.isBuffer(this._streams[i])) {331        dataBuffer = Buffer.concat( [dataBuffer, this._streams[i]]);332      }else {333        dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(this._streams[i])]);334      }335 336      // Add break after content.337      if (typeof this._streams[i] !== 'string' || this._streams[i].substring( 2, boundary.length + 2 ) !== boundary) {338        dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(FormData.LINE_BREAK)] );339      }340    }341  }342 343  // Add the footer and return the Buffer object.344  return Buffer.concat( [dataBuffer, Buffer.from(this._lastBoundary())] );345};346 347FormData.prototype._generateBoundary = function() {348  // This generates a 50 character boundary similar to those used by Firefox.349  // They are optimized for boyer-moore parsing.350  var boundary = '--------------------------';351  for (var i = 0; i < 24; i++) {352    boundary += Math.floor(Math.random() * 10).toString(16);353  }354 355  this._boundary = boundary;356};357 358// Note: getLengthSync DOESN'T calculate streams length359// As workaround one can calculate file size manually360// and add it as knownLength option361FormData.prototype.getLengthSync = function() {362  var knownLength = this._overheadLength + this._valueLength;363 364  // Don't get confused, there are 3 "internal" streams for each keyval pair365  // so it basically checks if there is any value added to the form366  if (this._streams.length) {367    knownLength += this._lastBoundary().length;368  }369 370  // https://github.com/form-data/form-data/issues/40371  if (!this.hasKnownLength()) {372    // Some async length retrievers are present373    // therefore synchronous length calculation is false.374    // Please use getLength(callback) to get proper length375    this._error(new Error('Cannot calculate proper length in synchronous way.'));376  }377 378  return knownLength;379};380 381// Public API to check if length of added values is known382// https://github.com/form-data/form-data/issues/196383// https://github.com/form-data/form-data/issues/262384FormData.prototype.hasKnownLength = function() {385  var hasKnownLength = true;386 387  if (this._valuesToMeasure.length) {388    hasKnownLength = false;389  }390 391  return hasKnownLength;392};393 394FormData.prototype.getLength = function(cb) {395  var knownLength = this._overheadLength + this._valueLength;396 397  if (this._streams.length) {398    knownLength += this._lastBoundary().length;399  }400 401  if (!this._valuesToMeasure.length) {402    process.nextTick(cb.bind(this, null, knownLength));403    return;404  }405 406  asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) {407    if (err) {408      cb(err);409      return;410    }411 412    values.forEach(function(length) {413      knownLength += length;414    });415 416    cb(null, knownLength);417  });418};419 420FormData.prototype.submit = function(params, cb) {421  var request422    , options423    , defaults = {method: 'post'}424    ;425 426  // parse provided url if it's string427  // or treat it as options object428  if (typeof params == 'string') {429 430    params = parseUrl(params);431    options = populate({432      port: params.port,433      path: params.pathname,434      host: params.hostname,435      protocol: params.protocol436    }, defaults);437 438  // use custom params439  } else {440 441    options = populate(params, defaults);442    // if no port provided use default one443    if (!options.port) {444      options.port = options.protocol == 'https:' ? 443 : 80;445    }446  }447 448  // put that good code in getHeaders to some use449  options.headers = this.getHeaders(params.headers);450 451  // https if specified, fallback to http in any other case452  if (options.protocol == 'https:') {453    request = https.request(options);454  } else {455    request = http.request(options);456  }457 458  // get content length and fire away459  this.getLength(function(err, length) {460    if (err && err !== 'Unknown stream') {461      this._error(err);462      return;463    }464 465    // add content length466    if (length) {467      request.setHeader('Content-Length', length);468    }469 470    this.pipe(request);471    if (cb) {472      var onResponse;473 474      var callback = function (error, responce) {475        request.removeListener('error', callback);476        request.removeListener('response', onResponse);477 478        return cb.call(this, error, responce);479      };480 481      onResponse = callback.bind(this, null);482 483      request.on('error', callback);484      request.on('response', onResponse);485    }486  }.bind(this));487 488  return request;489};490 491FormData.prototype._error = function(err) {492  if (!this.error) {493    this.error = err;494    this.pause();495    this.emit('error', err);496  }497};498 499FormData.prototype.toString = function () {500  return '[object FormData]';501};502 
codekingpro/portable-devtools · Team Ai