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 { validateRmOptionsSync } = require('internal/fs/utils'); 9 10tmpdir.refresh(); 11 12let count = 0; 13const nextDirPath = (name = 'rm') => 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.rm(dir, common.mustCall((err) => { 68 assert.strictEqual(err.syscall, 'rm'); 69 70 // Removal should fail without the recursive option set to true. 71 fs.rm(dir, { recursive: false }, common.mustCall((err) => { 72 assert.strictEqual(err.syscall, 'rm'); 73 74 // Recursive removal should succeed. 75 fs.rm(dir, { recursive: true }, common.mustSucceed(() => { 76 77 // Attempted removal should fail now because the directory is gone. 78 fs.rm(dir, common.mustCall((err) => { 79 assert.strictEqual(err.syscall, 'stat'); 80 })); 81 })); 82 })); 83 })); 84} 85 86// Test the asynchronous version 87{ 88 // Create a 4-level folder hierarchy including symlinks 89 let dir = nextDirPath(); 90 makeNonEmptyDirectory(4, 10, 2, dir, true); 91 removeAsync(dir); 92 93 // Create a 2-level folder hierarchy without symlinks 94 dir = nextDirPath(); 95 makeNonEmptyDirectory(2, 10, 2, dir, false); 96 removeAsync(dir); 97 98 // Create a flat folder including symlinks 99 dir = nextDirPath(); 100 makeNonEmptyDirectory(1, 10, 2, dir, true); 101 removeAsync(dir); 102 103 // Should fail if target does not exist 104 fs.rm( 105 path.join(tmpdir.path, 'noexist.txt'), 106 { recursive: true }, 107 common.mustCall((err) => { 108 assert.strictEqual(err.code, 'ENOENT'); 109 }) 110 ); 111 112 // Should delete a file 113 const filePath = path.join(tmpdir.path, 'rm-async-file.txt'); 114 fs.writeFileSync(filePath, ''); 115 fs.rm(filePath, { recursive: true }, common.mustCall((err) => { 116 try { 117 assert.strictEqual(err, null); 118 assert.strictEqual(fs.existsSync(filePath), false); 119 } finally { 120 fs.rmSync(filePath, { force: true }); 121 } 122 })); 123} 124 125// Test the synchronous version. 126{ 127 const dir = nextDirPath(); 128 makeNonEmptyDirectory(4, 10, 2, dir, true); 129 130 // Removal should fail without the recursive option set to true. 131 assert.throws(() => { 132 fs.rmSync(dir); 133 }, { syscall: 'rm' }); 134 assert.throws(() => { 135 fs.rmSync(dir, { recursive: false }); 136 }, { syscall: 'rm' }); 137 138 // Should fail if target does not exist 139 assert.throws(() => { 140 fs.rmSync(path.join(tmpdir.path, 'noexist.txt'), { recursive: true }); 141 }, { 142 code: 'ENOENT', 143 name: 'Error', 144 message: /^ENOENT: no such file or directory, stat/ 145 }); 146 147 // Should delete a file 148 const filePath = path.join(tmpdir.path, 'rm-file.txt'); 149 fs.writeFileSync(filePath, ''); 150 151 try { 152 fs.rmSync(filePath, { recursive: true }); 153 } finally { 154 fs.rmSync(filePath, { force: true }); 155 } 156 157 // Recursive removal should succeed. 158 fs.rmSync(dir, { recursive: true }); 159 160 // Attempted removal should fail now because the directory is gone. 161 assert.throws(() => fs.rmSync(dir), { syscall: 'stat' }); 162} 163 164// Test the Promises based version. 165(async () => { 166 const dir = nextDirPath(); 167 makeNonEmptyDirectory(4, 10, 2, dir, true); 168 169 // Removal should fail without the recursive option set to true. 170 assert.rejects(fs.promises.rm(dir), { syscall: 'rm' }); 171 assert.rejects(fs.promises.rm(dir, { recursive: false }), { 172 syscall: 'rm' 173 }); 174 175 // Recursive removal should succeed. 176 await fs.promises.rm(dir, { recursive: true }); 177 178 // Attempted removal should fail now because the directory is gone. 179 assert.rejects(fs.promises.rm(dir), { syscall: 'stat' }); 180 181 // Should fail if target does not exist 182 assert.rejects(fs.promises.rm( 183 path.join(tmpdir.path, 'noexist.txt'), 184 { recursive: true } 185 ), { 186 code: 'ENOENT', 187 name: 'Error', 188 message: /^ENOENT: no such file or directory, stat/ 189 }); 190 191 // Should not fail if target does not exist and force option is true 192 fs.promises.rm(path.join(tmpdir.path, 'noexist.txt'), { force: true }); 193 194 // Should delete file 195 const filePath = path.join(tmpdir.path, 'rm-promises-file.txt'); 196 fs.writeFileSync(filePath, ''); 197 198 try { 199 await fs.promises.rm(filePath, { recursive: true }); 200 } finally { 201 fs.rmSync(filePath, { force: true }); 202 } 203})().then(common.mustCall()); 204 205// Test input validation. 206{ 207 const dir = nextDirPath(); 208 makeNonEmptyDirectory(4, 10, 2, dir, true); 209 const filePath = (path.join(tmpdir.path, 'rm-args-file.txt')); 210 fs.writeFileSync(filePath, ''); 211 212 const defaults = { 213 retryDelay: 100, 214 maxRetries: 0, 215 recursive: false, 216 force: false 217 }; 218 const modified = { 219 retryDelay: 953, 220 maxRetries: 5, 221 recursive: true, 222 force: false 223 }; 224 225 assert.deepStrictEqual(validateRmOptionsSync(filePath), defaults); 226 assert.deepStrictEqual(validateRmOptionsSync(filePath, {}), defaults); 227 assert.deepStrictEqual(validateRmOptionsSync(filePath, modified), modified); 228 assert.deepStrictEqual(validateRmOptionsSync(filePath, { 229 maxRetries: 99 230 }), { 231 retryDelay: 100, 232 maxRetries: 99, 233 recursive: false, 234 force: false 235 }); 236 237 [null, 'foo', 5, NaN].forEach((bad) => { 238 assert.throws(() => { 239 validateRmOptionsSync(filePath, bad); 240 }, { 241 code: 'ERR_INVALID_ARG_TYPE', 242 name: 'TypeError', 243 message: /^The "options" argument must be of type object\./ 244 }); 245 }); 246 247 [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => { 248 assert.throws(() => { 249 validateRmOptionsSync(filePath, { recursive: bad }); 250 }, { 251 code: 'ERR_INVALID_ARG_TYPE', 252 name: 'TypeError', 253 message: /^The "options\.recursive" property must be of type boolean\./ 254 }); 255 }); 256 257 [undefined, null, 'foo', Infinity, function() {}].forEach((bad) => { 258 assert.throws(() => { 259 validateRmOptionsSync(filePath, { force: bad }); 260 }, { 261 code: 'ERR_INVALID_ARG_TYPE', 262 name: 'TypeError', 263 message: /^The "options\.force" property must be of type boolean\./ 264 }); 265 }); 266 267 assert.throws(() => { 268 validateRmOptionsSync(filePath, { retryDelay: -1 }); 269 }, { 270 code: 'ERR_OUT_OF_RANGE', 271 name: 'RangeError', 272 message: /^The value of "options\.retryDelay" is out of range\./ 273 }); 274 275 assert.throws(() => { 276 validateRmOptionsSync(filePath, { maxRetries: -1 }); 277 }, { 278 code: 'ERR_OUT_OF_RANGE', 279 name: 'RangeError', 280 message: /^The value of "options\.maxRetries" is out of range\./ 281 }); 282} 283