• 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 { 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