Description
Proposal
Problem statement
Currently, fs::remove_dir_all
will fail if the directory in question does not exist. It will also fail if any descendant fails deletion for any reason, even if that's because it doesn't exist.
This means the API is not idempotent, and can fail when multiple threads or processes call it concurrently.
Motivation, use-cases
The current documentation for this function does not directly state the above behavior, instead simply punting to the docs for fs::remove_file
and fs::remove_dir
.
The current behavior is also in stark contrast to fs::create_dir_all
, which has been race-resistant since 2017:
This function will return an error in the following situations, but is not limited to just these cases:
- If any directory in the path specified by path does not already exist and it could not be created otherwise. The specific error conditions for when a directory is being created (after it is determined to not exist) are outlined by fs::create_dir.
Notable exception is made for situations where any of the directories specified in the path could not be created as it was being created concurrently. Such cases are considered to be successful. That is, calling create_dir_all concurrently from multiple threads or processes is guaranteed not to fail due to a race condition with itself.
Solution sketches
Change the remove_dir_all
algorithm to work similarly to create_dir_all
:
- If the directory at path does not exist, immediately exit
Ok(())
. - Accept
ErrorKind::NotFound
as if the deletion succeeded. - Any other errors from
read_dir
andremove_dir
are immediately returned.
pub fn remove_dir_all(path: &Path) -> io::Result<()> {
let filetype = match fs::symlink_metadata(path) {
Ok(x) => x.file_type(),
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
if filetype.is_symlink() {
match fs::remove_file(path) {
Ok(_) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
} else { remove_dir_all_recursive(path) }
}
fn remove_dir_all_recursive(path: &Path) -> io::Result<()> {
let dir = match fs::read_dir(path) {
Ok(x) => x,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
for child in dir {
let child = match child {
Ok(x) => x,
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
Err(e) => return Err(e),
};
let file_type = match child.file_type() {
Ok(x) => x,
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
Err(e) => return Err(e),
};
if file_type.is_dir() {
remove_dir_all_recursive(&child.path())?;
} else {
match fs::remove_file(&child.path()) {
Ok(_) => (),
Err(e) if e.kind() == io::ErrorKind::NotFound => (),
Err(e) => return Err(e),
}
}
}
match fs::remove_dir(path) {
Ok(_) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
Links and related work
- PR that spawned this discussion: docs(std): clarify remove_dir_all errors rust#105745
- Commit that made
create_dir_all
race-resistant: rust-lang/rust@db00ba9
What happens now?
This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.