xref: /third_party/node/lib/internal/fs/rimraf.js (revision 1cb0ef41)
1// This file is a modified version of the rimraf module on npm. It has been
2// modified in the following ways:
3// - Use of the assert module has been replaced with core's error system.
4// - All code related to the glob dependency has been removed.
5// - Bring your own custom fs module is not currently supported.
6// - Some basic code cleanup.
7'use strict';
8
9const {
10  ArrayPrototypeForEach,
11  Promise,
12  SafeSet,
13} = primordials;
14
15const { Buffer } = require('buffer');
16const fs = require('fs');
17const {
18  chmod,
19  chmodSync,
20  lstat,
21  lstatSync,
22  readdir,
23  readdirSync,
24  rmdir,
25  rmdirSync,
26  stat,
27  statSync,
28  unlink,
29  unlinkSync,
30} = fs;
31const { sep } = require('path');
32const { setTimeout } = require('timers');
33const { sleep } = require('internal/util');
34const notEmptyErrorCodes = new SafeSet(['ENOTEMPTY', 'EEXIST', 'EPERM']);
35const retryErrorCodes = new SafeSet(
36  ['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'EPERM']);
37const isWindows = process.platform === 'win32';
38const epermHandler = isWindows ? fixWinEPERM : _rmdir;
39const epermHandlerSync = isWindows ? fixWinEPERMSync : _rmdirSync;
40const readdirEncoding = 'buffer';
41const separator = Buffer.from(sep);
42
43
44function rimraf(path, options, callback) {
45  let retries = 0;
46
47  _rimraf(path, options, function CB(err) {
48    if (err) {
49      if (retryErrorCodes.has(err.code) && retries < options.maxRetries) {
50        retries++;
51        const delay = retries * options.retryDelay;
52        return setTimeout(_rimraf, delay, path, options, CB);
53      }
54
55      // The file is already gone.
56      if (err.code === 'ENOENT')
57        err = null;
58    }
59
60    callback(err);
61  });
62}
63
64
65function _rimraf(path, options, callback) {
66  // SunOS lets the root user unlink directories. Use lstat here to make sure
67  // it's not a directory.
68  lstat(path, (err, stats) => {
69    if (err) {
70      if (err.code === 'ENOENT')
71        return callback(null);
72
73      // Windows can EPERM on stat.
74      if (isWindows && err.code === 'EPERM')
75        return fixWinEPERM(path, options, err, callback);
76    } else if (stats.isDirectory()) {
77      return _rmdir(path, options, err, callback);
78    }
79
80    unlink(path, (err) => {
81      if (err) {
82        if (err.code === 'ENOENT')
83          return callback(null);
84        if (err.code === 'EISDIR')
85          return _rmdir(path, options, err, callback);
86        if (err.code === 'EPERM') {
87          return epermHandler(path, options, err, callback);
88        }
89      }
90
91      return callback(err);
92    });
93  });
94}
95
96
97function fixWinEPERM(path, options, originalErr, callback) {
98  chmod(path, 0o666, (err) => {
99    if (err)
100      return callback(err.code === 'ENOENT' ? null : originalErr);
101
102    stat(path, (err, stats) => {
103      if (err)
104        return callback(err.code === 'ENOENT' ? null : originalErr);
105
106      if (stats.isDirectory())
107        _rmdir(path, options, originalErr, callback);
108      else
109        unlink(path, callback);
110    });
111  });
112}
113
114
115function _rmdir(path, options, originalErr, callback) {
116  rmdir(path, (err) => {
117    if (err) {
118      if (notEmptyErrorCodes.has(err.code))
119        return _rmchildren(path, options, callback);
120      if (err.code === 'ENOTDIR')
121        return callback(originalErr);
122    }
123
124    callback(err);
125  });
126}
127
128
129function _rmchildren(path, options, callback) {
130  const pathBuf = Buffer.from(path);
131
132  readdir(pathBuf, readdirEncoding, (err, files) => {
133    if (err)
134      return callback(err);
135
136    let numFiles = files.length;
137
138    if (numFiles === 0)
139      return rmdir(path, callback);
140
141    let done = false;
142
143    ArrayPrototypeForEach(files, (child) => {
144      const childPath = Buffer.concat([pathBuf, separator, child]);
145
146      rimraf(childPath, options, (err) => {
147        if (done)
148          return;
149
150        if (err) {
151          done = true;
152          return callback(err);
153        }
154
155        numFiles--;
156        if (numFiles === 0)
157          rmdir(path, callback);
158      });
159    });
160  });
161}
162
163
164function rimrafPromises(path, options) {
165  return new Promise((resolve, reject) => {
166    rimraf(path, options, (err) => {
167      if (err)
168        return reject(err);
169
170      resolve();
171    });
172  });
173}
174
175
176function rimrafSync(path, options) {
177  let stats;
178
179  try {
180    stats = lstatSync(path);
181  } catch (err) {
182    if (err.code === 'ENOENT')
183      return;
184
185    // Windows can EPERM on stat.
186    if (isWindows && err.code === 'EPERM')
187      fixWinEPERMSync(path, options, err);
188  }
189
190  try {
191    // SunOS lets the root user unlink directories.
192    if (stats?.isDirectory())
193      _rmdirSync(path, options, null);
194    else
195      _unlinkSync(path, options);
196  } catch (err) {
197    if (err.code === 'ENOENT')
198      return;
199    if (err.code === 'EPERM')
200      return epermHandlerSync(path, options, err);
201    if (err.code !== 'EISDIR')
202      throw err;
203
204    _rmdirSync(path, options, err);
205  }
206}
207
208
209function _unlinkSync(path, options) {
210  const tries = options.maxRetries + 1;
211
212  for (let i = 1; i <= tries; i++) {
213    try {
214      return unlinkSync(path);
215    } catch (err) {
216      // Only sleep if this is not the last try, and the delay is greater
217      // than zero, and an error was encountered that warrants a retry.
218      if (retryErrorCodes.has(err.code) &&
219          i < tries &&
220          options.retryDelay > 0) {
221        sleep(i * options.retryDelay);
222      } else if (err.code === 'ENOENT') {
223        // The file is already gone.
224        return;
225      } else if (i === tries) {
226        throw err;
227      }
228    }
229  }
230}
231
232
233function _rmdirSync(path, options, originalErr) {
234  try {
235    rmdirSync(path);
236  } catch (err) {
237    if (err.code === 'ENOENT')
238      return;
239    if (err.code === 'ENOTDIR') {
240      throw originalErr || err;
241    }
242
243    if (notEmptyErrorCodes.has(err.code)) {
244      // Removing failed. Try removing all children and then retrying the
245      // original removal. Windows has a habit of not closing handles promptly
246      // when files are deleted, resulting in spurious ENOTEMPTY failures. Work
247      // around that issue by retrying on Windows.
248      const pathBuf = Buffer.from(path);
249
250      ArrayPrototypeForEach(readdirSync(pathBuf, readdirEncoding), (child) => {
251        const childPath = Buffer.concat([pathBuf, separator, child]);
252
253        rimrafSync(childPath, options);
254      });
255
256      const tries = options.maxRetries + 1;
257
258      for (let i = 1; i <= tries; i++) {
259        try {
260          return fs.rmdirSync(path);
261        } catch (err) {
262          // Only sleep if this is not the last try, and the delay is greater
263          // than zero, and an error was encountered that warrants a retry.
264          if (retryErrorCodes.has(err.code) &&
265              i < tries &&
266              options.retryDelay > 0) {
267            sleep(i * options.retryDelay);
268          } else if (err.code === 'ENOENT') {
269            // The file is already gone.
270            return;
271          } else if (i === tries) {
272            throw err;
273          }
274        }
275      }
276    }
277
278    throw originalErr || err;
279  }
280}
281
282
283function fixWinEPERMSync(path, options, originalErr) {
284  try {
285    chmodSync(path, 0o666);
286  } catch (err) {
287    if (err.code === 'ENOENT')
288      return;
289
290    throw originalErr;
291  }
292
293  let stats;
294
295  try {
296    stats = statSync(path, { throwIfNoEntry: false });
297  } catch {
298    throw originalErr;
299  }
300
301  if (stats === undefined) return;
302
303  if (stats.isDirectory())
304    _rmdirSync(path, options, originalErr);
305  else
306    _unlinkSync(path, options);
307}
308
309
310module.exports = { rimraf, rimrafPromises, rimrafSync };
311