1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
//!
use std::path::{Path, PathBuf};
/// A special iterator which communicates its operation through results where…
///
/// * `Some(Ok(removed_directory))` is yielded once or more success, followed by `None`
/// * `Some(Err(std::io::Error))` is yielded exactly once on failure.
pub struct Iter<'a> {
cursor: Option<&'a Path>,
boundary: &'a Path,
}
/// Construction
impl<'a> Iter<'a> {
/// Create a new instance that deletes `target` but will stop at `boundary`, without deleting the latter.
/// Returns an error if `boundary` doesn't contain `target`
///
/// **Note** that we don't canonicalize the path for performance reasons.
pub fn new(target: &'a Path, boundary: &'a Path) -> std::io::Result<Self> {
if !target.starts_with(boundary) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Removal target {target:?} must be contained in boundary {boundary:?}"),
));
}
let cursor = if target == boundary {
None
} else if target.exists() {
Some(target)
} else {
None
};
Ok(Iter { cursor, boundary })
}
}
impl<'a> Iterator for Iter<'a> {
type Item = std::io::Result<&'a Path>;
fn next(&mut self) -> Option<Self::Item> {
match self.cursor.take() {
Some(dir) => {
let next = match std::fs::remove_dir(dir) {
Ok(()) => Some(Ok(dir)),
Err(err) => match err.kind() {
std::io::ErrorKind::NotFound => Some(Ok(dir)),
_other_error_kind => return Some(Err(err)),
},
};
self.cursor = match dir.parent() {
Some(parent) => (parent != self.boundary).then_some(parent),
None => {
unreachable!("directory {:?} ran out of parents, this really shouldn't happen before hitting the boundary {:?}", dir, self.boundary)
}
};
next
}
None => None,
}
}
}
/// Delete all empty directories from `delete_dir` upward and until (not including) the `boundary_dir`.
///
/// Note that `boundary_dir` must contain `delete_dir` or an error is returned, otherwise `delete_dir` is returned on success.
pub fn empty_upward_until_boundary<'a>(delete_dir: &'a Path, boundary_dir: &'a Path) -> std::io::Result<&'a Path> {
for item in Iter::new(delete_dir, boundary_dir)? {
match item {
Ok(_dir) => continue,
Err(err) => return Err(err),
}
}
Ok(delete_dir)
}
/// Delete all empty directories reachable from `delete_dir` from empty leaves moving upward to and including `delete_dir`.
///
/// If any encountered directory contains a file the entire operation is aborted.
/// Please note that this is inherently racy and no attempts are made to counter that, which will allow creators to win
/// as long as they retry.
pub fn empty_depth_first(delete_dir: impl Into<PathBuf>) -> std::io::Result<()> {
let delete_dir = delete_dir.into();
if let Ok(()) = std::fs::remove_dir(&delete_dir) {
return Ok(());
}
let mut stack = vec![delete_dir];
let mut next_to_push = Vec::new();
while let Some(dir_to_delete) = stack.pop() {
let mut num_entries = 0;
for entry in std::fs::read_dir(&dir_to_delete)? {
num_entries += 1;
let entry = entry?;
if entry.file_type()?.is_dir() {
next_to_push.push(entry.path());
} else {
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Directory not empty"));
}
}
if num_entries == 0 {
std::fs::remove_dir(&dir_to_delete)?;
} else {
stack.push(dir_to_delete);
stack.append(&mut next_to_push);
}
}
Ok(())
}
|