with-tarball-stream.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. 'use strict'
  2. const BB = require('bluebird')
  3. const cacache = require('cacache')
  4. const fetch = require('./fetch.js')
  5. const fs = require('fs')
  6. const npa = require('npm-package-arg')
  7. const optCheck = require('./util/opt-check.js')
  8. const path = require('path')
  9. const ssri = require('ssri')
  10. const retry = require('promise-retry')
  11. const statAsync = BB.promisify(fs.stat)
  12. const RETRIABLE_ERRORS = new Set(['ENOENT', 'EINTEGRITY', 'Z_DATA_ERROR'])
  13. module.exports = withTarballStream
  14. function withTarballStream (spec, opts, streamHandler) {
  15. opts = optCheck(opts)
  16. spec = npa(spec, opts.where)
  17. // First, we check for a file: resolved shortcut
  18. const tryFile = (
  19. !opts.preferOnline &&
  20. opts.integrity &&
  21. opts.resolved &&
  22. opts.resolved.startsWith('file:')
  23. )
  24. ? BB.try(() => {
  25. // NOTE - this is a special shortcut! Packages installed as files do not
  26. // have a `resolved` field -- this specific case only occurs when you have,
  27. // say, a git dependency or a registry dependency that you've packaged into
  28. // a local file, and put that file: spec in the `resolved` field.
  29. opts.log.silly('pacote', `trying ${spec} by local file: ${opts.resolved}`)
  30. const file = path.resolve(opts.where || '.', opts.resolved.substr(5))
  31. return statAsync(file)
  32. .then(() => {
  33. const verifier = ssri.integrityStream({ integrity: opts.integrity })
  34. const stream = fs.createReadStream(file)
  35. .on('error', err => verifier.emit('error', err))
  36. .pipe(verifier)
  37. return streamHandler(stream)
  38. })
  39. .catch(err => {
  40. if (err.code === 'EINTEGRITY') {
  41. opts.log.warn('pacote', `EINTEGRITY while extracting ${spec} from ${file}.You will have to recreate the file.`)
  42. opts.log.verbose('pacote', `EINTEGRITY for ${spec}: ${err.message}`)
  43. }
  44. throw err
  45. })
  46. })
  47. : BB.reject(Object.assign(new Error('no file!'), { code: 'ENOENT' }))
  48. const tryDigest = tryFile
  49. .catch(err => {
  50. if (
  51. opts.preferOnline ||
  52. !opts.cache ||
  53. !opts.integrity ||
  54. !RETRIABLE_ERRORS.has(err.code)
  55. ) {
  56. throw err
  57. } else {
  58. opts.log.silly('tarball', `trying ${spec} by hash: ${opts.integrity}`)
  59. const stream = cacache.get.stream.byDigest(
  60. opts.cache, opts.integrity, opts
  61. )
  62. stream.once('error', err => stream.on('newListener', (ev, l) => {
  63. if (ev === 'error') { l(err) }
  64. }))
  65. return streamHandler(stream)
  66. .catch(err => {
  67. if (err.code === 'EINTEGRITY' || err.code === 'Z_DATA_ERROR') {
  68. opts.log.warn('tarball', `cached data for ${spec} (${opts.integrity}) seems to be corrupted. Refreshing cache.`)
  69. return cleanUpCached(opts.cache, opts.integrity, opts)
  70. .then(() => { throw err })
  71. } else {
  72. throw err
  73. }
  74. })
  75. }
  76. })
  77. const trySpec = tryDigest
  78. .catch(err => {
  79. if (!RETRIABLE_ERRORS.has(err.code)) {
  80. // If it's not one of our retriable errors, bail out and give up.
  81. throw err
  82. } else {
  83. opts.log.silly(
  84. 'tarball',
  85. `no local data for ${spec}. Extracting by manifest.`
  86. )
  87. return BB.resolve(retry((tryAgain, attemptNum) => {
  88. const tardata = fetch.tarball(spec, opts)
  89. if (!opts.resolved) {
  90. tardata.on('manifest', m => {
  91. opts = opts.concat({ resolved: m._resolved })
  92. })
  93. tardata.on('integrity', i => {
  94. opts = opts.concat({ integrity: i })
  95. })
  96. }
  97. return BB.try(() => streamHandler(tardata))
  98. .catch(err => {
  99. // Retry once if we have a cache, to clear up any weird conditions.
  100. // Don't retry network errors, though -- make-fetch-happen has already
  101. // taken care of making sure we're all set on that front.
  102. if (opts.cache && err.code && !String(err.code).match(/^E\d{3}$/)) {
  103. if (err.code === 'EINTEGRITY' || err.code === 'Z_DATA_ERROR') {
  104. opts.log.warn('tarball', `tarball data for ${spec} (${opts.integrity}) seems to be corrupted. Trying one more time.`)
  105. }
  106. return cleanUpCached(opts.cache, err.sri, opts)
  107. .then(() => tryAgain(err))
  108. } else {
  109. throw err
  110. }
  111. })
  112. }, { retries: 1 }))
  113. }
  114. })
  115. return trySpec
  116. .catch(err => {
  117. if (err.code === 'EINTEGRITY') {
  118. err.message = `Verification failed while extracting ${spec}:\n${err.message}`
  119. }
  120. throw err
  121. })
  122. }
  123. function cleanUpCached (cachePath, integrity, opts) {
  124. return cacache.rm.content(cachePath, integrity, opts)
  125. }