Skip to content

Commit d820ac1

Browse files
committed
fs: move rmSync implementation to c++
1 parent 895fcb0 commit d820ac1

File tree

7 files changed

+100
-177
lines changed

7 files changed

+100
-177
lines changed

lib/fs.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ let promises = null;
159159
let ReadStream;
160160
let WriteStream;
161161
let rimraf;
162-
let rimrafSync;
163162
let kResistStopPropagation;
164163

165164
// These have to be separate because of how graceful-fs happens to do it's
@@ -1126,7 +1125,7 @@ function lazyLoadCp() {
11261125

11271126
function lazyLoadRimraf() {
11281127
if (rimraf === undefined)
1129-
({ rimraf, rimrafSync } = require('internal/fs/rimraf'));
1128+
({ rimraf } = require('internal/fs/rimraf'));
11301129
}
11311130

11321131
/**
@@ -1194,8 +1193,7 @@ function rmdirSync(path, options) {
11941193
emitRecursiveRmdirWarning();
11951194
options = validateRmOptionsSync(path, { ...options, force: false }, true);
11961195
if (options !== false) {
1197-
lazyLoadRimraf();
1198-
return rimrafSync(path, options);
1196+
return binding.rmSync(path, options.maxRetries, options.recursive, options.retryDelay);
11991197
}
12001198
} else {
12011199
validateRmdirOptions(options);
@@ -1246,11 +1244,8 @@ function rm(path, options, callback) {
12461244
* @returns {void}
12471245
*/
12481246
function rmSync(path, options) {
1249-
lazyLoadRimraf();
1250-
return rimrafSync(
1251-
getValidatedPath(path),
1252-
validateRmOptionsSync(path, options, false),
1253-
);
1247+
const opts = validateRmOptionsSync(path, options, false);
1248+
return binding.rmSync(getValidatedPath(path), opts.maxRetries, opts.recursive, opts.retryDelay);
12541249
}
12551250

12561251
/**

lib/internal/fs/rimraf.js

Lines changed: 1 addition & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,19 @@ const { Buffer } = require('buffer');
1616
const fs = require('fs');
1717
const {
1818
chmod,
19-
chmodSync,
2019
lstat,
21-
lstatSync,
2220
readdir,
23-
readdirSync,
2421
rmdir,
25-
rmdirSync,
2622
stat,
27-
statSync,
2823
unlink,
29-
unlinkSync,
3024
} = fs;
3125
const { sep } = require('path');
3226
const { setTimeout } = require('timers');
33-
const { sleep } = require('internal/util');
3427
const notEmptyErrorCodes = new SafeSet(['ENOTEMPTY', 'EEXIST', 'EPERM']);
3528
const retryErrorCodes = new SafeSet(
3629
['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'EPERM']);
3730
const isWindows = process.platform === 'win32';
3831
const epermHandler = isWindows ? fixWinEPERM : _rmdir;
39-
const epermHandlerSync = isWindows ? fixWinEPERMSync : _rmdirSync;
4032
const readdirEncoding = 'buffer';
4133
const separator = Buffer.from(sep);
4234

@@ -173,138 +165,4 @@ function rimrafPromises(path, options) {
173165
}
174166

175167

176-
function 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-
209-
function _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-
233-
function _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-
283-
function 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-
310-
module.exports = { rimraf, rimrafPromises, rimrafSync };
168+
module.exports = { rimraf, rimrafPromises };

src/node_errors.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
7070
V(ERR_DLOPEN_FAILED, Error) \
7171
V(ERR_ENCODING_INVALID_ENCODED_DATA, TypeError) \
7272
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
73+
V(ERR_FS_EISDIR, Error) \
7374
V(ERR_ILLEGAL_CONSTRUCTOR, Error) \
7475
V(ERR_INVALID_ADDRESS, Error) \
7576
V(ERR_INVALID_ARG_VALUE, TypeError) \

src/node_file.cc

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@
4848
# include <io.h>
4949
#endif
5050

51+
#ifndef _WIN32
52+
#include <unistd.h>
53+
#else
54+
#include <windows.h>
55+
#endif
56+
5157
namespace node {
5258

5359
namespace fs {
@@ -1605,6 +1611,88 @@ static void RMDir(const FunctionCallbackInfo<Value>& args) {
16051611
}
16061612
}
16071613

1614+
static void RmSync(const FunctionCallbackInfo<Value>& args) {
1615+
Environment* env = Environment::GetCurrent(args);
1616+
Isolate* isolate = env->isolate();
1617+
1618+
CHECK_EQ(args.Length(), 4); // path, maxRetries, recursive, retryDelay
1619+
1620+
BufferValue path(isolate, args[0]);
1621+
CHECK_NOT_NULL(*path);
1622+
ToNamespacedPath(env, &path);
1623+
THROW_IF_INSUFFICIENT_PERMISSIONS(
1624+
env, permission::PermissionScope::kFileSystemWrite, path.ToStringView());
1625+
auto file_path = std::filesystem::path(path.ToStringView());
1626+
std::error_code error;
1627+
auto file_status = std::filesystem::status(file_path, error);
1628+
1629+
if (file_status.type() == std::filesystem::file_type::not_found) {
1630+
return;
1631+
}
1632+
1633+
int maxRetries = args[1].As<Int32>()->Value();
1634+
int recursive = args[2]->IsTrue();
1635+
int retryDelay = args[3].As<Int32>()->Value();
1636+
1637+
// File is a directory and recursive is false
1638+
if (file_status.type() == std::filesystem::file_type::directory &&
1639+
!recursive) {
1640+
return THROW_ERR_FS_EISDIR(
1641+
isolate, "Path is a directory: %s", file_path.c_str());
1642+
}
1643+
1644+
// Allowed errors are:
1645+
// - EBUSY: std::errc::device_or_resource_busy
1646+
// - EMFILE: std::errc::too_many_files_open
1647+
// - ENFILE: std::errc::too_many_files_open_in_system
1648+
// - ENOTEMPTY: std::errc::directory_not_empty
1649+
// - EPERM: std::errc::operation_not_permitted
1650+
auto can_omit_error = [](std::error_code error) -> bool {
1651+
return (error == std::errc::device_or_resource_busy ||
1652+
error == std::errc::too_many_files_open ||
1653+
error == std::errc::too_many_files_open_in_system ||
1654+
error == std::errc::directory_not_empty ||
1655+
error == std::errc::operation_not_permitted);
1656+
};
1657+
1658+
while (maxRetries >= 0) {
1659+
if (recursive) {
1660+
std::filesystem::remove_all(file_path, error);
1661+
} else {
1662+
std::filesystem::remove(file_path, error);
1663+
}
1664+
1665+
if (!error || error == std::errc::no_such_file_or_directory) {
1666+
return;
1667+
} else if (!can_omit_error(error)) {
1668+
break;
1669+
}
1670+
1671+
if (retryDelay != 0) {
1672+
sleep(retryDelay / 1000);
1673+
}
1674+
maxRetries--;
1675+
}
1676+
1677+
if (error == std::errc::operation_not_permitted) {
1678+
std::string message = "Operation not permitted: " + file_path.string();
1679+
return env->ThrowErrnoException(
1680+
EPERM, "rm", message.c_str(), file_path.c_str());
1681+
} else if (error == std::errc::directory_not_empty) {
1682+
std::string message = "Directory not empty: " + file_path.string();
1683+
return env->ThrowErrnoException(
1684+
EACCES, "rm", message.c_str(), file_path.c_str());
1685+
} else if (error == std::errc::not_a_directory) {
1686+
std::string message = "Not a directory: " + file_path.string();
1687+
return env->ThrowErrnoException(
1688+
ENOTDIR, "rm", message.c_str(), file_path.c_str());
1689+
}
1690+
1691+
std::string message = "Unknown error: " + error.message();
1692+
return env->ThrowErrnoException(
1693+
UV_UNKNOWN, "rm", message.c_str(), file_path.c_str());
1694+
}
1695+
16081696
int MKDirpSync(uv_loop_t* loop,
16091697
uv_fs_t* req,
16101698
const std::string& path,
@@ -3323,6 +3411,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
33233411
SetMethod(isolate, target, "rename", Rename);
33243412
SetMethod(isolate, target, "ftruncate", FTruncate);
33253413
SetMethod(isolate, target, "rmdir", RMDir);
3414+
SetMethod(isolate, target, "rmSync", RmSync);
33263415
SetMethod(isolate, target, "mkdir", MKDir);
33273416
SetMethod(isolate, target, "readdir", ReadDir);
33283417
SetFastMethod(isolate,
@@ -3447,6 +3536,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
34473536
registry->Register(Rename);
34483537
registry->Register(FTruncate);
34493538
registry->Register(RMDir);
3539+
registry->Register(RmSync);
34503540
registry->Register(MKDir);
34513541
registry->Register(ReadDir);
34523542
registry->Register(InternalModuleStat);

test/parallel/test-fs-rm.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ if (isGitPresent) {
541541
name: 'Error',
542542
});
543543
} catch (err) {
544+
console.log(err);
544545
// Only fail the test if the folder was not deleted.
545546
// as in some cases rmSync successfully deletes read-only folders.
546547
if (fs.existsSync(root)) {

test/parallel/test-fs-rmdir-recursive.js

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -217,28 +217,3 @@ function removeAsync(dir) {
217217
message: /^The value of "options\.maxRetries" is out of range\./
218218
});
219219
}
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-
}

typings/internalBinding/fs.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ declare namespace InternalFSBinding {
194194
function rmdir(path: string): void;
195195
function rmdir(path: string, usePromises: typeof kUsePromises): Promise<void>;
196196

197+
function rmSync(path: StringOrBuffer, maxRetries: number, recursive: boolean, retryDelay: number): void;
198+
197199
function stat(path: StringOrBuffer, useBigint: boolean, req: FSReqCallback<Float64Array | BigUint64Array>): void;
198200
function stat(path: StringOrBuffer, useBigint: true, req: FSReqCallback<BigUint64Array>): void;
199201
function stat(path: StringOrBuffer, useBigint: false, req: FSReqCallback<Float64Array>): void;
@@ -276,6 +278,7 @@ export interface FsBinding {
276278
realpath: typeof InternalFSBinding.realpath;
277279
rename: typeof InternalFSBinding.rename;
278280
rmdir: typeof InternalFSBinding.rmdir;
281+
rmSync: typeof InternalFSBinding.rmSync;
279282
stat: typeof InternalFSBinding.stat;
280283
symlink: typeof InternalFSBinding.symlink;
281284
unlink: typeof InternalFSBinding.unlink;

0 commit comments

Comments
 (0)