1// Flags: --expose-internals 2'use strict'; 3const common = require('../common'); 4const tmpdir = require('../common/tmpdir'); 5const assert = require('assert'); 6const fs = require('fs'); 7const path = require('path'); 8const { validateRmdirOptions } = require('internal/fs/utils'); 9 10tmpdir.refresh(); 11 12let count = 0; 13const nextDirPath = (name = 'rmdir-recursive') => 14 path.join(tmpdir.path, `${name}-${count++}`); 15 16function makeNonEmptyDirectory(depth, files, folders, dirname, createSymLinks) { 17 fs.mkdirSync(dirname, { recursive: true }); 18 fs.writeFileSync(path.join(dirname, 'text.txt'), 'hello', 'utf8'); 19 20 const options = { flag: 'wx' }; 21 22 for (let f = files; f > 0; f--) { 23 fs.writeFileSync(path.join(dirname, `f-${depth}-${f}`), '', options); 24 } 25 26 if (createSymLinks) { 27 // Valid symlink 28 fs.symlinkSync( 29 `f-${depth}-1`, 30 path.join(dirname, `link-${depth}-good`), 31 'file' 32 ); 33 34 // Invalid symlink 35 fs.symlinkSync( 36 'does-not-exist', 37 path.join(dirname, `link-${depth}-bad`), 38 'file' 39 ); 40 } 41 42 // File with a name that looks like a glob 43 fs.writeFileSync(path.join(dirname, '[a-z0-9].txt'), '', options); 44 45 depth--; 46 if (depth <= 0) { 47 return; 48 } 49 50 for (let f = folders; f > 0; f--) { 51 fs.mkdirSync( 52 path.join(dirname, `folder-${depth}-${f}`), 53 { recursive: true } 54 ); 55 makeNonEmptyDirectory( 56 depth, 57 files, 58 folders, 59 path.join(dirname, `d-${depth}-${f}`), 60 createSymLinks 61 ); 62 } 63} 64 65function removeAsync(dir) { 66 // Removal should fail without the recursive option. 67 fs.rmdir(dir, common.mustCall((err) => { 68 assert.strictEqual(err.syscall, 'rmdir'); 69 70 // Removal should fail without the recursive option set to true. 71 fs.rmdir(dir, { recursive: false }, common.mustCall((err) => { 72 assert.strictEqual(err.syscall, 'rmdir'); 73 74 // Recursive removal should succeed. 75 fs.rmdir(dir, { recursive: true }, common.mustSucceed(() => { 76 // No error should occur if recursive and the directory does not exist. 77 fs.rmdir(dir, { recursive: true }, common.mustSucceed(() => { 78 // Attempted removal should fail now because the directory is gone. 79 fs.rmdir(dir, common.mustCall((err) => { 80 assert.strictEqual(err.syscall, 'rmdir'); 81 })); 82 })); 83 })); 84 })); 85 })); 86} 87 88// Test the asynchronous version 89{ 90 // Create a 4-level folder hierarchy including symlinks 91 let dir = nextDirPath(); 92 makeNonEmptyDirectory(4, 10, 2, dir, true); 93 removeAsync(dir); 94 95 // Create a 2-level folder hierarchy without symlinks 96 dir = nextDirPath(); 97 makeNonEmptyDirectory(2, 10, 2, dir, false); 98 removeAsync(dir); 99 100 // Create a flat folder including symlinks 101 dir = nextDirPath(); 102 makeNonEmptyDirectory(1, 10, 2, dir, true); 103 removeAsync(dir); 104} 105 106// Test the synchronous version. 107{ 108 const dir = nextDirPath(); 109 makeNonEmptyDirectory(4, 10, 2, dir, true); 110 111 // Removal should fail without the recursive option set to true. 112 assert.throws(() => { 113 fs.rmdirSync(dir); 114 }, { syscall: 'rmdir' }); 115 assert.throws(() => { 116 fs.rmdirSync(dir, { recursive: false }); 117 }, { syscall: 'rmdir' }); 118 119 // Recursive removal should succeed. 120 fs.rmdirSync(dir, { recursive: true }); 121 122 // No error should occur if recursive and the directory does not exist. 123 fs.rmdirSync(dir, { recursive: true }); 124 125 // Attempted removal should fail now because the directory is gone. 126 assert.throws(() => fs.rmdirSync(dir), { syscall: 'rmdir' }); 127} 128 129// Test the Promises based version. 130(async () => { 131 const dir = nextDirPath(); 132 makeNonEmptyDirectory(4, 10, 2, dir, true); 133 134 // Removal should fail without the recursive option set to true. 135 assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' }); 136 assert.rejects(fs.promises.rmdir(dir, { recursive: false }), { 137 syscall: 'rmdir' 138 }); 139 140 // Recursive removal should succeed. 141 await fs.promises.rmdir(dir, { recursive: true }); 142 143 // No error should occur if recursive and the directory does not exist. 144 await fs.promises.rmdir(dir, { recursive: true }); 145 146 // Attempted removal should fail now because the directory is gone. 147 assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' }); 148})().then(common.mustCall()); 149 150// Test input validation. 151{ 152 const defaults = { 153 retryDelay: 100, 154 maxRetries: 0, 155 recursive: false 156 }; 157 const modified = { 158 retryDelay: 953, 159 maxRetries: 5, 160 recursive: true 161 }; 162 163 assert.deepStrictEqual(validateRmdirOptions(), defaults); 164 assert.deepStrictEqual(validateRmdirOptions({}), defaults); 165 assert.deepStrictEqual(validateRmdirOptions(modified), modified); 166 assert.deepStrictEqual(validateRmdirOptions({ 167 maxRetries: 99 168 }), { 169 retryDelay: 100, 170 maxRetries: 99, 171 recursive: false 172 }); 173 174 [null, 'foo', 5, NaN].forEach((bad) => { 175 assert.throws(() => { 176 validateRmdirOptions(bad); 177 }, { 178 code: 'ERR_INVALID_ARG_TYPE', 179 name: 'TypeError', 180 message: /^The "options" argument must be of type object\./ 181 }); 182 }); 183 184 [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => { 185 assert.throws(() => { 186 validateRmdirOptions({ recursive: bad }); 187 }, { 188 code: 'ERR_INVALID_ARG_TYPE', 189 name: 'TypeError', 190 message: /^The "options\.recursive" property must be of type boolean\./ 191 }); 192 }); 193 194 assert.throws(() => { 195 validateRmdirOptions({ retryDelay: -1 }); 196 }, { 197 code: 'ERR_OUT_OF_RANGE', 198 name: 'RangeError', 199 message: /^The value of "options\.retryDelay" is out of range\./ 200 }); 201 202 assert.throws(() => { 203 validateRmdirOptions({ maxRetries: -1 }); 204 }, { 205 code: 'ERR_OUT_OF_RANGE', 206 name: 'RangeError', 207 message: /^The value of "options\.maxRetries" is out of range\./ 208 }); 209} 210 211// It should not pass recursive option to rmdirSync, when called from 212// rimraf (see: #35566) 213{ 214 // Make a non-empty directory: 215 const original = fs.rmdirSync; 216 const dir = `${nextDirPath()}/foo/bar`; 217 fs.mkdirSync(dir, { recursive: true }); 218 fs.writeFileSync(`${dir}/foo.txt`, 'hello world', 'utf8'); 219 220 // When called the second time from rimraf, the recursive option should 221 // not be set for rmdirSync: 222 let callCount = 0; 223 let rmdirSyncOptionsFromRimraf; 224 fs.rmdirSync = (path, options) => { 225 if (callCount > 0) { 226 rmdirSyncOptionsFromRimraf = { ...options }; 227 } 228 callCount++; 229 return original(path, options); 230 }; 231 fs.rmdirSync(dir, { recursive: true }); 232 fs.rmdirSync = original; 233 assert.strictEqual(rmdirSyncOptionsFromRimraf.recursive, undefined); 234} 235