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