• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const path = require('path')
4const validate = require('aproba')
5const fs = require('graceful-fs')
6const isInside = require('path-is-inside')
7const vacuum = require('fs-vacuum')
8const chain = require('slide').chain
9const asyncMap = require('slide').asyncMap
10const readCmdShim = require('read-cmd-shim')
11const iferr = require('iferr')
12
13exports = module.exports = rm
14
15function rm (target, opts, cb) {
16  var targetPath = path.normalize(path.resolve(opts.prefix, target))
17  if (opts.prefixes.indexOf(targetPath) !== -1) {
18    return cb(new Error('May not delete: ' + targetPath))
19  }
20  var options = {}
21  if (opts.force) { options.purge = true }
22  if (opts.base) options.base = path.normalize(path.resolve(opts.prefix, opts.base))
23
24  if (!opts.gently) {
25    options.purge = true
26    return vacuum(targetPath, options, cb)
27  }
28
29  var parent = options.base = options.base || path.normalize(opts.prefix)
30
31  // Do all the async work we'll need to do in order to tell if this is a
32  // safe operation
33  chain([
34    [isEverInside, parent, opts.prefixes, opts.log],
35    [readLinkOrShim, targetPath],
36    [isEverInside, targetPath, opts.prefixes, opts.log],
37    [isEverInside, targetPath, [parent], opts.log]
38  ], function (er, results) {
39    if (er) {
40      if (er.code === 'ENOENT') return cb()
41      return cb(er)
42    }
43    var parentInfo = {
44      path: parent,
45      managed: results[0]
46    }
47    var targetInfo = {
48      path: targetPath,
49      symlink: results[1],
50      managed: results[2],
51      inParent: results[3]
52    }
53
54    isSafeToRm(parentInfo, targetInfo, opts.name, opts.log, iferr(cb, thenRemove))
55
56    function thenRemove (toRemove, removeBase) {
57      if (!toRemove) return cb()
58      if (removeBase) options.base = removeBase
59      return vacuum(toRemove, options, cb)
60    }
61  })
62}
63
64exports._isSafeToRm = isSafeToRm
65function isSafeToRm (parent, target, pkgName, log, cb) {
66  log.silly('gentlyRm', 'parent.path =', parent.path)
67  log.silly('gentlyRm', 'parent.managed =',
68    parent.managed && parent.managed.target + ' is in ' + parent.managed.path)
69  log.silly('gentlyRm', 'target.path = ', target.path)
70  log.silly('gentlyRm', 'target.symlink =', target.symlink)
71  log.silly('gentlyRm', 'target.managed =',
72    target.managed && target.managed.target + ' is in ' + target.managed.path)
73  log.silly('gentlyRm', 'target.inParent = ', target.inParent)
74
75  // The parent directory or something it symlinks to must eventually be in
76  // a folder that we maintain.
77  if (!parent.managed) {
78    log.info('gentlyRm', parent.path,
79      'is not contained in any directory ' + pkgName + ' is known to control or ' +
80      'any place they link to')
81    return cb(clobberFail(target.path, 'containing path ' + parent.path +
82      " isn't under " + pkgName + "'s control"))
83  }
84
85  // The target or something it symlinks to must eventually be in the parent
86  // or something the parent symlinks to
87  if (target.inParent) {
88    var actualTarget = target.inParent.target
89    var targetsParent = target.inParent.path
90    // if the target.path was what we found in some version of parent, remove
91    // using that parent as the base
92    if (target.path === actualTarget) {
93      return cb(null, target.path, targetsParent)
94    } else {
95      // If something the target.path links to was what was found, just
96      // remove target.path in the location it was found.
97      return cb(null, target.path, path.dirname(target.path))
98    }
99  }
100
101  // If the target is in a managed directory and is in a symlink, but was
102  // not in our parent that usually means someone else installed a bin file
103  // with the same name as one of our bin files.
104  if (target.managed && target.symlink) {
105    log.warn('rm', 'not removing', target.path,
106      "as it wasn't installed by", parent.path)
107    return cb()
108  }
109
110  if (target.symlink) {
111    return cb(clobberFail(target.path, target.symlink +
112      ' symlink target is not controlled by ' + pkgName + ' ' + parent.path))
113  } else {
114    return cb(clobberFail(target.path, 'is outside ' + parent.path +
115      ' and not a link'))
116  }
117}
118
119function clobberFail (target, msg) {
120  validate('SS', arguments)
121  var er = new Error('Refusing to delete ' + target + ': ' + msg)
122  er.code = 'EEXIST'
123  er.path = target
124  return er
125}
126
127function isENOENT (err) {
128  return err && err.code === 'ENOENT'
129}
130
131function notENOENT (err) {
132  return !isENOENT(err)
133}
134
135function skipENOENT (cb) {
136  return function (err, value) {
137    if (isENOENT(err)) {
138      return cb(null, false)
139    } else {
140      return cb(err, value)
141    }
142  }
143}
144
145function errorsToValues (fn) {
146  return function () {
147    var args = Array.prototype.slice.call(arguments)
148    var cb = args.pop()
149    args.push(function (err, value) {
150      if (err) {
151        return cb(null, err)
152      } else {
153        return cb(null, value)
154      }
155    })
156    fn.apply(null, args)
157  }
158}
159
160function isNotError (value) {
161  return !(value instanceof Error)
162}
163
164exports._isEverInside = isEverInside
165// return the first of path, where target (or anything it symlinks to)
166// isInside the path (or anything it symlinks to)
167function isEverInside (target, paths, log, cb) {
168  validate('SAOF', arguments)
169  asyncMap(paths, errorsToValues(readAllLinks), iferr(cb, function (resolvedPaths) {
170    var errorFree = resolvedPaths.filter(isNotError)
171    if (errorFree.length === 0) {
172      var badErrors = resolvedPaths.filter(notENOENT)
173      if (badErrors.length === 0) {
174        return cb(null, false)
175      } else {
176        return cb(badErrors[0])
177      }
178    }
179    readAllLinks(target, iferr(skipENOENT(cb), function (targets) {
180      cb(null, areAnyInsideAny(targets, errorFree, log))
181    }))
182  }))
183}
184
185exports._areAnyInsideAny = areAnyInsideAny
186// Return the first path found that any target is inside
187function areAnyInsideAny (targets, paths, log) {
188  validate('AAO', arguments)
189  var toCheck = []
190  paths.forEach(function (path) {
191    targets.forEach(function (target) {
192      toCheck.push([target, path])
193    })
194  })
195  for (var ii = 0; ii < toCheck.length; ++ii) {
196    var target = toCheck[ii][0]
197    var path = toCheck[ii][1]
198    var inside = isInside(target, path)
199    if (!inside) log.silly('isEverInside', target, 'is not inside', path)
200    if (inside && path) return inside && path && {target: target, path: path}
201  }
202  return false
203}
204
205exports._readAllLinks = readAllLinks
206// resolves chains of symlinks of unlimited depth, returning a list of paths
207// it's seen in the process when it hits either a symlink cycle or a
208// non-symlink
209function readAllLinks (path, cb) {
210  validate('SF', arguments)
211  var seen = {}
212  _readAllLinks(path)
213
214  function _readAllLinks (path) {
215    if (seen[path]) return cb(null, Object.keys(seen))
216    seen[path] = true
217    resolveSymlink(path, iferr(cb, _readAllLinks))
218  }
219}
220
221exports._resolveSymlink = resolveSymlink
222var resolvedPaths = {}
223function resolveSymlink (symlink, cb) {
224  validate('SF', arguments)
225  var cached = resolvedPaths[symlink]
226  if (cached) return cb(null, cached)
227
228  readLinkOrShim(symlink, iferr(cb, function (symlinkTarget) {
229    if (symlinkTarget) {
230      resolvedPaths[symlink] = path.resolve(path.dirname(symlink), symlinkTarget)
231    } else {
232      resolvedPaths[symlink] = symlink
233    }
234    return cb(null, resolvedPaths[symlink])
235  }))
236}
237
238exports._readLinkOrShim = readLinkOrShim
239function readLinkOrShim (path, cb) {
240  validate('SF', arguments)
241  fs.lstat(path, iferr(cb, function (stat) {
242    if (stat.isSymbolicLink()) {
243      fs.readlink(path, cb)
244    } else {
245      readCmdShim(path, function (er, source) {
246        if (!er) return cb(null, source)
247        // lstat wouldn't return an error on these, so we don't either.
248        if (er.code === 'ENOTASHIM' || er.code === 'EISDIR') {
249          return cb(null, null)
250        } else {
251          return cb(er)
252        }
253      })
254    }
255  }))
256}
257