webseeded-torrent-generator
Version:
Generate webseeded torrent files from urls to files.
137 lines (113 loc) • 4.8 kB
JavaScript
;
var http = require('http');
var URL = require('url');
var assert = require('assert');
var q = require('q');
var BunyanPromiseLogger = require('bunyan-promise');
var _ = require('lodash');
var promiseLog = new BunyanPromiseLogger({
name: 'http-fs'
});
var encodeURIReservedCharacters = function (url) {
var reservedCharacters = ['#', '&', '+', '=', '?', '@'];
var encodedUri = _.reduce(reservedCharacters, function (memo, reservedCharacter) {
// escape all the characters, as some of them are reserved characters for regexps...ugh
// escaping non-reserved characters has no effect
var regexp = new RegExp('\\' + reservedCharacter, 'g');
var encodedReservedCharacter = encodeURIComponent(reservedCharacter);
return memo.replace(regexp, encodedReservedCharacter);
}, url);
return encodedUri;
};
var HttpFile = function (url) {
this.url = encodeURIReservedCharacters(encodeURI(url));
};
HttpFile.prototype.url = function () {
return this.url;
};
HttpFile.prototype.hostname = function () {
return URL.parse(this.url).hostname;
};
HttpFile.prototype.path = function () {
var path = URL.parse(this.url).path;
var hash = URL.parse(this.url).hash;
return path + (hash || '');
};
var promisedHttpRequest = function (options) {
var defer = q.defer();
http.get(options, function (response) {
defer.resolve(response);
}).on('error', function (error) {
defer.reject(error);
});
return defer.promise;
};
HttpFile.prototype.size = function () {
var ret = promisedHttpRequest({
method: 'HEAD',
hostname: this.hostname(),
path: this.path(),
headers: {
connection: 'close'
}
}).then(function (response) {
if (response.statusCode === 301 || response.statusCode === 302) {
assert(response.headers.hasOwnProperty('location'), 'redirect without a new location');
this.url = response.headers.location;
return this.size();
}
// require 200 back from HEAD request
assert(response.statusCode === 200, 'http server does not support HEAD method');
assert(response.headers.hasOwnProperty('content-length'), 'content length header not found');
var size = parseInt(response.headers['content-length'], 10);
assert(size >= 0);
return q.resolve(size);
}.bind(this));
promiseLog.trace(ret, 'file size read - ' + this.url);
return ret;
};
HttpFile.prototype.read = function (position, length) {
var start = position;
var end = position + length - 1;
var range = 'bytes=' + start + '-' + end;
var headers = {
range: range
};
var ret = promisedHttpRequest({
method: 'GET',
hostname: this.hostname(),
path: this.path(),
headers: headers
}).then(function (response) {
if (response.statusCode === 301 || response.statusCode === 302) {
assert(response.headers.hasOwnProperty('location'), 'redirect without a new location');
this.url = response.headers.location;
return this.read(position, length);
}
var defer = q.defer();
//require partial content status code
assert(response.statusCode === 206, 'http server does not support range requests - ' + response.statusCode);
//require that the server returns the size of the entire piece of content
var contentRangeHeader = response.headers['content-range'];
assert(typeof contentRangeHeader === 'string', 'content range header not found');
//parse out the content offset/length information
var match = contentRangeHeader.match(/bytes (\d+)-(\d+)\/\d+/);
assert(typeof match === 'object' && match.length === 3, 'invalid content range header');
assert(parseInt(match[1], 10) === start, 'incorrect range starting byte ' + parseInt(match[1], 10) + '/' + start);
assert(parseInt(match[2], 10) === end, 'incorrect range ending byte ' + contentRangeHeader);
var contentLengthHeader = response.headers['content-length'];
assert(typeof contentLengthHeader === 'string', 'content length header not found');
assert(parseInt(contentLengthHeader, 10) === length, 'incorrect content length returned ' + contentLengthHeader);
var buffers = [];
response.on('data', function (buffer) {
buffers.push(buffer);
});
response.on('end', function () {
defer.resolve(Buffer.concat(buffers));
});
return defer.promise;
}.bind(this));
promiseLog.trace(ret, 'file read - ' + this.url + ' - ' + position + '-' + (position + length));
return ret;
};
module.exports = HttpFile;