UNPKG

5.5 kBJavaScriptView Raw
1var request = require('request')
2var fs = require('fs')
3var path = require('path')
4var log = require('single-line-log').stdout
5var progress = require('progress-stream')
6var prettyBytes = require('pretty-bytes')
7var throttle = require('throttleit')
8var EventEmitter = require('events').EventEmitter
9var debug = require('debug')('nugget')
10
11function noop () {}
12
13module.exports = function(urls, opts, cb) {
14 if (!Array.isArray(urls)) urls = [urls]
15 if (urls.length === 1) opts.singleTarget = true
16 if (opts.sockets) {
17 var sockets = +opts.sockets
18 request = request.defaults({pool: {maxSockets: sockets}})
19 }
20 var downloads = []
21 var errors = []
22 var pending = 0
23 var truncated = urls.length * 2 >= (process.stdout.rows - 15)
24
25 urls.forEach(function (url) {
26 debug('start dl', url)
27 pending++
28 var dl = startDownload(url, opts, function done (err) {
29 debug('done dl', url, pending)
30 if (err) {
31 debug('error dl', url, err)
32 errors.push(err)
33 dl.error = err.message
34 }
35 if (truncated) {
36 var i = downloads.indexOf(dl)
37 downloads.splice(i, 1)
38 downloads.push(dl)
39 }
40 if (--pending === 0) {
41 render()
42 cb(errors.length ? errors : undefined)
43 }
44 })
45
46 downloads.push(dl)
47
48 dl.on('start', function (progressStream) {
49 throttledRender()
50 })
51
52 dl.on('progress', function(data) {
53 debug('progress', url, data.percentage)
54
55 dl.speed = data.speed
56 if (dl.percentage === 100) render()
57 else throttledRender()
58 })
59 })
60
61 var _log = opts.verbose ? log : noop
62 render()
63 var throttledRender = throttle(render, opts.frequency || 250)
64
65 if (opts.singleTarget) return downloads[0]
66 else return downloads
67
68 function render () {
69 var height = process.stdout.rows
70 var rendered = 0
71 var output = ""
72 var totalSpeed = 0
73 downloads.forEach(function (dl) {
74 if (2 * rendered >= height - 15) return
75 rendered++
76 if (dl.error) {
77 output += 'Downloading '+path.basename(dl.target)+'\n'
78 output += 'Error: ' + dl.error + '\n'
79 return
80 }
81 var pct = dl.percentage
82 var speed = dl.speed
83 var total = dl.fileSize
84 totalSpeed += speed
85 var bar = Array(Math.floor(50 * pct / 100)).join('=')+'>'
86 while (bar.length < 50) bar += ' '
87 output += 'Downloading '+path.basename(dl.target)+'\n'+
88 '['+bar+'] '+pct.toFixed(1)+'%'
89 if (total) output += ' of ' + prettyBytes(total)
90 output += ' (' + prettyBytes(speed) + '/s)\n'
91 })
92 if (rendered < downloads.length) output += '\n... and ' + (downloads.length - rendered) + ' more\n'
93 if (downloads.length > 1) output += '\nCombined Speed: ' + prettyBytes(totalSpeed) + '/s\n'
94 _log(output)
95 }
96
97 function startDownload (url, opts, cb) {
98 var targetName = path.basename(url)
99 if (opts.singleTarget && opts.target) targetName = opts.target
100 var target = path.resolve(opts.dir || process.cwd(), targetName)
101 if (opts.resume) {
102 resume(url, opts, cb)
103 } else {
104 download(url, opts, cb)
105 }
106
107 var progressEmitter = new EventEmitter()
108 progressEmitter.target = target
109 progressEmitter.speed = 0
110 progressEmitter.percentage = 0
111
112 return progressEmitter
113
114 function resume(url, opts, cb) {
115 fs.stat(target, function (err, stats) {
116 if (err && err.code === 'ENOENT') {
117 return download(url, opts, cb)
118 }
119 if (err) {
120 return cb(err)
121 }
122 var offset = stats.size
123 var req = request.get(url)
124
125 req.on('error', cb)
126 req.on('response', function(resp) {
127 resp.destroy()
128
129 var length = parseInt(resp.headers['content-length'], 10)
130
131 // file is already downloaded.
132 if (length === offset) return cb()
133
134 if (!isNaN(length) && length > offset && /bytes/.test(resp.headers['accept-ranges'])) {
135 opts.range = [offset, length]
136 }
137
138 download(url, opts, cb)
139 })
140 })
141 }
142
143 function download(url, opts, cb) {
144 var headers = opts.headers || {}
145 if (opts.range) {
146 headers.Range = 'bytes=' + opts.range[0] + '-' + opts.range[1]
147 }
148 var read = request(url, { headers: headers })
149 var speed = "0 Kb"
150
151 read.on('error', cb)
152 read.on('response', function(resp) {
153 debug('response', url, resp.statusCode)
154 if (resp.statusCode > 299 && !opts.force) return cb(new Error('GET ' + url + ' returned ' + resp.statusCode))
155 var write = fs.createWriteStream(target, {flags: opts.resume ? 'a' : 'w'})
156 write.on('error', cb)
157 write.on('finish', cb)
158
159 var fullLen
160 var contentLen = Number(resp.headers['content-length'])
161 var range = resp.headers['content-range']
162 if (range) {
163 fullLen = Number(range.split('/')[1])
164 } else {
165 fullLen = contentLen
166 }
167
168 progressEmitter.fileSize = fullLen
169 if (range) {
170 var downloaded = fullLen - contentLen
171 }
172 var progressStream = progress({ length: fullLen, transferred: downloaded }, onprogress)
173 progressEmitter.emit('start', progressStream)
174
175 resp
176 .pipe(progressStream)
177 .pipe(write)
178 })
179
180 function onprogress (p) {
181 var pct = p.percentage
182 progressEmitter.progress = p
183 progressEmitter.percentage = pct
184 progressEmitter.emit('progress', p)
185 }
186 }
187 }
188}