index.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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. function noop () {}
  11. module.exports = function (urls, opts, cb) {
  12. if (!Array.isArray(urls)) urls = [urls]
  13. if (urls.length === 1) opts.singleTarget = true
  14. var defaultProps = {}
  15. if (opts.sockets) {
  16. var sockets = +opts.sockets
  17. defaultProps.pool = {maxSockets: sockets}
  18. }
  19. if (opts.proxy) {
  20. defaultProps.proxy = opts.proxy
  21. }
  22. if (opts.strictSSL !== null) {
  23. defaultProps.strictSSL = opts.strictSSL
  24. }
  25. if (Object.keys(defaultProps).length > 0) {
  26. request = request.defaults(defaultProps)
  27. }
  28. var downloads = []
  29. var errors = []
  30. var pending = 0
  31. var truncated = urls.length * 2 >= (process.stdout.rows - 15)
  32. urls.forEach(function (url) {
  33. debug('start dl', url)
  34. pending++
  35. var dl = startDownload(url, opts, function done (err) {
  36. debug('done dl', url, pending)
  37. if (err) {
  38. debug('error dl', url, err)
  39. errors.push(err)
  40. dl.error = err.message
  41. }
  42. if (truncated) {
  43. var i = downloads.indexOf(dl)
  44. downloads.splice(i, 1)
  45. downloads.push(dl)
  46. }
  47. if (--pending === 0) {
  48. render()
  49. cb(errors.length ? errors : undefined)
  50. }
  51. })
  52. downloads.push(dl)
  53. dl.on('start', function (progressStream) {
  54. throttledRender()
  55. })
  56. dl.on('progress', function (data) {
  57. debug('progress', url, data.percentage)
  58. dl.speed = data.speed
  59. if (dl.percentage === 100) render()
  60. else throttledRender()
  61. })
  62. })
  63. var _log = opts.quiet ? noop : log
  64. render()
  65. var throttledRender = throttle(render, opts.frequency || 250)
  66. if (opts.singleTarget) return downloads[0]
  67. else return downloads
  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(45 * pct / 100)).join('=') + '>'
  86. while (bar.length < 45) 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. function startDownload (url, opts, cb) {
  97. var targetName = path.basename(url).split('?')[0]
  98. if (opts.singleTarget && opts.target) targetName = opts.target
  99. var target = path.resolve(opts.dir || process.cwd(), targetName)
  100. if (opts.resume) {
  101. resume(url, opts, cb)
  102. } else {
  103. download(url, opts, cb)
  104. }
  105. var progressEmitter = new EventEmitter()
  106. progressEmitter.target = target
  107. progressEmitter.speed = 0
  108. progressEmitter.percentage = 0
  109. return progressEmitter
  110. function resume (url, opts, cb) {
  111. fs.stat(target, function (err, stats) {
  112. if (err && err.code === 'ENOENT') {
  113. return download(url, opts, cb)
  114. }
  115. if (err) {
  116. return cb(err)
  117. }
  118. var offset = stats.size
  119. var req = request.get(url)
  120. req.on('error', cb)
  121. req.on('response', function (resp) {
  122. resp.destroy()
  123. var length = parseInt(resp.headers['content-length'], 10)
  124. // file is already downloaded.
  125. if (length === offset) return cb()
  126. if (!isNaN(length) && length > offset && /bytes/.test(resp.headers['accept-ranges'])) {
  127. opts.range = [offset, length]
  128. }
  129. download(url, opts, cb)
  130. })
  131. })
  132. }
  133. function download (url, opts, cb) {
  134. var headers = opts.headers || {}
  135. if (opts.range) {
  136. headers.Range = 'bytes=' + opts.range[0] + '-' + opts.range[1]
  137. }
  138. var read = request(url, { headers: headers })
  139. read.on('error', cb)
  140. read.on('response', function (resp) {
  141. debug('response', url, resp.statusCode)
  142. if (resp.statusCode > 299 && !opts.force) return cb(new Error('GET ' + url + ' returned ' + resp.statusCode))
  143. var write = fs.createWriteStream(target, {flags: opts.resume ? 'a' : 'w'})
  144. write.on('error', cb)
  145. write.on('finish', cb)
  146. var fullLen
  147. var contentLen = Number(resp.headers['content-length'])
  148. var range = resp.headers['content-range']
  149. if (range) {
  150. fullLen = Number(range.split('/')[1])
  151. } else {
  152. fullLen = contentLen
  153. }
  154. progressEmitter.fileSize = fullLen
  155. if (range) {
  156. var downloaded = fullLen - contentLen
  157. }
  158. var progressStream = progress({ length: fullLen, transferred: downloaded }, onprogress)
  159. progressEmitter.emit('start', progressStream)
  160. resp
  161. .pipe(progressStream)
  162. .pipe(write)
  163. })
  164. function onprogress (p) {
  165. var pct = p.percentage
  166. progressEmitter.progress = p
  167. progressEmitter.percentage = pct
  168. progressEmitter.emit('progress', p)
  169. }
  170. }
  171. }
  172. }