-
Notifications
You must be signed in to change notification settings - Fork 29.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make recursive rmdir more strict #35250
Changes from 3 commits
02cee53
43c785e
e26dbba
0eb9255
7fb4613
523ec9b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,11 @@ const { | |
const { sep } = require('path'); | ||
const { setTimeout } = require('timers'); | ||
const { sleep } = require('internal/util'); | ||
const { | ||
codes: { | ||
ERR_INVALID_ARG_VALUE | ||
}, | ||
} = require('internal/errors'); | ||
const notEmptyErrorCodes = new Set(['ENOTEMPTY', 'EEXIST', 'EPERM']); | ||
const retryErrorCodes = new Set( | ||
['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'EPERM']); | ||
|
@@ -40,14 +45,29 @@ const separator = Buffer.from(sep); | |
|
||
|
||
function rimraf(path, options, callback) { | ||
stat(path, (err, stats) => { | ||
if (err && err.code === 'ENOENT') { | ||
callback(err); | ||
} else if (stats && !stats.isDirectory()) { | ||
callback(new ERR_INVALID_ARG_VALUE( | ||
'path', path, 'is not a directory' | ||
)); | ||
} else { | ||
_rimraf(path, options, callback); | ||
} | ||
}); | ||
} | ||
|
||
|
||
function _rimraf(path, options, callback) { | ||
let retries = 0; | ||
|
||
_rimraf(path, options, function CB(err) { | ||
__rimraf(path, options, function CB(err) { | ||
if (err) { | ||
if (retryErrorCodes.has(err.code) && retries < options.maxRetries) { | ||
retries++; | ||
const delay = retries * options.retryDelay; | ||
return setTimeout(_rimraf, delay, path, options, CB); | ||
return setTimeout(__rimraf, delay, path, options, CB); | ||
} | ||
|
||
// The file is already gone. | ||
|
@@ -60,7 +80,7 @@ function rimraf(path, options, callback) { | |
} | ||
|
||
|
||
function _rimraf(path, options, callback) { | ||
function __rimraf(path, options, callback) { | ||
// SunOS lets the root user unlink directories. Use lstat here to make sure | ||
// it's not a directory. | ||
lstat(path, (err, stats) => { | ||
|
@@ -141,7 +161,7 @@ function _rmchildren(path, options, callback) { | |
files.forEach((child) => { | ||
const childPath = Buffer.concat([pathBuf, separator, child]); | ||
|
||
rimraf(childPath, options, (err) => { | ||
_rimraf(childPath, options, (err) => { | ||
if (done) | ||
return; | ||
|
||
|
@@ -174,6 +194,24 @@ function rimrafPromises(path, options) { | |
function rimrafSync(path, options) { | ||
let stats; | ||
|
||
try { | ||
stats = statSync(path); | ||
} catch (err) { | ||
if (err.code === 'ENOENT') | ||
throw err; | ||
} | ||
|
||
if (!stats.isDirectory()) { | ||
throw new ERR_INVALID_ARG_VALUE('path', path, 'is not a directory'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this the correct error to throw? unsure of the intent of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that it is. I wanted to throw an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There doesn't seem to be a convenience function for this error; maybe we should create one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @iansu in E('ENOTDIR',
'no such file or directory',
SystemError); You'd then instantiate it something like this: new SystemError('ENOTDIR', {syscall: 'rmdir', code: 'ENOTDIR', path: './', message: 'whatever error message rimraf throws'', errno: whatever error code rmdir has}) Basically the ideal is that someone would be able to look at Edit: I mean ENOTDIR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoops, sorry I mean |
||
} | ||
|
||
_rimrafSync(path, options); | ||
} | ||
|
||
|
||
function _rimrafSync(path, options) { | ||
let stats; | ||
|
||
try { | ||
stats = lstatSync(path); | ||
} catch (err) { | ||
|
@@ -242,7 +280,7 @@ function _rmdirSync(path, options, originalErr) { | |
readdirSync(pathBuf, readdirEncoding).forEach((child) => { | ||
const childPath = Buffer.concat([pathBuf, separator, child]); | ||
|
||
rimrafSync(childPath, options); | ||
_rimrafSync(childPath, options); | ||
}); | ||
|
||
const tries = options.maxRetries + 1; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,14 +75,9 @@ function removeAsync(dir) { | |
fs.rmdir(dir, { recursive: true }, common.mustCall((err) => { | ||
assert.ifError(err); | ||
|
||
// No error should occur if recursive and the directory does not exist. | ||
fs.rmdir(dir, { recursive: true }, common.mustCall((err) => { | ||
assert.ifError(err); | ||
|
||
// Attempted removal should fail now because the directory is gone. | ||
fs.rmdir(dir, common.mustCall((err) => { | ||
assert.strictEqual(err.syscall, 'rmdir'); | ||
})); | ||
// Attempted removal should fail now because the directory is gone. | ||
fs.rmdir(dir, common.mustCall((err) => { | ||
assert.strictEqual(err.syscall, 'rmdir'); | ||
})); | ||
})); | ||
})); | ||
|
@@ -105,6 +100,25 @@ function removeAsync(dir) { | |
dir = nextDirPath(); | ||
makeNonEmptyDirectory(1, 10, 2, dir, true); | ||
removeAsync(dir); | ||
|
||
// Should fail if target does not exist | ||
fs.rmdir( | ||
path.join(tmpdir.path, 'noexist.txt'), | ||
{ recursive: true }, | ||
common.mustCall((err) => { | ||
assert.strictEqual(err.code, 'ENOENT'); | ||
}) | ||
); | ||
|
||
// Should fail if target is a file | ||
const filePath = path.join(tmpdir.path, 'rmdir-async-file.txt'); | ||
fs.writeFileSync(filePath, ''); | ||
fs.rmdir(filePath, { recursive: true }, common.mustCall((err) => { | ||
assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE'); | ||
assert.strictEqual(err.name, 'TypeError'); | ||
assert.match(err.message, /^The argument 'path' is not a directory\./); | ||
fs.unlinkSync(filePath); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this needs a |
||
})); | ||
} | ||
|
||
// Test the synchronous version. | ||
|
@@ -120,10 +134,33 @@ function removeAsync(dir) { | |
fs.rmdirSync(dir, { recursive: false }); | ||
}, { syscall: 'rmdir' }); | ||
|
||
// Recursive removal should succeed. | ||
fs.rmdirSync(dir, { recursive: true }); | ||
// Should fail if target does not exist | ||
assert.throws(() => { | ||
fs.rmdirSync(path.join(tmpdir.path, 'noexist.txt'), { recursive: true }); | ||
}, { | ||
code: 'ENOENT', | ||
name: 'Error', | ||
message: /^ENOENT: no such file or directory, stat/ | ||
}); | ||
|
||
// Should fail if target is a file | ||
const filePath = path.join(tmpdir.path, 'rmdir-sync-file.txt'); | ||
fs.writeFileSync(filePath, ''); | ||
|
||
try { | ||
assert.throws(() => { | ||
fs.rmdirSync(filePath, { recursive: true }); | ||
}, { | ||
code: 'ERR_INVALID_ARG_VALUE', | ||
name: 'TypeError', | ||
message: /^The argument 'path' is not a directory\./ | ||
}); | ||
} finally { | ||
fs.unlinkSync(filePath); | ||
} | ||
|
||
|
||
// No error should occur if recursive and the directory does not exist. | ||
// Recursive removal should succeed. | ||
fs.rmdirSync(dir, { recursive: true }); | ||
|
||
// Attempted removal should fail now because the directory is gone. | ||
|
@@ -144,8 +181,32 @@ function removeAsync(dir) { | |
// Recursive removal should succeed. | ||
await fs.promises.rmdir(dir, { recursive: true }); | ||
|
||
// No error should occur if recursive and the directory does not exist. | ||
await fs.promises.rmdir(dir, { recursive: true }); | ||
// Should fail if target does not exist | ||
assert.rejects(fs.promises.rmdir( | ||
path.join(tmpdir.path, 'noexist.txt'), | ||
{ recursive: true } | ||
), { | ||
code: 'ENOENT', | ||
name: 'Error', | ||
message: /^ENOENT: no such file or directory, stat/ | ||
}); | ||
|
||
// Should fail if target is a file | ||
const filePath = path.join(tmpdir.path, 'rmdir-promises-file.txt'); | ||
fs.writeFileSync(filePath, ''); | ||
|
||
try { | ||
assert.rejects(fs.promises.rmdir( | ||
filePath, | ||
{ recursive: true } | ||
), { | ||
code: 'ERR_INVALID_ARG_VALUE', | ||
name: 'TypeError', | ||
message: /^The argument 'path' is not a directory\./ | ||
}); | ||
} finally { | ||
fs.unlinkSync(filePath); | ||
} | ||
|
||
// Attempted removal should fail now because the directory is gone. | ||
assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' }); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is swallowed here if not
ENOENT
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rimraf
swallows all kinds of errors and throwing all errors fromstat
caused lots of failures.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it looks like you'd get into trouble here if L198 threw, because
stats
would then be undefined, and the call toisDirectory()
on L204 would fail... it doesn't seem recoverable?