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