• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1var assert = require('assert')
2var dirname = require('path').dirname
3var resolve = require('path').resolve
4var isInside = require('path-is-inside')
5
6var rimraf = require('rimraf')
7var lstat = require('graceful-fs').lstat
8var readdir = require('graceful-fs').readdir
9var rmdir = require('graceful-fs').rmdir
10var unlink = require('graceful-fs').unlink
11
12module.exports = vacuum
13
14function vacuum (leaf, options, cb) {
15  assert(typeof leaf === 'string', 'must pass in path to remove')
16  assert(typeof cb === 'function', 'must pass in callback')
17
18  if (!options) options = {}
19  assert(typeof options === 'object', 'options must be an object')
20
21  var log = options.log ? options.log : function () {}
22
23  leaf = leaf && resolve(leaf)
24  var base = options.base && resolve(options.base)
25  if (base && !isInside(leaf, base)) {
26    return cb(new Error(leaf + ' is not a child of ' + base))
27  }
28
29  lstat(leaf, function (error, stat) {
30    if (error) {
31      if (error.code === 'ENOENT') return cb(null)
32
33      log(error.stack)
34      return cb(error)
35    }
36
37    if (!(stat && (stat.isDirectory() || stat.isSymbolicLink() || stat.isFile()))) {
38      log(leaf, 'is not a directory, file, or link')
39      return cb(new Error(leaf + ' is not a directory, file, or link'))
40    }
41
42    if (options.purge) {
43      log('purging', leaf)
44      rimraf(leaf, function (error) {
45        if (error) return cb(error)
46
47        next(dirname(leaf))
48      })
49    } else if (!stat.isDirectory()) {
50      log('removing', leaf)
51      unlink(leaf, function (error) {
52        if (error) return cb(error)
53
54        next(dirname(leaf))
55      })
56    } else {
57      next(leaf)
58    }
59  })
60
61  function next (branch) {
62    branch = branch && resolve(branch)
63    // either we've reached the base or we've reached the root
64    if ((base && branch === base) || branch === dirname(branch)) {
65      log('finished vacuuming up to', branch)
66      return cb(null)
67    }
68
69    readdir(branch, function (error, files) {
70      if (error) {
71        if (error.code === 'ENOENT') return cb(null)
72
73        log('unable to check directory', branch, 'due to', error.message)
74        return cb(error)
75      }
76
77      if (files.length > 0) {
78        log('quitting because other entries in', branch)
79        return cb(null)
80      }
81
82      if (branch === process.env.HOME) {
83        log('quitting because cannot remove home directory', branch)
84        return cb(null)
85      }
86
87      log('removing', branch)
88      lstat(branch, function (error, stat) {
89        if (error) {
90          if (error.code === 'ENOENT') return cb(null)
91
92          log('unable to lstat', branch, 'due to', error.message)
93          return cb(error)
94        }
95
96        var remove = stat.isDirectory() ? rmdir : unlink
97        remove(branch, function (error) {
98          if (error) {
99            if (error.code === 'ENOENT') {
100              log('quitting because lost the race to remove', branch)
101              return cb(null)
102            }
103            if (error.code === 'ENOTEMPTY' || error.code === 'EEXIST') {
104              log('quitting because new (racy) entries in', branch)
105              return cb(null)
106            }
107
108            log('unable to remove', branch, 'due to', error.message)
109            return cb(error)
110          }
111
112          next(dirname(branch))
113        })
114      })
115    })
116  }
117}
118