Skip to content
This repository has been archived by the owner on Aug 11, 2022. It is now read-only.

Commit

Permalink
gently-rm: Refactor to be easier to understand, unit-testable
Browse files Browse the repository at this point in the history
PR-URL: #10270
Fixes: #9980
Credit: @iarna
  • Loading branch information
iarna committed Nov 12, 2015
1 parent 11a4e1a commit 5653366
Showing 1 changed file with 175 additions and 141 deletions.
316 changes: 175 additions & 141 deletions lib/utils/gently-rm.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
// only remove the thing if it's a symlink into a specific folder.
// This is a very common use-case of npm's, but not so common elsewhere.
// only remove the thing if it's a symlink into a specific folder. This is
// a very common use-case of npm's, but not so common elsewhere.

module.exports = gentlyRm
exports = module.exports = gentlyRm

var npm = require('../npm.js')
var log = require('npmlog')
var resolve = require('path').resolve
var dirname = require('path').dirname
var normalize = require('path').normalize
var validate = require('aproba')
var log = require('npmlog')
var lstat = require('graceful-fs').lstat
var readlink = require('graceful-fs').readlink
var isInside = require('path-is-inside')
var vacuum = require('fs-vacuum')
var some = require('async-some')
var chain = require('slide').chain
var asyncMap = require('slide').asyncMap
var normalize = require('path').normalize
var readCmdShim = require('read-cmd-shim')
var iferr = require('iferr')
var npm = require('../npm.js')

function gentlyRm (target, gently, base, cb) {
if (!cb) {
Expand All @@ -37,8 +38,9 @@ function gentlyRm (target, gently, base, cb) {

// never rm the root, prefix, or bin dirs
//
// globals included because of `npm link` -- as far as the package requesting
// the link is concerned, the linked package is always installed globally
// globals included because of `npm link` -- as far as the package
// requesting the link is concerned, the linked package is always
// installed globally
var prefixes = [
npm.prefix,
npm.globalPrefix,
Expand All @@ -49,153 +51,192 @@ function gentlyRm (target, gently, base, cb) {
npm.globalBin
]

var resolved = normalize(resolve(npm.prefix, target))
if (prefixes.indexOf(resolved) !== -1) {
log.verbose('gentlyRm', resolved, "is part of npm and can't be removed")
return cb(new Error('May not delete: ' + resolved))
var targetPath = normalize(resolve(npm.prefix, target))
if (prefixes.indexOf(targetPath) !== -1) {
log.verbose('gentlyRm', targetPath, "is part of npm and can't be removed")
return cb(new Error('May not delete: ' + targetPath))
}
var options = { log: log.silly.bind(log, 'vacuum-fs') }
if (npm.config.get('force') || !gently) options.purge = true
if (base) options.base = normalize(resolve(npm.prefix, base))

follow(resolved, function (realpath) {
var options = { log: log.silly.bind(log, 'vacuum-fs') }
if (npm.config.get('force') || !gently) options.purge = true
if (base) options.base = normalize(resolve(npm.prefix, base))
if (!gently) {
log.verbose('gentlyRm', "don't care about contents; nuking", targetPath)
return vacuum(targetPath, options, cb)
}

if (!gently) {
log.verbose('gentlyRm', "don't care about contents; nuking", resolved)
return vacuum(resolved, options, cb)
var parent = options.base = options.base || normalize(npm.prefix)

// Do all the async work we'll need to do in order to tell if this is a
// safe operation
chain([
[isEverInside, parent, prefixes],
[readLinkOrShim, targetPath],
[isEverInside, targetPath, prefixes],
[isEverInside, targetPath, [parent]]
], function (er, results) {
if (er) {
if (er.code === 'ENOENT') return cb()
return cb(er)
}

var parent = options.base = normalize(base ? resolve(npm.prefix, base) : npm.prefix)

// is the parent directory managed by npm?
log.silly('gentlyRm', 'verifying', parent, 'is an npm working directory')
some(prefixes, isManaged(parent), function (er, matched) {
if (er) return cb(er)

if (!matched) {
log.error('gentlyRm', 'containing path', parent, "isn't under npm's control")
return clobberFail(resolved, parent, cb)
}
log.silly('gentlyRm', 'containing path', parent, "is under npm's control, in", matched)

// is the target directly contained within the (now known to be
// managed) parent?
if (isInside(resolved, parent)) {
log.silly('gentlyRm', 'deletion target', resolved, 'is under', parent)
log.verbose('gentlyRm', 'vacuuming from', resolved, 'up to', parent)
options.base = parent
return vacuum(resolved, options, cb)
}
log.silly('gentlyRm', realpath, 'is not under', parent)

// the target isn't directly within the parent, but is it itself managed?
log.silly('gentlyRm', 'verifying', realpath, 'is an npm working directory')
some(prefixes, isManaged(realpath), function (er, matched) {
if (er) return cb(er)

if (matched) {
log.silly('gentlyRm', resolved, "is under npm's control, in", matched)
if (isInside(realpath, parent)) {
log.silly('gentlyRm', realpath, 'is controlled by', parent)
options.base = matched
log.verbose('gentlyRm', 'removing', resolved, 'with base', options.base)
return vacuum(resolved, options, cb)
} else if (resolved !== realpath) {
log.warn('gentlyRm', 'not removing', resolved, "as it wasn't installed by", parent)
return cb()
}
}
log.verbose('gentlyRm', resolved, "is not under npm's control")

// the target isn't managed directly, but maybe it's a link...
log.silly('gentlyRm', 'checking to see if', resolved, 'is a link')
readLinkOrShim(resolved, function (er, link) {
if (er) {
// race conditions are common when unbuilding
if (er.code === 'ENOENT') return cb(null)
return cb(er)
}

if (!link) {
log.error('gentlyRm', resolved, 'is outside', parent, 'and not a link')
return clobberFail(resolved, parent, cb)
}

// ...and maybe the link source, when read...
log.silly('gentlyRm', resolved, 'is a link')
// ...is inside the managed parent
var source = resolve(dirname(resolved), link)
if (isInside(source, parent)) {
log.silly('gentlyRm', source, 'symlink target', resolved, 'is inside', parent)
log.verbose('gentlyRm', 'vacuuming', resolved)
return vacuum(resolved, options, cb)
}

log.error('gentlyRm', source, 'symlink target', resolved, 'is not controlled by npm', parent)
return clobberFail(target, parent, cb)
})
})
})
})
}

var resolvedPaths = {}
function isManaged (target) {
return function predicate (path, cb) {
if (!path) {
log.verbose('isManaged', 'no path passed for target', target)
return cb(null, false)
var parentInfo = {
path: parent,
managed: results[0]
}
var targetInfo = {
path: targetPath,
symlink: results[1],
managed: results[2],
inParent: results[3]
}

asyncMap([path, target], resolveSymlink, function (er, results) {
if (er) {
if (er.code === 'ENOENT') return cb(null, false)
isSafeToRm(parentInfo, targetInfo, iferr(cb, thenRemove))

return cb(er)
}
function thenRemove (toRemove, removeBase) {
if (!toRemove) return cb()
if (removeBase) options.base = removeBase
log.verbose('gentlyRm', options.purge ? 'Purging' : 'Vacuuming',
toRemove, 'up to', options.base)
return vacuum(toRemove, options, cb)
}
})
}

var path = results[0]
var target = results[1]
var inside = isInside(target, path)
if (!inside) log.silly('isManaged', target, 'is not inside', path)
exports._isSafeToRm = isSafeToRm
function isSafeToRm (parent, target, cb) {
log.silly('gentlyRm', 'parent.path =', parent.path)
log.silly('gentlyRm', 'parent.managed =',
parent.managed && parent.managed[0] + ' is in ' + parent.managed[1])
log.silly('gentlyRm', 'target.path = ', target.path)
log.silly('gentlyRm', 'target.symlink =', target.symlink)
log.silly('gentlyRm', 'target.managed =',
target.managed && target.managed[0] + ' is in ' + target.managed[1])
log.silly('gentlyRm', 'target.inParent = ', target.inParent)

// The parent directory or something it symlinks to must eventually be in
// a folder that npm maintains.
if (!parent.managed) {
log.verbose('gentlyRm', parent.path,
'is not contained in any diretory npm is known to control or ' +
'any place they link to')
return cb(clobberFail(target.path, 'containing path ' + parent.path +
" isn't under npm's control"))
}

return cb(null, inside && path)
})
// The target or something it symlinks to must eventually be in the parent
// or something the parent symlinks to
if (target.inParent) {
var actualTarget = target.inParent[0]
var targetsParent = target.inParent[1]
// if the target.path was what we found in some version of parent, remove
// using that parent as the base
if (target.path === actualTarget) {
return cb(null, target.path, targetsParent)
} else {
// If something the target.path links to was what was found, just
// remove target.path in the location it was found.
return cb(null, target.path, dirname(target.path))
}
}

function resolveSymlink (toResolve, cb) {
var resolved = resolve(npm.prefix, toResolve)
// If the target is in a managed directory and is in a symlink, but was
// not in our parent that usually means someone else installed a bin file
// with the same name as one of our bin files.
if (target.managed && target.symlink) {
log.warn('gentlyRm', 'not removing', target.path,
"as it wasn't installed by", parent.path)
return cb()
}

// if the path has already been memoized, return immediately
var cached = resolvedPaths[resolved]
if (cached) return cb(null, cached)
if (target.symlink) {
return cb(clobberFail(target.path, target.symlink +
' symlink target is not controlled by npm ' + parent.path))
} else {
return cb(clobberFail(target.path, 'is outside ' + parent.path +
' and not a link'))
}
}

// otherwise, check the path
readLinkOrShim(resolved, function (er, source) {
if (er) return cb(er)
function clobberFail (target, msg) {
validate('SS', arguments)
var er = new Error('Refusing to delete ' + target + ': ' + msg)
er.code = 'EEXIST'
er.path = target
return er
}

// if it's not a link, cache & return the path itself
if (!source) {
resolvedPaths[resolved] = resolved
return cb(null, resolved)
}
exports._isEverInside = isEverInside
// return the first of path, where target (or anything it symlinks to)
// isInside the path (or anything it symlinks to)
function isEverInside (target, paths, cb) {
validate('SAF', arguments)
function skipENOENT (er) {
if (er.code === 'ENOENT') return cb(null, false)
return cb(er)
}
asyncMap(paths, readAllLinks, iferr(skipENOENT, function (resolvedPaths) {
readAllLinks(target, iferr(skipENOENT, function (targets) {
cb(null, areAnyInsideAny(targets, resolvedPaths))
}))
}))
}

// otherwise, cache & return the link's source
resolved = resolve(resolved, source)
resolvedPaths[resolved] = resolved
cb(null, resolved)
exports._areAnyInsideAny = areAnyInsideAny
// Return the first path found that any target is inside
function areAnyInsideAny (targets, paths) {
validate('AA', arguments)
var toCheck = []
paths.forEach(function (path) {
targets.forEach(function (target) {
toCheck.push([target, path])
})
})
for (var ii = 0; ii < toCheck.length; ++ii) {
var target = toCheck[ii][0]
var path = toCheck[ii][1]
var inside = isInside(target, path)
if (!inside) log.silly('isEverInside', target, 'is not inside', path)
if (inside && path) return inside && path && {target: target, path: path}
}
return false
}

function clobberFail (target, root, cb) {
var er = new Error('Refusing to delete: ' + target + ' not in ' + root)
er.code = 'EEXIST'
er.path = target
return cb(er)
exports._readAllLinks = readAllLinks
// resolves chains of symlinks of unlimited depth, returning a list of paths
// it's seen in the process when it hits either a symlink cycle or a
// non-symlink
function readAllLinks (path, cb) {
validate('SF', arguments)
var seen = {}
_readAllLinks(path)

function _readAllLinks (path) {
if (seen[path]) return cb(null, Object.keys(seen))
seen[path] = true
resolveSymlink(path, iferr(cb, _readAllLinks))
}
}

exports._resolveSymlink = resolveSymlink
var resolvedPaths = {}
function resolveSymlink (symlink, cb) {
validate('SF', arguments)
var cached = resolvedPaths[symlink]
if (cached) return cb(null, cached)

readLinkOrShim(symlink, iferr(cb, function (symlinkTarget) {
if (symlinkTarget) {
resolvedPaths[symlink] = resolve(dirname(symlink), symlinkTarget)
} else {
resolvedPaths[symlink] = symlink
}
return cb(null, resolvedPaths[symlink])
}))
}

exports._readLinkOrShim = readLinkOrShim
function readLinkOrShim (path, cb) {
validate('SF', arguments)
lstat(path, iferr(cb, function (stat) {
if (stat.isSymbolicLink()) {
readlink(path, cb)
Expand All @@ -212,10 +253,3 @@ function readLinkOrShim (path, cb) {
}
}))
}

function follow (path, cb) {
readLinkOrShim(path, function (er, source) {
if (!source) return cb(path)
cb(normalize(resolve(dirname(path), source)))
})
}

0 comments on commit 5653366

Please sign in to comment.