|
| 1 | +var events = require('events') |
| 2 | +var inherits = require('inherits') |
| 3 | +var http = require('http') |
| 4 | +var https = require('https') |
| 5 | +var url = require('url') |
| 6 | +var xtend = require('xtend') |
| 7 | +var concat = require('concat-stream') |
| 8 | +var pump = require('pump') |
| 9 | +var limitStream = require('size-limit-stream') |
| 10 | + |
| 11 | +module.exports = RandomAccessHTTP |
| 12 | + |
| 13 | +function RandomAccessHTTP (fileUrl, opts) { |
| 14 | + if (!(this instanceof RandomAccessHTTP)) return new RandomAccessHTTP(fileUrl, opts) |
| 15 | + if (!opts) opts = {} |
| 16 | + |
| 17 | + events.EventEmitter.call(this) |
| 18 | + |
| 19 | + this.url = fileUrl |
| 20 | + this.urlObj = url.parse(fileUrl) |
| 21 | + this.client = { |
| 22 | + http: http, |
| 23 | + https: https |
| 24 | + }[this.urlObj.protocol.split(':')[0]] |
| 25 | + this.readable = opts.readable !== false |
| 26 | + this.writable = false |
| 27 | + this.length = opts.length || 0 |
| 28 | + this.opened = false |
| 29 | +} |
| 30 | + |
| 31 | +inherits(RandomAccessHTTP, events.EventEmitter) |
| 32 | + |
| 33 | +RandomAccessHTTP.prototype.open = function (cb) { |
| 34 | + var self = this |
| 35 | + |
| 36 | + this.keepAliveAgent = new this.client.Agent({ keepAlive: true }) |
| 37 | + var reqOpts = xtend(this.urlObj, { |
| 38 | + method: 'HEAD', |
| 39 | + agent: this.keepAliveAgent |
| 40 | + }) |
| 41 | + var req = this.client.request(reqOpts, onres) |
| 42 | + |
| 43 | + function onres (res) { |
| 44 | + if (res.statusCode !== 200) return cb(new Error('Bad response: ' + res.statusCode)) |
| 45 | + if (headersInvalid(res.headers)) { |
| 46 | + return cb(new Error("Source doesn't support 'accept-ranges'")) |
| 47 | + } |
| 48 | + self.opened = true |
| 49 | + if (res.headers['content-length']) self.length = res.headers['content-length'] |
| 50 | + self.emit('open') |
| 51 | + cb() |
| 52 | + } |
| 53 | + |
| 54 | + req.on('error', (e) => { |
| 55 | + return cb(new Error(`problem with request: ${e.message}`)) |
| 56 | + }) |
| 57 | + |
| 58 | + req.end() |
| 59 | +} |
| 60 | + |
| 61 | +function headersInvalid (headers) { |
| 62 | + if (!headers['accept-ranges']) return true |
| 63 | + if (headers['accept-ranges'] !== 'bytes') return true |
| 64 | +} |
| 65 | + |
| 66 | +RandomAccessHTTP.prototype.write = function (offset, buf, cb) { |
| 67 | + if (!cb) cb = noop |
| 68 | + if (!this.opened) return openAndWrite(this, offset, buf, cb) |
| 69 | + if (!this.writable) return cb(new Error('URL is not writable')) |
| 70 | + cb(new Error('Write Not Implemented')) |
| 71 | +} |
| 72 | + |
| 73 | +RandomAccessHTTP.prototype.read = function (offset, length, cb) { |
| 74 | + if (!this.opened) return openAndRead(this, offset, length, cb) |
| 75 | + if (!this.readable) return cb(new Error('URL is not readable')) |
| 76 | + |
| 77 | + var self = this |
| 78 | + |
| 79 | + var range = `${offset}-${offset + length - 1}` // 0 index'd |
| 80 | + var reqOpts = xtend(this.urlObj, { |
| 81 | + method: 'GET', |
| 82 | + agent: this.keepAliveAgent, |
| 83 | + headers: { |
| 84 | + Accept: '*/*', |
| 85 | + Range: `bytes=${range}` |
| 86 | + } |
| 87 | + }) |
| 88 | + |
| 89 | + var req = this.client.request(reqOpts, onres) |
| 90 | + |
| 91 | + req.on('error', (e) => { |
| 92 | + return cb(new Error(`problem with read request: ${e.message}`)) |
| 93 | + }) |
| 94 | + |
| 95 | + req.end() |
| 96 | + |
| 97 | + function onres (res) { |
| 98 | + if (!res.headers['content-range']) return cb(new Error('Server did not return a byte range')) |
| 99 | + if (res.statusCode !== 206) return cb(new Error('Bad response: ' + res.statusCode)) |
| 100 | + var expectedRange = `bytes ${range}/${self.length}` |
| 101 | + if (res.headers['content-range'] !== expectedRange) return cb(new Error('Server returned unexpected range: ' + res.headers['content-range'])) |
| 102 | + if (offset + length > self.length) return cb(new Error('Could not satisfy length')) |
| 103 | + var concatStream = concat(onBuf) |
| 104 | + var limiter = limitStream(length + 1) // blow up if we get more data back than needed |
| 105 | + |
| 106 | + pump(res, limiter, concatStream, function (err) { |
| 107 | + if (err) return cb(new Error(`problem while reading stream: ${err}`)) |
| 108 | + }) |
| 109 | + } |
| 110 | + |
| 111 | + function onBuf (buf) { |
| 112 | + return cb(null, buf) |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +// function parseRangeHeader (rangeHeader) { |
| 117 | +// var range = {} |
| 118 | +// var byteRangeArr = rangeHeader.split(' ') |
| 119 | +// range.unit = byteRangeArr[0] |
| 120 | +// var ranges = byteRangeArr[1].split('/') |
| 121 | +// range.totalLength = ranges[1] |
| 122 | +// var startStop = ranges[0].split('-') |
| 123 | +// range.offset = startStop[0] |
| 124 | +// range.end = startStop[1] |
| 125 | +// range.length = range.end - range.offset |
| 126 | +// return range |
| 127 | +// } |
| 128 | + |
| 129 | +RandomAccessHTTP.prototype.close = function (cb) { |
| 130 | + this.opened = false |
| 131 | + this.keepAliveAgent.destroy() |
| 132 | + this.emit('close') |
| 133 | + cb(null) |
| 134 | +} |
| 135 | + |
| 136 | +function noop () {} |
| 137 | + |
| 138 | +function openAndRead (self, offset, length, cb) { |
| 139 | + self.open(function (err) { |
| 140 | + if (err) return cb(err) |
| 141 | + self.read(offset, length, cb) |
| 142 | + }) |
| 143 | +} |
| 144 | + |
| 145 | +function openAndWrite (self, offset, buf, cb) { |
| 146 | + self.open(function (err) { |
| 147 | + if (err) return cb(err) |
| 148 | + self.write(offset, buf, cb) |
| 149 | + }) |
| 150 | +} |
0 commit comments