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