1use std::io;
6use std::path::Path;
7
8use crate::CleanReporter;
9
10pub fn rm_rf(path: impl AsRef<Path>) -> io::Result<Removal> {
13 Remover::default().rm_rf(path, false)
14}
15
16#[derive(Default)]
18pub(crate) struct Remover {
19 reporter: Option<Box<dyn CleanReporter>>,
20}
21
22impl Remover {
23 pub(crate) fn new(reporter: Box<dyn CleanReporter>) -> Self {
25 Self {
26 reporter: Some(reporter),
27 }
28 }
29
30 pub(crate) fn rm_rf(
33 &self,
34 path: impl AsRef<Path>,
35 skip_locked_file: bool,
36 ) -> io::Result<Removal> {
37 let mut removal = Removal::default();
38 removal.rm_rf(path.as_ref(), self.reporter.as_deref(), skip_locked_file)?;
39 Ok(removal)
40 }
41}
42
43#[derive(Debug, Default)]
45pub struct Removal {
46 pub num_files: u64,
48 pub num_dirs: u64,
50 pub total_bytes: u64,
55}
56
57impl Removal {
58 fn rm_rf(
60 &mut self,
61 path: &Path,
62 reporter: Option<&dyn CleanReporter>,
63 skip_locked_file: bool,
64 ) -> io::Result<()> {
65 let metadata = match fs_err::symlink_metadata(path) {
66 Ok(metadata) => metadata,
67 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
68 Err(err) => return Err(err),
69 };
70
71 if !metadata.is_dir() {
72 self.num_files += 1;
73
74 self.total_bytes += metadata.len();
76 if metadata.is_symlink() {
77 #[cfg(windows)]
78 {
79 use std::os::windows::fs::FileTypeExt;
80
81 if metadata.file_type().is_symlink_dir() {
82 remove_dir(path)?;
83 } else {
84 remove_file(path)?;
85 }
86 }
87
88 #[cfg(not(windows))]
89 {
90 remove_file(path)?;
91 }
92 } else {
93 remove_file(path)?;
94 }
95
96 reporter.map(CleanReporter::on_clean);
97
98 return Ok(());
99 }
100
101 for entry in walkdir::WalkDir::new(path).contents_first(true) {
102 if let Err(ref err) = entry {
104 if err
105 .io_error()
106 .is_some_and(|err| err.kind() == io::ErrorKind::PermissionDenied)
107 {
108 if let Some(dir) = err.path() {
109 if set_readable(dir).unwrap_or(false) {
110 return self.rm_rf(path, reporter, skip_locked_file);
113 }
114 }
115 }
116 }
117
118 let entry = entry?;
119
120 if skip_locked_file
122 && entry.file_name() == ".lock"
123 && entry
124 .path()
125 .strip_prefix(path)
126 .is_ok_and(|suffix| suffix == Path::new(".lock"))
127 {
128 continue;
129 }
130
131 if entry.file_type().is_symlink() && {
132 #[cfg(windows)]
133 {
134 use std::os::windows::fs::FileTypeExt;
135 entry.file_type().is_symlink_dir()
136 }
137 #[cfg(not(windows))]
138 {
139 false
140 }
141 } {
142 self.num_files += 1;
143 remove_dir(entry.path())?;
144 } else if entry.file_type().is_dir() {
145 if skip_locked_file && entry.path() == path {
147 continue;
148 }
149
150 self.num_dirs += 1;
151
152 remove_dir_all(entry.path())?;
156 } else {
157 self.num_files += 1;
158
159 if let Ok(meta) = entry.metadata() {
161 self.total_bytes += meta.len();
162 }
163 remove_file(entry.path())?;
164 }
165
166 reporter.map(CleanReporter::on_clean);
167 }
168
169 reporter.map(CleanReporter::on_complete);
170
171 Ok(())
172 }
173}
174
175impl std::ops::AddAssign for Removal {
176 fn add_assign(&mut self, other: Self) {
177 self.num_files += other.num_files;
178 self.num_dirs += other.num_dirs;
179 self.total_bytes += other.total_bytes;
180 }
181}
182
183#[cfg_attr(windows, allow(unused_variables, clippy::unnecessary_wraps))]
185fn set_readable(path: &Path) -> io::Result<bool> {
186 #[cfg(unix)]
187 {
188 use std::os::unix::fs::PermissionsExt;
189 let mut perms = fs_err::metadata(path)?.permissions();
190 if perms.mode() & 0o500 == 0 {
191 perms.set_mode(perms.mode() | 0o500);
192 fs_err::set_permissions(path, perms)?;
193 return Ok(true);
194 }
195 }
196 Ok(false)
197}
198
199fn set_not_readonly(path: &Path) -> io::Result<bool> {
201 let mut perms = fs_err::metadata(path)?.permissions();
202 if !perms.readonly() {
203 return Ok(false);
204 }
205
206 #[allow(clippy::permissions_set_readonly_false)]
208 perms.set_readonly(false);
209
210 fs_err::set_permissions(path, perms)?;
211
212 Ok(true)
213}
214
215fn remove_file(path: &Path) -> io::Result<()> {
218 match fs_err::remove_file(path) {
219 Ok(()) => Ok(()),
220 Err(err)
221 if err.kind() == io::ErrorKind::PermissionDenied
222 && set_not_readonly(path).unwrap_or(false) =>
223 {
224 fs_err::remove_file(path)
225 }
226 Err(err) => Err(err),
227 }
228}
229
230fn remove_dir(path: &Path) -> io::Result<()> {
233 match fs_err::remove_dir(path) {
234 Ok(()) => Ok(()),
235 Err(err)
236 if err.kind() == io::ErrorKind::PermissionDenied
237 && set_readable(path).unwrap_or(false) =>
238 {
239 fs_err::remove_dir(path)
240 }
241 Err(err) => Err(err),
242 }
243}
244
245fn remove_dir_all(path: &Path) -> io::Result<()> {
248 match fs_err::remove_dir_all(path) {
249 Ok(()) => Ok(()),
250 Err(err)
251 if err.kind() == io::ErrorKind::PermissionDenied
252 && set_readable(path).unwrap_or(false) =>
253 {
254 fs_err::remove_dir_all(path)
255 }
256 Err(err) => Err(err),
257 }
258}