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