Windows: Iterative remove_dir_all
This will allow better strategies for use of memory and File handles. However, fully taking advantage of that is left to future work.
This commit is contained in:
parent
7417110cef
commit
8b1f85caed
@ -680,7 +680,7 @@ fn new(buffer: &'a DirBuff) -> Self {
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for DirBuffIter<'a> {
|
||||
type Item = &'a [u16];
|
||||
type Item = (&'a [u16], bool);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use crate::mem::size_of;
|
||||
let buffer = &self.buffer?[self.cursor..];
|
||||
@ -689,14 +689,16 @@ fn next(&mut self) -> Option<Self::Item> {
|
||||
// SAFETY: The buffer contains a `FILE_ID_BOTH_DIR_INFO` struct but the
|
||||
// last field (the file name) is unsized. So an offset has to be
|
||||
// used to get the file name slice.
|
||||
let (name, next_entry) = unsafe {
|
||||
let (name, is_directory, next_entry) = unsafe {
|
||||
let info = buffer.as_ptr().cast::<c::FILE_ID_BOTH_DIR_INFO>();
|
||||
let next_entry = (*info).NextEntryOffset as usize;
|
||||
let name = crate::slice::from_raw_parts(
|
||||
(*info).FileName.as_ptr().cast::<u16>(),
|
||||
(*info).FileNameLength as usize / size_of::<u16>(),
|
||||
);
|
||||
(name, next_entry)
|
||||
let is_directory = ((*info).FileAttributes & c::FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
|
||||
(name, is_directory, next_entry)
|
||||
};
|
||||
|
||||
if next_entry == 0 {
|
||||
@ -709,7 +711,7 @@ fn next(&mut self) -> Option<Self::Item> {
|
||||
const DOT: u16 = b'.' as u16;
|
||||
match name {
|
||||
[DOT] | [DOT, DOT] => self.next(),
|
||||
_ => Some(name),
|
||||
_ => Some((name, is_directory)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -994,89 +996,77 @@ pub fn remove_dir_all(path: &Path) -> io::Result<()> {
|
||||
if (file.basic_info()?.FileAttributes & c::FILE_ATTRIBUTE_DIRECTORY) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(c::ERROR_DIRECTORY as _));
|
||||
}
|
||||
let mut delete: fn(&File) -> io::Result<()> = File::posix_delete;
|
||||
let result = match delete(&file) {
|
||||
Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => {
|
||||
match remove_dir_all_recursive(&file, delete) {
|
||||
// Return unexpected errors.
|
||||
Err(e) if e.kind() != io::ErrorKind::DirectoryNotEmpty => return Err(e),
|
||||
result => result,
|
||||
|
||||
match remove_dir_all_iterative(&file, File::posix_delete) {
|
||||
Err(e) => {
|
||||
if let Some(code) = e.raw_os_error() {
|
||||
match code as u32 {
|
||||
// If POSIX delete is not supported for this filesystem then fallback to win32 delete.
|
||||
c::ERROR_NOT_SUPPORTED
|
||||
| c::ERROR_INVALID_FUNCTION
|
||||
| c::ERROR_INVALID_PARAMETER => {
|
||||
remove_dir_all_iterative(&file, File::win32_delete)
|
||||
}
|
||||
_ => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
// If POSIX delete is not supported for this filesystem then fallback to win32 delete.
|
||||
Err(e)
|
||||
if e.raw_os_error() == Some(c::ERROR_NOT_SUPPORTED as i32)
|
||||
|| e.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as i32) =>
|
||||
{
|
||||
delete = File::win32_delete;
|
||||
Err(e)
|
||||
}
|
||||
result => result,
|
||||
};
|
||||
if result.is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
// This is a fallback to make sure the directory is actually deleted.
|
||||
// Otherwise this function is prone to failing with `DirectoryNotEmpty`
|
||||
// due to possible delays between marking a file for deletion and the
|
||||
// file actually being deleted from the filesystem.
|
||||
//
|
||||
// So we retry a few times before giving up.
|
||||
for _ in 0..5 {
|
||||
match remove_dir_all_recursive(&file, delete) {
|
||||
Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => {}
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
// Try one last time.
|
||||
delete(&file)
|
||||
ok => ok,
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_dir_all_recursive(f: &File, delete: fn(&File) -> io::Result<()>) -> io::Result<()> {
|
||||
fn remove_dir_all_iterative(f: &File, delete: fn(&File) -> io::Result<()>) -> io::Result<()> {
|
||||
let mut buffer = DirBuff::new();
|
||||
let mut restart = true;
|
||||
// Fill the buffer and iterate the entries.
|
||||
while f.fill_dir_buff(&mut buffer, restart)? {
|
||||
for name in buffer.iter() {
|
||||
// Open the file without following symlinks and try deleting it.
|
||||
// We try opening will all needed permissions and if that is denied
|
||||
// fallback to opening without `FILE_LIST_DIRECTORY` permission.
|
||||
// Note `SYNCHRONIZE` permission is needed for synchronous access.
|
||||
let mut result =
|
||||
open_link_no_reparse(&f, name, c::SYNCHRONIZE | c::DELETE | c::FILE_LIST_DIRECTORY);
|
||||
if matches!(&result, Err(e) if e.kind() == io::ErrorKind::PermissionDenied) {
|
||||
result = open_link_no_reparse(&f, name, c::SYNCHRONIZE | c::DELETE);
|
||||
}
|
||||
match result {
|
||||
Ok(file) => match delete(&file) {
|
||||
Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => {
|
||||
// Iterate the directory's files.
|
||||
// Ignore `DirectoryNotEmpty` errors here. They will be
|
||||
// caught when `remove_dir_all` tries to delete the top
|
||||
// level directory. It can then decide if to retry or not.
|
||||
match remove_dir_all_recursive(&file, delete) {
|
||||
Err(e) if e.kind() == io::ErrorKind::DirectoryNotEmpty => {}
|
||||
result => result?,
|
||||
}
|
||||
let mut dirlist = vec![f.duplicate()?];
|
||||
|
||||
// FIXME: This is a hack so we can push to the dirlist vec after borrowing from it.
|
||||
fn copy_handle(f: &File) -> mem::ManuallyDrop<File> {
|
||||
unsafe { mem::ManuallyDrop::new(File::from_raw_handle(f.as_raw_handle())) }
|
||||
}
|
||||
|
||||
while let Some(dir) = dirlist.last() {
|
||||
let dir = copy_handle(dir);
|
||||
|
||||
// Fill the buffer and iterate the entries.
|
||||
let more_data = dir.fill_dir_buff(&mut buffer, false)?;
|
||||
for (name, is_directory) in buffer.iter() {
|
||||
if is_directory {
|
||||
let child_dir = open_link_no_reparse(
|
||||
&dir,
|
||||
name,
|
||||
c::SYNCHRONIZE | c::DELETE | c::FILE_LIST_DIRECTORY,
|
||||
)?;
|
||||
dirlist.push(child_dir);
|
||||
} else {
|
||||
const MAX_RETRIES: u32 = 10;
|
||||
for i in 1..=MAX_RETRIES {
|
||||
let result = open_link_no_reparse(&dir, name, c::SYNCHRONIZE | c::DELETE);
|
||||
match result {
|
||||
Ok(f) => delete(&f)?,
|
||||
// Already deleted, so skip.
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => break,
|
||||
// Retry a few times if the file is locked or a delete is already in progress.
|
||||
Err(e)
|
||||
if i < MAX_RETRIES
|
||||
&& (e.raw_os_error() == Some(c::ERROR_DELETE_PENDING as _)
|
||||
|| e.raw_os_error()
|
||||
== Some(c::ERROR_SHARING_VIOLATION as _)) => {}
|
||||
// Otherwise return the error.
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
result => result?,
|
||||
},
|
||||
// Ignore error if a delete is already in progress or the file
|
||||
// has already been deleted. It also ignores sharing violations
|
||||
// (where a file is locked by another process) as these are
|
||||
// usually temporary.
|
||||
Err(e)
|
||||
if e.raw_os_error() == Some(c::ERROR_DELETE_PENDING as _)
|
||||
|| e.kind() == io::ErrorKind::NotFound
|
||||
|| e.raw_os_error() == Some(c::ERROR_SHARING_VIOLATION as _) => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there were no more files then delete the directory.
|
||||
if !more_data {
|
||||
if let Some(dir) = dirlist.pop() {
|
||||
delete(&dir)?;
|
||||
}
|
||||
}
|
||||
// Continue reading directory entries without restarting from the beginning,
|
||||
restart = false;
|
||||
}
|
||||
delete(&f)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn readlink(path: &Path) -> io::Result<PathBuf> {
|
||||
|
Loading…
Reference in New Issue
Block a user