1use std::io;
6use std::path::Path;
7
8use tracing::debug;
9
10use crate::CleanReporter;
11
12#[derive(Debug, Clone, Copy, Default)]
14pub enum RemovalMode {
15 #[default]
17 Logical,
18 Physical,
20}
21
22#[derive(Default)]
24pub(crate) struct Remover {
25 reporter: Option<Box<dyn CleanReporter>>,
26 removal_mode: RemovalMode,
27}
28
29impl Remover {
30 pub(crate) fn new(reporter: Box<dyn CleanReporter>) -> Self {
32 Self {
33 reporter: Some(reporter),
34 ..Self::default()
35 }
36 }
37
38 pub(crate) fn with_removal_mode(mut self, removal_mode: RemovalMode) -> Self {
40 self.removal_mode = removal_mode;
41 self
42 }
43
44 pub(crate) fn rm_rf(
47 &self,
48 path: impl AsRef<Path>,
49 skip_locked_file: bool,
50 ) -> io::Result<Removal> {
51 let mut removal = Removal::new(self.removal_mode);
52 removal.rm_rf(path.as_ref(), self.reporter.as_deref(), skip_locked_file)?;
53 Ok(removal)
54 }
55}
56
57#[derive(Debug, Default)]
59pub struct Removal {
60 pub num_files: u64,
62 pub num_dirs: u64,
64 pub logical_bytes: u64,
69 pub physical_bytes: Option<u64>,
71 pub physical_bytes_incomplete: bool,
73}
74
75impl Removal {
76 pub(crate) fn new(removal_mode: RemovalMode) -> Self {
78 Self {
79 physical_bytes: match removal_mode {
80 RemovalMode::Logical => None,
81 RemovalMode::Physical => Some(0),
82 },
83 ..Self::default()
84 }
85 }
86
87 fn add_file(&mut self, path: &Path, metadata: &std::fs::Metadata) {
89 self.logical_bytes += metadata.len();
90
91 if let Some(physical_bytes) = self.physical_bytes {
92 match uv_fs::physical_space(path, metadata) {
93 Ok(physical) => {
94 self.physical_bytes = Some(physical_bytes.saturating_add(physical));
95 }
96 Err(error) => {
97 debug!(
98 "Failed to measure physical space for {}: {error}",
99 path.display()
100 );
101 self.physical_bytes_incomplete = true;
102 }
103 }
104 }
105 }
106
107 fn rm_rf(
109 &mut self,
110 path: &Path,
111 reporter: Option<&dyn CleanReporter>,
112 skip_locked_file: bool,
113 ) -> io::Result<()> {
114 let path = uv_fs::verbatim_path(path);
115
116 let metadata = match fs_err::symlink_metadata(&path) {
117 Ok(metadata) => metadata,
118 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
119 Err(err) => return Err(err),
120 };
121
122 if !metadata.is_dir() {
123 self.num_files += 1;
124
125 self.add_file(&path, &metadata);
127 if metadata.is_symlink() {
128 cfg_select! {
129 windows => {
130 use std::os::windows::fs::FileTypeExt;
131
132 if metadata.file_type().is_symlink_dir() {
133 remove_dir(&path)?;
134 } else {
135 remove_file(&path)?;
136 }
137 },
138 _ => {
139 remove_file(&path)?;
140 },
141 }
142 } else {
143 remove_file(&path)?;
144 }
145
146 reporter.map(CleanReporter::on_clean);
147
148 return Ok(());
149 }
150
151 for entry in walkdir::WalkDir::new(&path).contents_first(true) {
152 if let Err(ref err) = entry {
154 if err
155 .io_error()
156 .is_some_and(|err| err.kind() == io::ErrorKind::PermissionDenied)
157 {
158 if let Some(dir) = err.path() {
159 if set_readable(dir).unwrap_or(false) {
160 return self.rm_rf(&path, reporter, skip_locked_file);
163 }
164 }
165 }
166 }
167
168 let entry = entry?;
169
170 if skip_locked_file
172 && entry.file_name() == ".lock"
173 && entry
174 .path()
175 .strip_prefix(&path)
176 .is_ok_and(|suffix| suffix == Path::new(".lock"))
177 {
178 continue;
179 }
180
181 if entry.file_type().is_symlink() && {
182 #[cfg(windows)]
183 {
184 use std::os::windows::fs::FileTypeExt;
185 entry.file_type().is_symlink_dir()
186 }
187 #[cfg(not(windows))]
188 {
189 false
190 }
191 } {
192 self.num_files += 1;
193 remove_dir(entry.path())?;
194 } else if entry.file_type().is_dir() {
195 if skip_locked_file && entry.path() == path.as_ref() {
197 continue;
198 }
199
200 self.num_dirs += 1;
201
202 remove_dir_all(entry.path())?;
206 } else {
207 self.num_files += 1;
208
209 if let Ok(metadata) = entry.metadata() {
211 self.add_file(entry.path(), &metadata);
212 } else if self.physical_bytes.is_some() {
213 self.physical_bytes_incomplete = true;
214 }
215 remove_file(entry.path())?;
216 }
217
218 reporter.map(CleanReporter::on_clean);
219 }
220
221 reporter.map(CleanReporter::on_complete);
222
223 Ok(())
224 }
225}
226
227impl std::ops::AddAssign for Removal {
228 fn add_assign(&mut self, other: Self) {
229 self.num_files += other.num_files;
230 self.num_dirs += other.num_dirs;
231 self.logical_bytes += other.logical_bytes;
232 self.physical_bytes = self
233 .physical_bytes
234 .zip(other.physical_bytes)
235 .map(|(left, right)| left.saturating_add(right));
236 self.physical_bytes_incomplete |= other.physical_bytes_incomplete;
237 }
238}
239
240#[cfg_attr(windows, allow(unused_variables, clippy::unnecessary_wraps))]
242fn set_readable(path: &Path) -> io::Result<bool> {
243 #[cfg(unix)]
244 {
245 use std::os::unix::fs::PermissionsExt;
246 let mut perms = fs_err::metadata(path)?.permissions();
247 if perms.mode() & 0o500 == 0 {
248 perms.set_mode(perms.mode() | 0o500);
249 fs_err::set_permissions(path, perms)?;
250 return Ok(true);
251 }
252 }
253 Ok(false)
254}
255
256fn set_not_readonly(path: &Path) -> io::Result<bool> {
258 let mut perms = fs_err::metadata(path)?.permissions();
259 if !perms.readonly() {
260 return Ok(false);
261 }
262
263 #[expect(clippy::permissions_set_readonly_false)]
265 perms.set_readonly(false);
266
267 fs_err::set_permissions(path, perms)?;
268
269 Ok(true)
270}
271
272fn remove_file(path: &Path) -> io::Result<()> {
275 match fs_err::remove_file(path) {
276 Ok(()) => Ok(()),
277 Err(err)
278 if err.kind() == io::ErrorKind::PermissionDenied
279 && set_not_readonly(path).unwrap_or(false) =>
280 {
281 fs_err::remove_file(path)
282 }
283 Err(err) => Err(err),
284 }
285}
286
287fn remove_dir(path: &Path) -> io::Result<()> {
290 match fs_err::remove_dir(path) {
291 Ok(()) => Ok(()),
292 Err(err)
293 if err.kind() == io::ErrorKind::PermissionDenied
294 && set_readable(path).unwrap_or(false) =>
295 {
296 fs_err::remove_dir(path)
297 }
298 Err(err) => Err(err),
299 }
300}
301
302fn remove_dir_all(path: &Path) -> io::Result<()> {
305 match fs_err::remove_dir_all(path) {
306 Ok(()) => Ok(()),
307 Err(err)
308 if err.kind() == io::ErrorKind::PermissionDenied
309 && set_readable(path).unwrap_or(false) =>
310 {
311 fs_err::remove_dir_all(path)
312 }
313 Err(err) => Err(err),
314 }
315}