fast-download
Version:
accelerated (multiple connections) http download stream
216 lines (214 loc) • 7.9 kB
JavaScript
var http = require('http');
var util = require('util');
var path = require('path');
var fs = require('fs');
var Readable = require('stream').Readable;
var EventEmitter = require('events').EventEmitter;
var _ = require('lodash');
var async = require('async');
var request = require('request');
http.globalAgent.maxSockets = 999;
function FastDownload(url, options, cb) {
var self = this;
Readable.apply(self);
self.headers = null;
self.file_size = null;
self.size = null;
self.downloaded = 0;
self.position = 0;
self.chunks = [];
self._url = url;
self._options = _.assign(_.clone(FastDownload.defaultOptions, true), options || {});
self._buffers = [];
var next = function (error) {
if (cb && (typeof cb === 'function')) { cb(error, self); }
if (error) { self.emit('error', error); return; }
self.emit('start', self);
};
request.head(self._url, _.clone(self._options, true), function (error, response) {
if (error) { next(error); return; }
if (response.statusCode !== 200) { next(new Error('http status code ' + response.statusCode)); return; }
self.headers = response.headers;
self.file_size = Number(self.headers['content-length']);
if (self._options.destFile) {
self._init_file_stream.call(self, function (error) {
if (error) { next(error); return; }
self._init_http(next);
});
} else {
self.once('end', function () { self.emit('done'); })
self._init_http(next);
}
});
}
util.inherits(FastDownload, Readable);
FastDownload.prototype._init_file_stream = function (cb) {
var self = this;
var open_file = function (append) {
var file_stream = fs.createWriteStream(self._options.destFile, { flags: append ? 'a' : 'w' });
file_stream.on('error', cb);
file_stream.on('open', function () {
file_stream.removeListener('error', cb);
file_stream.on('error', function (error) {
self.emit('error', error);
self.abort();
});
file_stream.on('finish', function () { self.emit('done'); });
self.pipe(file_stream);
cb(null);
});
};
if (self._options.resumeFile && (self.headers['accept-ranges'] === 'bytes')) {
fs.stat(self._options.destFile, function (error, stat) {
if (error) { open_file(false); return; }
self._options.start = stat.size;
open_file(true);
});
} else {
open_file(false);
}
};
FastDownload.prototype._init_http = function (cb) {
var self = this;
if (!self._options.end) { self._options.end = self.file_size; }
self.size = self._options.end - self._options.start;
if (!self._options.chunkSize) { self._options.chunkSize = Math.ceil(self.size / self._options.chunksAtOnce); }
var accept_ranges = self.headers['accept-ranges'] === 'bytes';
if ((!accept_ranges) && ((self._options.start !== 0) || (self._options.end !== self.file_size))) {
cb(new Error("the server will not accept range requests")); return;
}
if (accept_ranges) {
self._init_fast_http(cb);
} else {
self._init_normal_http(cb);
}
};
FastDownload.prototype._init_normal_http = function (cb) {
var self = this;
self._request = request(self._url, _.clone(self._options, true));
self._request.on('error', function (error) {
self.emit('error', error)
});
self._request.on('data', function (data) {
self.downloaded += data.length;
self._buffers.push(data);
self.read(0);
});
self._request.on('end', function () {
self._buffers.push(null);
self.read(0);
});
cb(null);
};
FastDownload.prototype._init_fast_http = function (cb) {
var self = this;
var chunk_numbers = _.range(Math.ceil(self.size / self._options.chunkSize));
var tasks = _.map(chunk_numbers, function (chunk_number) {
return function (cb) {
var chunk = new Chunk(self, chunk_number);
self.chunks.push(chunk);
chunk.on('error', cb);
chunk.on('end', function () {
if (!chunk._buffers) {
if (self.chunks[0] !== chunk) { throw new Error('this chunk SHOULD be the leading chunk in download.chunks'); }
self.chunks.shift();
var complete_chunk;
while (self.chunks[0] && (self.chunks[0].position === self.chunks[0].size)) {
complete_chunk = self.chunks.shift();
self._buffers = self._buffers.concat(complete_chunk._buffers);
complete_chunk._buffers = null;
}
if (self.chunks[0]) { self.chunks[0]._start_piping(); }
}
cb(null);
});
if (chunk_number === 0) { chunk._start_piping(); }
};
});
async.parallelLimit(tasks, self._options.chunksAtOnce, function (error) {
if (error) {
self.abort();
self.emit('error', error);
//no return here
}
self._buffers.push(null);
self.read(0);
});
cb(null);
};
FastDownload.prototype._read = function () {
var self = this;
if (self._buffers.length === 0) {
self.push(new Buffer(0));
return;
}
var loop = function () {
var buffer = self._buffers.shift();
if (buffer === undefined) { return; }
if (buffer === null) { self.push(null); return; }
self.position += buffer.length;
if (self.push(buffer)) { loop(); }
};
loop();
};
FastDownload.prototype.abort = function () {
var self = this;
if (self._request) {
self._request.abort();
}
_.each(self.chunks, function (chunk) {
chunk._abort();
});
};
FastDownload.defaultOptions = {
destFile: null,
resumeFile: false,
start: 0,
end: null,
chunksAtOnce: 3,
chunkSize: null
};
function Chunk(dl, number) {
var self = this;
EventEmitter.apply(self);
self.offset = dl._options.start + (number * dl._options.chunkSize);
self.size = Math.min(dl._options.chunkSize, dl.size - (number * dl._options.chunkSize));
self.position = 0;
self._dl = dl;
self._buffers = [];
var req_options = _.clone(dl._options, true);
req_options.headers = req_options.headers || {};
req_options.headers.range = 'bytes=' + (self.offset + self.position) + '-' + (self.offset + self.size - 1);
self._req = request(dl._url, req_options);
self._req.on('error', function (error) { self.emit('error', error); });
self._req.on('end', function () {
if (self.position !== self.size) {
self.emit('error', new Error('expected ' + self.size + ' bytes but received ' + self.position + ' bytes'));
}
});
self._req.on('data', function (data) {
self.position += data.length;
dl.downloaded += data.length;
if (self._buffers) {
self._buffers.push(data);
} else {
dl._buffers.push(data);
dl.read(0);
}
if (self.position === self.size) {
self.emit('end');
}
});
}
util.inherits(Chunk, EventEmitter);
Chunk.prototype._start_piping = function () {
var self = this;
self._dl._buffers = self._dl._buffers.concat(self._buffers);
self._buffers = null;
self._dl.read(0);
};
Chunk.prototype._abort = function () {
var self = this;
self._req.abort();
};
module.exports = FastDownload;