Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81146 views
1
var CombinedStream = require('combined-stream');
2
var util = require('util');
3
var path = require('path');
4
var http = require('http');
5
var https = require('https');
6
var parseUrl = require('url').parse;
7
var fs = require('fs');
8
var mime = require('mime-types');
9
var async = require('async');
10
11
module.exports = FormData;
12
function FormData() {
13
this._overheadLength = 0;
14
this._valueLength = 0;
15
this._lengthRetrievers = [];
16
17
CombinedStream.call(this);
18
}
19
util.inherits(FormData, CombinedStream);
20
21
FormData.LINE_BREAK = '\r\n';
22
23
FormData.prototype.append = function(field, value, options) {
24
options = options || {};
25
26
var append = CombinedStream.prototype.append.bind(this);
27
28
// all that streamy business can't handle numbers
29
if (typeof value == 'number') value = ''+value;
30
31
// https://github.com/felixge/node-form-data/issues/38
32
if (util.isArray(value)) {
33
// Please convert your array into string
34
// the way web server expects it
35
this._error(new Error('Arrays are not supported.'));
36
return;
37
}
38
39
var header = this._multiPartHeader(field, value, options);
40
var footer = this._multiPartFooter(field, value, options);
41
42
append(header);
43
append(value);
44
append(footer);
45
46
// pass along options.knownLength
47
this._trackLength(header, value, options);
48
};
49
50
FormData.prototype._trackLength = function(header, value, options) {
51
var valueLength = 0;
52
53
// used w/ getLengthSync(), when length is known.
54
// e.g. for streaming directly from a remote server,
55
// w/ a known file a size, and not wanting to wait for
56
// incoming file to finish to get its size.
57
if (options.knownLength != null) {
58
valueLength += +options.knownLength;
59
} else if (Buffer.isBuffer(value)) {
60
valueLength = value.length;
61
} else if (typeof value === 'string') {
62
valueLength = Buffer.byteLength(value);
63
}
64
65
this._valueLength += valueLength;
66
67
// @check why add CRLF? does this account for custom/multiple CRLFs?
68
this._overheadLength +=
69
Buffer.byteLength(header) +
70
+ FormData.LINE_BREAK.length;
71
72
// empty or either doesn't have path or not an http response
73
if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) {
74
return;
75
}
76
77
// no need to bother with the length
78
if (!options.knownLength)
79
this._lengthRetrievers.push(function(next) {
80
81
if (value.hasOwnProperty('fd')) {
82
83
// take read range into a account
84
// `end` = Infinity –> read file till the end
85
//
86
// TODO: Looks like there is bug in Node fs.createReadStream
87
// it doesn't respect `end` options without `start` options
88
// Fix it when node fixes it.
89
// https://github.com/joyent/node/issues/7819
90
if (value.end != undefined && value.end != Infinity && value.start != undefined) {
91
92
// when end specified
93
// no need to calculate range
94
// inclusive, starts with 0
95
next(null, value.end+1 - (value.start ? value.start : 0));
96
97
// not that fast snoopy
98
} else {
99
// still need to fetch file size from fs
100
fs.stat(value.path, function(err, stat) {
101
102
var fileSize;
103
104
if (err) {
105
next(err);
106
return;
107
}
108
109
// update final size based on the range options
110
fileSize = stat.size - (value.start ? value.start : 0);
111
next(null, fileSize);
112
});
113
}
114
115
// or http response
116
} else if (value.hasOwnProperty('httpVersion')) {
117
next(null, +value.headers['content-length']);
118
119
// or request stream http://github.com/mikeal/request
120
} else if (value.hasOwnProperty('httpModule')) {
121
// wait till response come back
122
value.on('response', function(response) {
123
value.pause();
124
next(null, +response.headers['content-length']);
125
});
126
value.resume();
127
128
// something else
129
} else {
130
next('Unknown stream');
131
}
132
});
133
};
134
135
FormData.prototype._multiPartHeader = function(field, value, options) {
136
var boundary = this.getBoundary();
137
var header = '';
138
139
// custom header specified (as string)?
140
// it becomes responsible for boundary
141
// (e.g. to handle extra CRLFs on .NET servers)
142
if (options.header != null) {
143
header = options.header;
144
} else {
145
header += '--' + boundary + FormData.LINE_BREAK +
146
'Content-Disposition: form-data; name="' + field + '"';
147
148
// fs- and request- streams have path property
149
// or use custom filename and/or contentType
150
// TODO: Use request's response mime-type
151
if (options.filename || value.path) {
152
header +=
153
'; filename="' + path.basename(options.filename || value.path) + '"' + FormData.LINE_BREAK +
154
'Content-Type: ' + (options.contentType || mime.lookup(options.filename || value.path));
155
156
// http response has not
157
} else if (value.readable && value.hasOwnProperty('httpVersion')) {
158
header +=
159
'; filename="' + path.basename(value.client._httpMessage.path) + '"' + FormData.LINE_BREAK +
160
'Content-Type: ' + value.headers['content-type'];
161
}
162
163
header += FormData.LINE_BREAK + FormData.LINE_BREAK;
164
}
165
166
return header;
167
};
168
169
FormData.prototype._multiPartFooter = function(field, value, options) {
170
return function(next) {
171
var footer = FormData.LINE_BREAK;
172
173
var lastPart = (this._streams.length === 0);
174
if (lastPart) {
175
footer += this._lastBoundary();
176
}
177
178
next(footer);
179
}.bind(this);
180
};
181
182
FormData.prototype._lastBoundary = function() {
183
return '--' + this.getBoundary() + '--';
184
};
185
186
FormData.prototype.getHeaders = function(userHeaders) {
187
var formHeaders = {
188
'content-type': 'multipart/form-data; boundary=' + this.getBoundary()
189
};
190
191
for (var header in userHeaders) {
192
formHeaders[header.toLowerCase()] = userHeaders[header];
193
}
194
195
return formHeaders;
196
}
197
198
FormData.prototype.getCustomHeaders = function(contentType) {
199
contentType = contentType ? contentType : 'multipart/form-data';
200
201
var formHeaders = {
202
'content-type': contentType + '; boundary=' + this.getBoundary(),
203
'content-length': this.getLengthSync()
204
};
205
206
return formHeaders;
207
}
208
209
FormData.prototype.getBoundary = function() {
210
if (!this._boundary) {
211
this._generateBoundary();
212
}
213
214
return this._boundary;
215
};
216
217
FormData.prototype._generateBoundary = function() {
218
// This generates a 50 character boundary similar to those used by Firefox.
219
// They are optimized for boyer-moore parsing.
220
var boundary = '--------------------------';
221
for (var i = 0; i < 24; i++) {
222
boundary += Math.floor(Math.random() * 10).toString(16);
223
}
224
225
this._boundary = boundary;
226
};
227
228
// Note: getLengthSync DOESN'T calculate streams length
229
// As workaround one can calculate file size manually
230
// and add it as knownLength option
231
FormData.prototype.getLengthSync = function(debug) {
232
var knownLength = this._overheadLength + this._valueLength;
233
234
// Don't get confused, there are 3 "internal" streams for each keyval pair
235
// so it basically checks if there is any value added to the form
236
if (this._streams.length) {
237
knownLength += this._lastBoundary().length;
238
}
239
240
// https://github.com/felixge/node-form-data/issues/40
241
if (this._lengthRetrievers.length) {
242
// Some async length retrivers are present
243
// therefore synchronous length calculation is false.
244
// Please use getLength(callback) to get proper length
245
this._error(new Error('Cannot calculate proper length in synchronous way.'));
246
}
247
248
return knownLength;
249
};
250
251
FormData.prototype.getLength = function(cb) {
252
var knownLength = this._overheadLength + this._valueLength;
253
254
if (this._streams.length) {
255
knownLength += this._lastBoundary().length;
256
}
257
258
if (!this._lengthRetrievers.length) {
259
process.nextTick(cb.bind(this, null, knownLength));
260
return;
261
}
262
263
async.parallel(this._lengthRetrievers, function(err, values) {
264
if (err) {
265
cb(err);
266
return;
267
}
268
269
values.forEach(function(length) {
270
knownLength += length;
271
});
272
273
cb(null, knownLength);
274
});
275
};
276
277
FormData.prototype.submit = function(params, cb) {
278
279
var request
280
, options
281
, defaults = {
282
method : 'post'
283
};
284
285
// parse provided url if it's string
286
// or treat it as options object
287
if (typeof params == 'string') {
288
params = parseUrl(params);
289
290
options = populate({
291
port: params.port,
292
path: params.pathname,
293
host: params.hostname
294
}, defaults);
295
}
296
else // use custom params
297
{
298
options = populate(params, defaults);
299
// if no port provided use default one
300
if (!options.port) {
301
options.port = options.protocol == 'https:' ? 443 : 80;
302
}
303
}
304
305
// put that good code in getHeaders to some use
306
options.headers = this.getHeaders(params.headers);
307
308
// https if specified, fallback to http in any other case
309
if (params.protocol == 'https:') {
310
request = https.request(options);
311
} else {
312
request = http.request(options);
313
}
314
315
// get content length and fire away
316
this.getLength(function(err, length) {
317
318
// TODO: Add chunked encoding when no length (if err)
319
320
// add content length
321
request.setHeader('Content-Length', length);
322
323
this.pipe(request);
324
if (cb) {
325
request.on('error', cb);
326
request.on('response', cb.bind(this, null));
327
}
328
}.bind(this));
329
330
return request;
331
};
332
333
FormData.prototype._error = function(err) {
334
if (this.error) return;
335
336
this.error = err;
337
this.pause();
338
this.emit('error', err);
339
};
340
341
/*
342
* Santa's little helpers
343
*/
344
345
// populates missing values
346
function populate(dst, src) {
347
for (var prop in src) {
348
if (!dst[prop]) dst[prop] = src[prop];
349
}
350
return dst;
351
}
352
353