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