Skip to content

Commit 4104dce

Browse files
authored
Merge pull request #16 from random-access-storage/merge-with-http-random-access
merge with http-random-access
2 parents d8bd457 + ebaa0b4 commit 4104dce

14 files changed

+384
-199
lines changed

.coveralls.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
repo_token: Ol7Ap8LS2ER32hLdx9evIMFgCSChJNueO

.eslintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "extends": ["standard"] }

.travis.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
language: node_js
2-
node_js:
3-
- 'node'
4-
- '4'
52
sudo: false
6-
cache:
7-
directories:
8-
- node_modules
3+
node_js:
4+
- 6
5+
- 8
6+
- 9
7+
script:
8+
- npm run test-travis
9+
after_script:
10+
- npm run report-coverage

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2016, Bret Comnes
1+
Copyright (c) 2016, Bret Comnes and contributors
22

33
Permission to use, copy, modify, and/or distribute this software for any
44
purpose with or without fee is hereby granted, provided that the above

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Continuous reading from a http(s) url using random offsets and lengths
66
npm install random-access-http
77
```
88

9-
[![Build Status](https://travis-ci.org/bcomnes/random-access-http.svg?branch=master)](https://travis-ci.org/bcomnes/random-access-http)
9+
[![Build Status](https://travis-ci.org/random-access-storage/random-access-http.svg?branch=master)](https://travis-ci.org/random-access-storage/random-access-http) [![Coverage Status](https://coveralls.io/repos/github/random-access-storage/random-access-http/badge.svg?branch=master)](https://coveralls.io/github/random-access-storage/random-access-http?branch=master)
1010

1111
## Why?
1212

@@ -34,13 +34,25 @@ file will use a keepalive agent to reduce the number http requests needed for th
3434

3535
## API
3636

37-
#### `var file = randomAccessHTTP(url)`
37+
#### `var file = randomAccessHTTP(url, [options])`
3838

39-
Create a new 'file' that reads from the provided `url`. The `url` can be either `http` or `https`.
39+
Create a new 'file' that reads from the provided `url`. The `url` can be either `http`, `https` or a relative path if url is set in options.
40+
41+
Options include:
42+
```js
43+
{
44+
url: string // Optionsal. The base url if first argument is relative
45+
verbose: boolean, // Optional. Default: false.
46+
timeout: number, // Optional. Default: 60000
47+
maxRedirects: number, // Optional. Default: 10
48+
maxContentLength: number, // Optional. Default: 50MB
49+
}
50+
```
4051

4152
#### `file.write(offset, buffer, [callback])`
4253

43-
Not implemented! Please let me know if you have opinions on how to implement this.
54+
**Not implemented!** Please let us know if you have opinions on how to implement this.
55+
This will silently fail with no data being writen.
4456

4557
#### `file.read(offset, length, callback)`
4658

example.js

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,12 @@
1-
var http = require('http')
2-
var https = require('https')
3-
var url = require('url')
1+
var raHttp = require('./')
42

5-
var urlObj = url.parse('https://ia800309.us.archive.org/2/items/Popeye_Nearlyweds/Popeye_Nearlyweds_512kb.mp4')
3+
var file = raHttp('/readme.md', { url: 'https://raw.githubusercontent.com/e-e-e/http-random-access/master/' })
64

7-
var options = Object.assign({}, urlObj, {
8-
method: 'HEAD',
9-
headers: {
10-
Connection: 'keep-alive'
5+
file.read(2, 10, (err, data) => {
6+
if (err) {
7+
console.log('Something went wrong!')
8+
console.log(err)
9+
return
1110
}
11+
console.log(data.toString())
1212
})
13-
14-
var client = {
15-
http: http,
16-
https: https
17-
}[urlObj.protocol.split(':')[0]]
18-
19-
var req = client.request(options, res => {
20-
console.log(`STATUS: ${res.statusCode}`)
21-
console.log(`HEADERS: ${JSON.stringify(res.headers, null, '\t')}`)
22-
res.setEncoding('utf8')
23-
res.on('data', (chunk) => {
24-
console.log(`BODY: ${chunk}`)
25-
})
26-
res.on('end', () => {
27-
console.log('No more data in response.')
28-
})
29-
})
30-
31-
req.on('error', (e) => {
32-
console.log(`problem with request: ${e.message}`)
33-
})
34-
35-
// write data to request body
36-
req.end()

index.js

Lines changed: 94 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,102 @@
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,
2912
}
3013

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')
5218
}
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 })
10926
}
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
11332
}
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+
}
14299
})
143100
}
144101

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

lib/is-node.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = !!(process && process.release && process.release.name === 'node')

lib/logger.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// This is simply for testing
2+
module.exports = {
3+
log: console.log
4+
}

lib/valid-url.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
var url = require('url')
2+
3+
module.exports = function (str) {
4+
if (typeof str !== 'string') return false
5+
var parsed = url.parse(str)
6+
return ['http:', 'https:'].includes(parsed.protocol)
7+
}

0 commit comments

Comments
 (0)