poster
Version:
node.js module for streaming local/remote files over multipart.
299 lines (266 loc) • 10.4 kB
JavaScript
var http = require('http');
var https = require('https');
var url = require('url');
var fs = require('fs');
var path = require('path');
var mimetypes = require('./mimetypes');
module.exports = (function() { 'use strict';
var BOUNDRARY = '----Y0uR3tH3m4nN0wd0g';
var MULTIPART_END = '\r\n--' + BOUNDRARY + '--\r\n';
function upload(uploadUrl, parsedUri, uploadOptions, fileSize, fileName, callback) {
var form = getMultipartForm(uploadOptions.fields, uploadOptions.fileId, fileName, uploadOptions.fileContentType);
var contentLength = form.length + fileSize + MULTIPART_END.length;
var resData = '';
var uploadProtocol = getProtocol(uploadUrl);
var headers = {
'Content-Length': contentLength,
'Content-Type': 'multipart/form-data; boundary=' + BOUNDRARY
};
if (uploadOptions.uploadHeaders) {
for (var attr in uploadOptions.uploadHeaders) { headers[attr] = uploadOptions.uploadHeaders[attr]; }
}
var options = getRequestOptions('POST', uploadUrl, headers, uploadOptions.uploadAgent);
var req = uploadProtocol.request(options, function(res) {
if ((res.statusCode < 200) || (res.statusCode >= 300)) {
return callback('Invalid response from upload server. statusCode: ' + res.statusCode);
}
res.setEncoding('utf8');
res.on('data', function(chunk) {
resData += chunk;
});
res.on('end', function() {
callback(null, resData);
});
});
/** We do not want to buffer any data since we could buffer a ton of data before the connection
* is made (or not), lets wait to be connected to the remote server before sending any data */
req.on('socket', function() {
req.socket.on('connect', function() {
req.write(form);
if (!parsedUri.isValidUrl) {
var fileStream = fs.createReadStream(parsedUri.path);
fileStream.on('data', function (data) {
req.write(data);
});
fileStream.on('end', function() {
req.write(MULTIPART_END);
req.end();
});
fileStream.on('error', function(err) {
req.destroy(err);
});
}
else {
var downloadProtocol = getProtocol(parsedUri);
var downloadOptions = getRequestOptions('GET', parsedUri, uploadOptions.downloadHeaders, uploadOptions.downloadAgent);
var downloadReq = downloadProtocol.request(downloadOptions, function(res) {
if ((res.statusCode < 200) || (res.statusCode >= 300)) {
downloadReq.destroy('Invalid response from remote file server. statusCode: ' + res.statusCode);
}
res.on('data', function (data) {
req.write(data);
});
res.on('end', function() {
req.write(MULTIPART_END);
req.end();
});
});
downloadReq.on('error', function(err) {
req.destroy(err);
});
downloadReq.end();
}
});
});
req.on('error', function(err) {
return callback(err);
});
}
/** this function is kind of hacky, but since not all web servers support chunked
* encoding and we DO NOT want to buffer any of the file we're downloading into memory,
* this is how we get around this so we can calculate the content-length. while this
* does indeed slow down the upload, it does save us some time if the file doesn't exist
* on the remote web server of if maxFileSize is provided, we can bail out if it's too large */
function head(url, uploadOptions, redirectCount, callback) {
var options = getRequestOptions('HEAD', url, uploadOptions.downloadHeaders, uploadOptions.downloadAgent);
var downloadProtocol = getProtocol(url);
var req = downloadProtocol.request(options, function(res) {
try {
if ((res.statusCode == 301) || (res.statusCode == 302)) {
if (redirectCount >= uploadOptions.maxRedirects) {
return callback('Redirect count reached. Aborting upload.');
}
var location = res.headers.location;
if (location) {
redirectCount++;
var redirectUrl = parseUri(location);
return head(redirectUrl, uploadOptions, redirectCount, callback);
}
}
if ((res.statusCode < 200) || (res.statusCode >= 300)) {
return callback('Invalid response from remote file server. statusCode: ' + res.statusCode);
}
var contentLength = parseInt(res.headers['content-length'], 10);
if (isNaN(contentLength)) { //this shouldn't happen, but it does
return callback('Remote web server returned an invalid content length');
}
if (!validateFileSize(uploadOptions.maxFileSize, contentLength)) {
return callback('File is too large. maxFileSize: ' + uploadOptions.maxFileSize + ', content-length: ' + contentLength);
}
//can we bail out early?
if (uploadOptions.downloadFileName) {
return callback(null, url, contentLength, uploadOptions.downloadFileName);
}
//no download specified, attempt to parse one out
var file, ext, mimeExt;
var contentType = res.headers['content-type'].split(';')[0];
//attempt to get the filename from the url
file = sanitizeFileName(path.basename(url.pathname));
if (file) {
ext = path.extname(file);
file = file.replace(ext, '');
ext = ext.replace('.', '');
if (ext) {
mimeExt = mimetypes.extension(contentType);
if (mimeExt) {
if (ext.toLowerCase() !== '.' + mimeExt.toLowerCase()) {
ext = mimeExt;
}
}
}
}
//default file name if we couldn't parse one
if (!file) { file = 'poster'; }
//default file extension if we cannot find one (unlikely)
if (!ext) {
ext = 'unk';
if (contentType) {
mimeExt = mimetypes.extension(contentType);
if (mimeExt) {
ext = mimeExt;
}
}
}
return callback(null, url, contentLength, file + '.' + ext);
}
catch (e) {
callback(e);
}
});
req.on('error', function(e) {
callback(e);
});
req.end();
}
function parseUri(uri) {
var uriRes = { host: null, path: uri, isValidUrl: false, protocol: null };
var parsedUri = url.parse(uri);
if ((parsedUri.protocol === 'http:') || (parsedUri.protocol === 'https:')) {
uriRes.isValidUrl = true;
uriRes.protocol = parsedUri.protocol;
uriRes.path = parsedUri.path;
uriRes.host = parsedUri.hostname;
uriRes.port = parsedUri.port;
uriRes.pathname = parsedUri.pathname;
}
return uriRes;
}
function getProtocol(url) {
return (url.protocol === 'https:') ? https : http;
}
function getMultipartForm(fields, fileFieldName, fileName, fileContentType) {
var form = '';
if (fields) {
for(var field in fields) {
form += '--' + BOUNDRARY + '\r\n';
form += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n';
form += fields[field] + '\r\n';
}
}
form += '--' + BOUNDRARY + '\r\n';
form += 'Content-Disposition: form-data; name="' + fileFieldName + '"; filename="' + fileName + '"\r\n';
form += 'Content-Type: ' + fileContentType + '\r\n\r\n';
return new Buffer(form);
}
function getRequestOptions(method, url, headers, agent) {
var options = {
method: method,
host: url.host,
path: url.path,
port: url.port,
headers: headers
};
//custom agent support
if (agent) {
options.agent = agent;
}
return options;
}
function validateFileSize(maxFileSize, fileSize) {
if (maxFileSize > 0) {
if (fileSize > maxFileSize) {
return false;
}
}
return true;
}
function sanitizeFileName(fileName) {
var re = new RegExp('[\\/:"*?<>|]+', "mg");
var sanitized = fileName.replace(re, '');
return (sanitized.length > 0) ? sanitized : null;
}
return {
post: function(uri, options, callback) {
if (!uri) return callback('Invalid url or file path argument');
if (!options) return callback('Invalid options argument');
if (!options.uploadUrl) return callback('Invalid upload url argument');
var uploadUrl = parseUri(options.uploadUrl);
if (!uploadUrl.isValidUrl) return callback('Invalid upload url argument');
var uploadOptions = {
method: 'POST',
maxFileSize: 0,
fileId: 'Filedata',
maxRedirects: 5,
fileContentType: 'application/octet-stream'
};
//set default upload options
for (var attr in options) { uploadOptions[attr] = options[attr]; }
//one agent to rule them all?
if (options.agent) {
options.downloadAgent = options.agent;
options.uploadAgent = options.agent;
}
//one headers to rule them all?
if (options.headers) {
options.downloadHeaders = options.headers;
options.uploadHeaders = options.headers;
}
//lets do this
try {
var parsedUri = parseUri(uri);
if (parsedUri.isValidUrl) {
head(parsedUri, uploadOptions, 0, function(err, fileUrl, fileSize, fileName) {
if (err) return callback(err);
upload(uploadUrl, fileUrl, uploadOptions, fileSize, fileName, callback);
});
}
else {
fs.exists(uri, function(exists) {
if (!exists) return callback('File does not exist on the file system.');
fs.stat(uri, function(err, stats) {
if (err) return callback(err);
if (!validateFileSize(uploadOptions.maxFileSize, stats.size)) {
return callback('File is too large, maxFileSize: ' + uploadOptions.maxFileSize + ', size: ' + stats.size);
}
var fileName = uploadOptions.fileName ? uploadOptions.fileName : path.basename(uri);
upload(uploadUrl, parsedUri, uploadOptions, stats.size, fileName, callback);
});
});
}
}
catch (e) {
callback(e);
}
}
};
})();