index.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. 'use strict'
  2. const debug = require('debug')('electron-download')
  3. const envPaths = require('env-paths')
  4. const fs = require('fs-extra')
  5. const rc = require('rc')
  6. const nugget = require('nugget')
  7. const os = require('os')
  8. const path = require('path')
  9. const pathExists = require('path-exists')
  10. const semver = require('semver')
  11. const sumchecker = require('sumchecker')
  12. class ElectronDownloader {
  13. constructor (opts) {
  14. this.opts = opts
  15. this.npmrc = {}
  16. try {
  17. rc('npm', this.npmrc)
  18. } catch (error) {
  19. console.error(`Error reading npm configuration: ${error.message}`)
  20. }
  21. }
  22. get baseUrl () {
  23. return process.env.NPM_CONFIG_ELECTRON_MIRROR ||
  24. process.env.npm_config_electron_mirror ||
  25. process.env.ELECTRON_MIRROR ||
  26. this.opts.mirror ||
  27. 'https://github.com/electron/electron/releases/download/v'
  28. }
  29. get middleUrl () {
  30. return process.env.ELECTRON_CUSTOM_DIR || this.opts.customDir || this.version
  31. }
  32. get urlSuffix () {
  33. return process.env.ELECTRON_CUSTOM_FILENAME || this.opts.customFilename || this.filename
  34. }
  35. get arch () {
  36. return this.opts.arch || os.arch()
  37. }
  38. get cache () {
  39. if (this.opts.cache) return this.opts.cache
  40. const oldCacheDirectory = path.join(os.homedir(), './.electron')
  41. if (pathExists.sync(path.join(oldCacheDirectory, this.filename))) {
  42. return oldCacheDirectory
  43. }
  44. // use passed argument or XDG environment variable fallback to OS default
  45. return envPaths('electron', {suffix: ''}).cache
  46. }
  47. get cachedChecksum () {
  48. return path.join(this.cache, `${this.checksumFilename}-${this.version}`)
  49. }
  50. get cachedZip () {
  51. return path.join(this.cache, this.filename)
  52. }
  53. get checksumFilename () {
  54. return 'SHASUMS256.txt'
  55. }
  56. get checksumUrl () {
  57. return `${this.baseUrl}${this.middleUrl}/${this.checksumFilename}`
  58. }
  59. get filename () {
  60. const type = `${this.platform}-${this.arch}`
  61. const suffix = `v${this.version}-${type}`
  62. if (this.chromedriver) {
  63. // Chromedriver started using Electron's version in asset name in 1.7.0
  64. if (semver.gte(this.version, '1.7.0')) {
  65. return `chromedriver-${suffix}.zip`
  66. } else {
  67. return `chromedriver-v2.21-${type}.zip`
  68. }
  69. } else if (this.mksnapshot) {
  70. return `mksnapshot-${suffix}.zip`
  71. } else if (this.ffmpeg) {
  72. return `ffmpeg-${suffix}.zip`
  73. } else if (this.symbols) {
  74. return `electron-${suffix}-symbols.zip`
  75. } else if (this.dsym) {
  76. return `electron-${suffix}-dsym.zip`
  77. } else {
  78. return `electron-${suffix}.zip`
  79. }
  80. }
  81. get platform () {
  82. return this.opts.platform || os.platform()
  83. }
  84. get proxy () {
  85. let proxy
  86. if (this.npmrc && this.npmrc.proxy) proxy = this.npmrc.proxy
  87. if (this.npmrc && this.npmrc['https-proxy']) proxy = this.npmrc['https-proxy']
  88. return proxy
  89. }
  90. get quiet () {
  91. return this.opts.quiet || process.stdout.rows < 1
  92. }
  93. get strictSSL () {
  94. let strictSSL = true
  95. if (this.opts.strictSSL === false || this.npmrc['strict-ssl'] === false) {
  96. strictSSL = false
  97. }
  98. return strictSSL
  99. }
  100. get force () {
  101. return this.opts.force || false
  102. }
  103. get symbols () {
  104. return this.opts.symbols || false
  105. }
  106. get dsym () {
  107. return this.opts.dsym || false
  108. }
  109. get chromedriver () {
  110. return this.opts.chromedriver || false
  111. }
  112. get mksnapshot () {
  113. return this.opts.mksnapshot || false
  114. }
  115. get ffmpeg () {
  116. return this.opts.ffmpeg || false
  117. }
  118. get url () {
  119. return `${this.baseUrl}${this.middleUrl}/${this.urlSuffix}`
  120. }
  121. get verifyChecksumNeeded () {
  122. return semver.gte(this.version, '1.3.2')
  123. }
  124. get version () {
  125. return this.opts.version
  126. }
  127. checkForCachedChecksum (cb) {
  128. pathExists(this.cachedChecksum)
  129. .then(exists => {
  130. if (exists && !this.force) {
  131. this.verifyChecksum(cb)
  132. } else {
  133. this.downloadChecksum(cb)
  134. }
  135. })
  136. }
  137. checkForCachedZip (cb) {
  138. pathExists(this.cachedZip).then(exists => {
  139. if (exists && !this.force) {
  140. debug('zip exists', this.cachedZip)
  141. this.checkIfZipNeedsVerifying(cb)
  142. } else {
  143. this.ensureCacheDir(cb)
  144. }
  145. })
  146. }
  147. checkIfZipNeedsVerifying (cb) {
  148. if (this.verifyChecksumNeeded) {
  149. debug('Verifying zip with checksum')
  150. return this.checkForCachedChecksum(cb)
  151. }
  152. return cb(null, this.cachedZip)
  153. }
  154. createCacheDir (cb) {
  155. fs.mkdirs(this.cache, (err) => {
  156. if (err) {
  157. if (err.code !== 'EACCES') return cb(err)
  158. // try local folder if homedir is off limits (e.g. some linuxes return '/' as homedir)
  159. let localCache = path.resolve('./.electron')
  160. return fs.mkdirs(localCache, function (err) {
  161. if (err) return cb(err)
  162. cb(null, localCache)
  163. })
  164. }
  165. cb(null, this.cache)
  166. })
  167. }
  168. downloadChecksum (cb) {
  169. this.downloadFile(this.checksumUrl, this.cachedChecksum, cb, this.verifyChecksum.bind(this))
  170. }
  171. downloadFile (url, cacheFilename, cb, onSuccess) {
  172. const tempFileName = `tmp-${process.pid}-${(ElectronDownloader.tmpFileCounter++).toString(16)}-${path.basename(cacheFilename)}`
  173. debug('downloading', url, 'to', this.cache)
  174. let nuggetOpts = {
  175. target: tempFileName,
  176. dir: this.cache,
  177. resume: true,
  178. quiet: this.quiet,
  179. strictSSL: this.strictSSL,
  180. proxy: this.proxy
  181. }
  182. nugget(url, nuggetOpts, (errors) => {
  183. if (errors) {
  184. // nugget returns an array of errors but we only need 1st because we only have 1 url
  185. return this.handleDownloadError(cb, errors[0])
  186. }
  187. this.moveFileToCache(tempFileName, cacheFilename, cb, onSuccess)
  188. })
  189. }
  190. downloadIfNotCached (cb) {
  191. if (!this.version) return cb(new Error('must specify version'))
  192. debug('info', {cache: this.cache, filename: this.filename, url: this.url})
  193. this.checkForCachedZip(cb)
  194. }
  195. downloadZip (cb) {
  196. this.downloadFile(this.url, this.cachedZip, cb, this.checkIfZipNeedsVerifying.bind(this))
  197. }
  198. ensureCacheDir (cb) {
  199. debug('creating cache dir')
  200. this.createCacheDir((err, actualCache) => {
  201. if (err) return cb(err)
  202. this.opts.cache = actualCache // in case cache dir changed
  203. this.downloadZip(cb)
  204. })
  205. }
  206. handleDownloadError (cb, error) {
  207. if (error.message.indexOf('404') === -1) return cb(error)
  208. if (this.symbols) {
  209. error.message = `Failed to find Electron symbols v${this.version} for ${this.platform}-${this.arch} at ${this.url}`
  210. } else {
  211. error.message = `Failed to find Electron v${this.version} for ${this.platform}-${this.arch} at ${this.url}`
  212. }
  213. return cb(error)
  214. }
  215. moveFileToCache (filename, target, cb, onSuccess) {
  216. const cache = this.cache
  217. debug('moving', filename, 'from', cache, 'to', target)
  218. fs.rename(path.join(cache, filename), target, (err) => {
  219. if (err) {
  220. fs.unlink(cache, cleanupError => {
  221. try {
  222. if (cleanupError) {
  223. console.error(`Error deleting cache file: ${cleanupError.message}`)
  224. }
  225. } finally {
  226. cb(err)
  227. }
  228. })
  229. } else {
  230. onSuccess(cb)
  231. }
  232. })
  233. }
  234. verifyChecksum (cb) {
  235. let options = {}
  236. if (semver.lt(this.version, '1.3.5')) {
  237. options.defaultTextEncoding = 'binary'
  238. }
  239. let checker = new sumchecker.ChecksumValidator('sha256', this.cachedChecksum, options)
  240. checker.validate(this.cache, this.filename).then(() => {
  241. cb(null, this.cachedZip)
  242. }, (err) => {
  243. fs.unlink(this.cachedZip, (fsErr) => {
  244. if (fsErr) return cb(fsErr)
  245. cb(err)
  246. })
  247. })
  248. }
  249. }
  250. ElectronDownloader.tmpFileCounter = 0
  251. module.exports = function download (opts, cb) {
  252. let downloader = new ElectronDownloader(opts)
  253. downloader.downloadIfNotCached(cb)
  254. }