UNPKG

webseeded-torrent-generator

Version:

Generate webseeded torrent files from urls to files.

137 lines (113 loc) 4.8 kB
'use strict'; 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;