1 | var request = require('request')
|
2 | var fs = require('fs')
|
3 | var path = require('path')
|
4 | var log = require('single-line-log').stdout
|
5 | var progress = require('progress-stream')
|
6 | var prettyBytes = require('pretty-bytes')
|
7 | var throttle = require('throttleit')
|
8 | var EventEmitter = require('events').EventEmitter
|
9 | var debug = require('debug')('nugget')
|
10 |
|
11 | function noop () {}
|
12 |
|
13 | module.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 |
|
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 | }
|