-
-
Notifications
You must be signed in to change notification settings - Fork 34.3k
Make recursive rmdir more strict #35250
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
Changes from 1 commit
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'); | ||
|
||
| } | ||
|
|
||
| _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); | ||
|
||
| } | ||
|
|
||
| // Test the synchronous version. | ||
|
|
@@ -120,10 +134,30 @@ 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, ''); | ||
|
|
||
| assert.throws(() => { | ||
| fs.rmdirSync(filePath, { recursive: true }); | ||
| }, { | ||
| code: 'ERR_INVALID_ARG_VALUE', | ||
| name: 'TypeError', | ||
| message: /^The argument 'path' is not a directory\./ | ||
| }); | ||
|
|
||
| // No error should occur if recursive and the directory does not exist. | ||
| fs.unlinkSync(filePath); | ||
|
|
||
| // Recursive removal should succeed. | ||
| fs.rmdirSync(dir, { recursive: true }); | ||
|
|
||
| // Attempted removal should fail now because the directory is gone. | ||
|
|
@@ -144,8 +178,28 @@ 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-sync-file.txt'); | ||
| fs.writeFileSync(filePath, ''); | ||
|
|
||
| assert.rejects(fs.promises.rmdir( | ||
| filePath, | ||
| { recursive: true } | ||
| ), { | ||
| code: 'ERR_INVALID_ARG_VALUE', | ||
| name: 'TypeError', | ||
| message: /^The argument 'path' is not a directory\./ | ||
| }); | ||
|
|
||
| // 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.
rimrafswallows all kinds of errors and throwing all errors fromstatcaused 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
statswould then be undefined, and the call toisDirectory()on L204 would fail... it doesn't seem recoverable?