|
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