|
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 |
| 1 | +var axios = require('axios') |
| 2 | +var randomAccess = require('random-access-storage') |
| 3 | +var logger = require('./lib/logger') |
| 4 | +var isNode = require('./lib/is-node') |
| 5 | +var validUrl = require('./lib/valid-url') |
| 6 | + |
| 7 | +var defaultOptions = { |
| 8 | + responseType: 'arraybuffer', |
| 9 | + timeout: 60000, |
| 10 | + maxRedirects: 10, // follow up to 10 HTTP 3xx redirects |
| 11 | + maxContentLength: 50 * 1000 * 1000 // cap at 50MB, |
29 | 12 | }
|
30 | 13 |
|
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() |
| 14 | +var randomAccessHttp = function (filename, options) { |
| 15 | + var url = options && options.url |
| 16 | + if (!filename || (!validUrl(filename) && !validUrl(url))) { |
| 17 | + throw new Error('Expect first argument to be a valid URL or a relative path, with url set in options') |
52 | 18 | }
|
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 |
| - }) |
| 19 | + var axiosConfig = Object.assign({}, defaultOptions) |
| 20 | + if (isNode) { |
| 21 | + var http = require('http') |
| 22 | + var https = require('https') |
| 23 | + // keepAlive pools and reuses TCP connections, so it's faster |
| 24 | + axiosConfig.httpAgent = new http.Agent({ keepAlive: true }) |
| 25 | + axiosConfig.httpsAgent = new https.Agent({ keepAlive: true }) |
109 | 26 | }
|
110 |
| - |
111 |
| - function onBuf (buf) { |
112 |
| - return cb(null, buf) |
| 27 | + if (options) { |
| 28 | + if (url) axiosConfig.baseURL = url |
| 29 | + if (options.timeout) axiosConfig.timeout = options.timeout |
| 30 | + if (options.maxRedirects) axiosConfig.maxRedirects = options.maxRedirects |
| 31 | + if (options.maxContentLength) axiosConfig.maxContentLength = options.maxContentLength |
113 | 32 | }
|
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) |
| 33 | + var _axios = axios.create(axiosConfig) |
| 34 | + var file = filename |
| 35 | + var verbose = !!(options && options.verbose) |
| 36 | + |
| 37 | + return randomAccess({ |
| 38 | + open: function (req) { |
| 39 | + if (verbose) logger.log('Testing to see if server accepts range requests', url, file) |
| 40 | + // should cache this |
| 41 | + _axios.head(file) |
| 42 | + .then((response) => { |
| 43 | + if (verbose) logger.log('Received headers from server') |
| 44 | + var accepts = response.headers['accept-ranges'] |
| 45 | + if (accepts && accepts.toLowerCase().indexOf('bytes') !== -1) { |
| 46 | + if (response.headers['content-length']) this.length = response.headers['content-length'] |
| 47 | + return req.callback(null) |
| 48 | + } |
| 49 | + return req.callback(new Error('Accept-Ranges does not include "bytes"')) |
| 50 | + }) |
| 51 | + .catch((err) => { |
| 52 | + if (verbose) logger.log('Error opening', file, '-', err) |
| 53 | + req.callback(err) |
| 54 | + }) |
| 55 | + }, |
| 56 | + read: function (req) { |
| 57 | + var range = `${req.offset}-${req.offset + req.size - 1}` |
| 58 | + var headers = { |
| 59 | + range: `bytes=${range}` |
| 60 | + } |
| 61 | + if (verbose) logger.log('Trying to read', file, headers.Range) |
| 62 | + _axios.get(file, { headers: headers }) |
| 63 | + .then((response) => { |
| 64 | + if (!response.headers['content-range']) throw new Error('Server did not return a byte range') |
| 65 | + if (response.status !== 206) throw new Error('Bad response: ' + response.status) |
| 66 | + var expectedRange = `bytes ${range}/${this.length}` |
| 67 | + if (response.headers['content-range'] !== expectedRange) throw new Error('Server returned unexpected range: ' + response.headers['content-range']) |
| 68 | + if (req.offset + req.size > this.length) throw new Error('Could not satisfy length') |
| 69 | + if (verbose) logger.log('read', JSON.stringify(response.headers, null, 2)) |
| 70 | + req.callback(null, Buffer.from(response.data)) |
| 71 | + }) |
| 72 | + .catch((err) => { |
| 73 | + if (verbose) { |
| 74 | + logger.log('error', file, headers.Range) |
| 75 | + logger.log(err, err.stack) |
| 76 | + } |
| 77 | + req.callback(err) |
| 78 | + }) |
| 79 | + }, |
| 80 | + write: function (req) { |
| 81 | + // This is a dummy write function - does not write, but fails silently |
| 82 | + if (verbose) logger.log('trying to write', file, req.offset, req.data) |
| 83 | + req.callback() |
| 84 | + }, |
| 85 | + del: function (req) { |
| 86 | + // This is a dummy del function - does not del, but fails silently |
| 87 | + if (verbose) logger.log('trying to del', file, req.offset, req.size) |
| 88 | + req.callback() |
| 89 | + }, |
| 90 | + close: function (req) { |
| 91 | + if (_axios.defaults.httpAgent) { |
| 92 | + _axios.defaults.httpAgent.destroy() |
| 93 | + } |
| 94 | + if (_axios.defaults.httpsAgent) { |
| 95 | + _axios.defaults.httpsAgent.destroy() |
| 96 | + } |
| 97 | + req.callback() |
| 98 | + } |
142 | 99 | })
|
143 | 100 | }
|
144 | 101 |
|
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 |
| -} |
| 102 | +module.exports = randomAccessHttp |
0 commit comments