1 // only remove the thing if it's a symlink into a specific folder.
2 // This is a very common use-case of npm's, but not so common elsewhere.
4 module.exports = gentlyRm
6 var npm = require('../npm.js')
7 var log = require('npmlog')
8 var resolve = require('path').resolve
9 var dirname = require('path').dirname
10 var lstat = require('graceful-fs').lstat
11 var readlink = require('graceful-fs').readlink
12 var isInside = require('path-is-inside')
13 var vacuum = require('fs-vacuum')
14 var some = require('async-some')
15 var asyncMap = require('slide').asyncMap
16 var normalize = require('path').normalize
18 function gentlyRm (target, gently, base, cb) {
32 'is being', gently ? 'gently removed' : 'purged',
33 base ? 'from base ' + base : ''
36 // never rm the root, prefix, or bin dirs
38 // globals included because of `npm link` -- as far as the package requesting
39 // the link is concerned, the linked package is always installed globally
50 var resolved = normalize(resolve(npm.prefix, target))
51 if (prefixes.indexOf(resolved) !== -1) {
52 log.verbose('gentlyRm', resolved, "is part of npm and can't be removed")
53 return cb(new Error('May not delete: ' + resolved))
56 var options = { log: log.silly.bind(log, 'vacuum-fs') }
57 if (npm.config.get('force') || !gently) options.purge = true
58 if (base) options.base = normalize(resolve(npm.prefix, base))
61 log.verbose('gentlyRm', "don't care about contents; nuking", resolved)
62 return vacuum(resolved, options, cb)
65 var parent = options.base = normalize(base ? resolve(npm.prefix, base) : npm.prefix)
67 // is the parent directory managed by npm?
68 log.silly('gentlyRm', 'verifying', parent, 'is an npm working directory')
69 some(prefixes, isManaged(parent), function (er, matched) {
73 log.error('gentlyRm', 'containing path', parent, "isn't under npm's control")
74 return clobberFail(resolved, parent, cb)
76 log.silly('gentlyRm', 'containing path', parent, "is under npm's control, in", matched)
78 // is the target directly contained within the (now known to be
80 if (isInside(resolved, parent)) {
81 log.silly('gentlyRm', 'deletion target', resolved, 'is under', parent)
82 log.verbose('gentlyRm', 'vacuuming from', resolved, 'up to', parent)
83 return vacuum(resolved, options, cb)
85 log.silly('gentlyRm', resolved, 'is not under', parent)
87 // the target isn't directly within the parent, but is it itself managed?
88 log.silly('gentlyRm', 'verifying', resolved, 'is an npm working directory')
89 some(prefixes, isManaged(resolved), function (er, matched) {
93 log.silly('gentlyRm', resolved, "is under npm's control, in", matched)
94 options.base = matched
95 log.verbose('gentlyRm', 'removing', resolved, 'with base', options.base)
96 return vacuum(resolved, options, cb)
98 log.verbose('gentlyRm', resolved, "is not under npm's control")
100 // the target isn't managed directly, but maybe it's a link...
101 log.silly('gentlyRm', 'checking to see if', resolved, 'is a link')
102 lstat(resolved, function (er, stat) {
104 // race conditions are common when unbuilding
105 if (er.code === 'ENOENT') return cb(null)
109 if (!stat.isSymbolicLink()) {
110 log.error('gentlyRm', resolved, 'is outside', parent, 'and not a link')
111 return clobberFail(resolved, parent, cb)
114 // ...and maybe the link source, when read...
115 log.silly('gentlyRm', resolved, 'is a link')
116 readlink(resolved, function (er, link) {
118 // race conditions are common when unbuilding
119 if (er.code === 'ENOENT') return cb(null)
123 // ...is inside the managed parent
124 var source = resolve(dirname(resolved), link)
125 if (isInside(source, parent)) {
126 log.silly('gentlyRm', source, 'symlink target', resolved, 'is inside', parent)
127 log.verbose('gentlyRm', 'vacuuming', resolved)
128 return vacuum(resolved, options, cb)
131 log.error('gentlyRm', source, 'symlink target', resolved, 'is not controlled by npm', parent)
132 return clobberFail(target, parent, cb)
139 var resolvedPaths = {}
140 function isManaged (target) {
141 return function predicate (path, cb) {
143 log.verbose('isManaged', 'no path passed for target', target)
144 return cb(null, false)
147 asyncMap([path, target], resolveSymlink, function (er, results) {
149 if (er.code === 'ENOENT') return cb(null, false)
154 var path = results[0]
155 var target = results[1]
156 var inside = isInside(target, path)
157 if (!inside) log.silly('isManaged', target, 'is not inside', path)
159 return cb(null, inside && path)
163 function resolveSymlink (toResolve, cb) {
164 var resolved = resolve(npm.prefix, toResolve)
166 // if the path has already been memoized, return immediately
167 var cached = resolvedPaths[resolved]
168 if (cached) return cb(null, cached)
170 // otherwise, check the path
171 lstat(resolved, function (er, stat) {
172 if (er) return cb(er)
174 // if it's not a link, cache & return the path itself
175 if (!stat.isSymbolicLink()) {
176 resolvedPaths[resolved] = resolved
177 return cb(null, resolved)
180 // otherwise, cache & return the link's source
181 readlink(resolved, function (er, source) {
182 if (er) return cb(er)
184 resolved = resolve(resolved, source)
185 resolvedPaths[resolved] = resolved
192 function clobberFail (target, root, cb) {
193 var er = new Error('Refusing to delete: ' + target + ' not in ' + root)