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