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