Skip to content

Commit

Permalink
feat(tarball): support file: opts.resolved shortcut
Browse files Browse the repository at this point in the history
This commit also refactors all the code related to extract.js
and tarball.js to a common area so they work the same for real.
  • Loading branch information
zkat committed Feb 17, 2018
1 parent 354943f commit a6cf279
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 255 deletions.
154 changes: 39 additions & 115 deletions extract.js
Expand Up @@ -2,15 +2,14 @@

const BB = require('bluebird')

const cacache = require('cacache')
const extractStream = require('./lib/extract-stream')
const extractStream = require('./lib/extract-stream.js')
const fs = require('fs')
const mkdirp = BB.promisify(require('mkdirp'))
const npa = require('npm-package-arg')
const optCheck = require('./lib/util/opt-check')
const optCheck = require('./lib/util/opt-check.js')
const path = require('path')
const retry = require('promise-retry')
const rimraf = BB.promisify(require('rimraf'))
const withTarballStream = require('./lib/with-tarball-stream.js')

const truncateAsync = BB.promisify(fs.truncate)
const readFileAsync = BB.promisify(fs.readFile)
Expand All @@ -21,126 +20,51 @@ function extract (spec, dest, opts) {
opts = optCheck(opts)
spec = npa(spec, opts.where)
const startTime = Date.now()
if (opts.integrity && opts.cache && !opts.preferOnline) {
opts.log.silly('pacote', `trying ${spec} by hash: ${opts.integrity}`)
return extractByDigest(
startTime, spec, dest, opts
).catch(err => {
if (err.code === 'ENOENT') {
opts.log.silly('pacote', `data for ${opts.integrity} not present. Using manifest.`)
return extractByManifest(startTime, spec, dest, opts)
}

if (err.code === 'EINTEGRITY' || err.code === 'Z_DATA_ERROR') {
opts.log.warn('pacote', `cached data for ${spec} (${opts.integrity}) seems to be corrupted. Refreshing cache.`)
}
return cleanUpCached(
dest, opts.cache, opts.integrity, opts
).then(() => {
return extractByManifest(startTime, spec, dest, opts)
})
})
} else {
opts.log.silly('pacote', 'no tarball hash provided for', spec.name, '- extracting by manifest')
return BB.resolve(retry((tryAgain, attemptNum) => {
return extractByManifest(
startTime, spec, dest, opts
).catch(err => {
// Retry once if we have a cache, to clear up any weird conditions.
// Don't retry network errors, though -- make-fetch-happen has already
// taken care of making sure we're all set on that front.
if (opts.cache && !err.code.match(/^E\d{3}$/)) {
if (err.code === 'EINTEGRITY' || err.code === 'Z_DATA_ERROR') {
opts.log.warn('pacote', `tarball data for ${spec} (${opts.integrity}) seems to be corrupted. Trying one more time.`)
}
return cleanUpCached(
dest, opts.cache, err.sri, opts
).then(() => tryAgain(err))
} else {
throw err
}
})
}, {retries: 1}))
}
}

function extractByDigest (start, spec, dest, opts) {
return mkdirp(dest).then(() => {
const xtractor = extractStream(spec, dest, opts)
const cached = cacache.get.stream.byDigest(opts.cache, opts.integrity, opts)
cached.pipe(xtractor)
return new BB((resolve, reject) => {
cached.on('error', reject)
xtractor.on('error', reject)
xtractor.on('close', resolve)
})
}).then(() => {
opts.log.silly('pacote', `${spec} extracted to ${dest} by content address ${Date.now() - start}ms`)
}).catch(err => {
if (err.code === 'EINTEGRITY') {
err.message = `Verification failed while extracting ${spec}:\n${err.message}`
}
throw err
return withTarballStream(spec, opts, stream => {
return tryExtract(spec, stream, dest, opts)
})
}

let fetch
function extractByManifest (start, spec, dest, opts) {
let integrity = opts.integrity
let resolved = opts.resolved
return mkdirp(dest).then(() => {
const xtractor = extractStream(spec, dest, opts)
if (!fetch) {
fetch = require('./lib/fetch')
}
const tardata = fetch.tarball(spec, opts)
if (!resolved) {
tardata.on('manifest', m => {
resolved = m._resolved
})
tardata.on('integrity', i => {
integrity = i
})
}
tardata.pipe(xtractor)
return new BB((resolve, reject) => {
tardata.on('error', reject)
xtractor.on('error', reject)
xtractor.on('close', resolve)
})
}).then(() => {
.then(() => {
if (!opts.resolved) {
const pjson = path.join(dest, 'package.json')
return readFileAsync(pjson, 'utf8')
.then(str => {
return truncateAsync(pjson)
.then(() => {
return appendFileAsync(pjson, str.replace(
/}\s*$/,
`\n,"_resolved": ${
JSON.stringify(resolved || '')
}\n,"_integrity": ${
JSON.stringify(integrity || '')
}\n,"_from": ${
JSON.stringify(spec.toString())
}\n}`
))
})
})
.then(str => truncateAsync(pjson)
.then(() => appendFileAsync(pjson, str.replace(
/}\s*$/,
`\n,"_resolved": ${
JSON.stringify(opts.resolved || '')
}\n,"_integrity": ${
JSON.stringify(opts.integrity || '')
}\n,"_from": ${
JSON.stringify(spec.toString())
}\n}`
))))
}
}).then(() => {
opts.log.silly('pacote', `${spec} extracted in ${Date.now() - start}ms`)
}).catch(err => {
})
.then(() => opts.log.silly(
'extract',
`${spec} extracted to ${dest} (${Date.now() - startTime}ms)`
))
}

function tryExtract (spec, tarStream, dest, opts) {
return new BB((resolve, reject) => {
tarStream.on('error', reject)
setImmediate(resolve)
})
.then(() => rimraf(dest))
.then(() => mkdirp(dest))
.then(() => new BB((resolve, reject) => {
const xtractor = extractStream(spec, dest, opts)
tarStream.on('error', reject)
xtractor.on('error', reject)
xtractor.on('close', resolve)
tarStream.pipe(xtractor)
}))
.catch(err => {
if (err.code === 'EINTEGRITY') {
err.message = `Verification failed while extracting ${spec}:\n${err.message}`
}
throw err
})
}

function cleanUpCached (dest, cachePath, integrity, opts) {
return BB.join(
rimraf(dest),
cacache.rm.content(cachePath, integrity, opts)
)
}
131 changes: 131 additions & 0 deletions lib/with-tarball-stream.js
@@ -0,0 +1,131 @@
'use strict'

const BB = require('bluebird')

const cacache = require('cacache')
const fetch = require('./fetch.js')
const fs = require('fs')
const npa = require('npm-package-arg')
const optCheck = require('./util/opt-check.js')
const path = require('path')
const ssri = require('ssri')
const retry = require('promise-retry')

const statAsync = BB.promisify(fs.stat)

const RETRIABLE_ERRORS = new Set(['ENOENT', 'EINTEGRITY', 'Z_DATA_ERROR'])

module.exports = withTarballStream
function withTarballStream (spec, opts, streamHandler) {
opts = optCheck(opts)
spec = npa(spec, opts.where)

// First, we check for a file: resolved shortcut
const tryFile = (
!opts.preferOnline &&
opts.integrity &&
opts.resolved &&
opts.resolved.startsWith('file:')
)
? BB.try(() => {
// NOTE - this is a special shortcut! Packages installed as files do not
// have a `resolved` field -- this specific case only occurs when you have,
// say, a git dependency or a registry dependency that you've packaged into
// a local file, and put that file: spec in the `resolved` field.
opts.log.silly('pacote', `trying ${spec} by local file: ${opts.resolved}`)
const file = path.resolve(opts.where || '.', opts.resolved.substr(5))
return statAsync(file)
.then(() => {
const verifier = ssri.integrityStream({integrity: opts.integrity})
const stream = fs.createReadStream(file)
.on('error', err => verifier.emit('error', err))
.pipe(verifier)
return streamHandler(stream)
})
})
: BB.reject(Object.assign(new Error('no file!'), {code: 'ENOENT'}))

const tryDigest = tryFile
.catch(err => {
if (
opts.preferOnline ||
!opts.cache ||
!opts.integrity ||
!RETRIABLE_ERRORS.has(err.code)
) {
throw err
} else {
opts.log.silly('tarball', `trying ${spec} by hash: ${opts.integrity}`)
const stream = cacache.get.stream.byDigest(
opts.cache, opts.integrity, opts
)
stream.once('error', err => stream.on('newListener', (ev, l) => {
if (ev === 'error') { l(err) }
}))
return streamHandler(stream)
.catch(err => {
if (err.code === 'EINTEGRITY' || err.code === 'Z_DATA_ERROR') {
opts.log.warn('tarball', `cached data for ${spec} (${opts.integrity}) seems to be corrupted. Refreshing cache.`)
return cleanUpCached(opts.cache, opts.integrity, opts)
.then(() => { throw err })
} else {
throw err
}
})
}
})

const trySpec = tryDigest
.catch(err => {
if (!RETRIABLE_ERRORS.has(err.code)) {
// If it's not one of our retriable errors, bail out and give up.
throw err
} else {
opts.log.silly(
'tarball',
`no local data for ${spec}. Extracting by manifest.`
)
return BB.resolve(retry((tryAgain, attemptNum) => {
const tardata = fetch.tarball(spec, opts)
if (!opts.resolved) {
tardata.on('manifest', m => {
opts.resolved = m._resolved
})
tardata.on('integrity', i => {
opts.integrity = i
})
}
tardata.once('error', err => tardata.on('newListener', (ev, l) => {
if (ev === 'error') { l(err) }
}))
return streamHandler(tardata)
.catch(err => {
// Retry once if we have a cache, to clear up any weird conditions.
// Don't retry network errors, though -- make-fetch-happen has already
// taken care of making sure we're all set on that front.
if (opts.cache && !err.code.match(/^E\d{3}$/)) {
if (err.code === 'EINTEGRITY' || err.code === 'Z_DATA_ERROR') {
opts.log.warn('tarball', `tarball data for ${spec} (${opts.integrity}) seems to be corrupted. Trying one more time.`)
}
return cleanUpCached(opts.cache, err.sri, opts)
.then(() => tryAgain(err))
} else {
throw err
}
})
}, {retries: 1}))
}
})

return trySpec
.catch(err => {
if (err.code === 'EINTEGRITY') {
err.message = `Verification failed while extracting ${spec}:\n${err.message}`
}
throw err
})
}

function cleanUpCached (cachePath, integrity, opts) {
return cacache.rm.content(cachePath, integrity, opts)
}

0 comments on commit a6cf279

Please sign in to comment.