• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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