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 path = uv_fs::verbatim_path(path);
66
67 let metadata = match fs_err::symlink_metadata(&path) {
68 Ok(metadata) => metadata,
69 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
70 Err(err) => return Err(err),
71 };
72
73 if !metadata.is_dir() {
74 self.num_files += 1;
75
76 self.total_bytes += metadata.len();
78 if metadata.is_symlink() {
79 cfg_select! {
80 windows => {
81 use std::os::windows::fs::FileTypeExt;
82
83 if metadata.file_type().is_symlink_dir() {
84 remove_dir(&path)?;
85 } else {
86 remove_file(&path)?;
87 }
88 },
89 _ => {
90 remove_file(&path)?;
91 },
92 }
93 } else {
94 remove_file(&path)?;
95 }
96
97 reporter.map(CleanReporter::on_clean);
98
99 return Ok(());
100 }
101
102 for entry in walkdir::WalkDir::new(&path).contents_first(true) {
103 if let Err(ref err) = entry {
105 if err
106 .io_error()
107 .is_some_and(|err| err.kind() == io::ErrorKind::PermissionDenied)
108 {
109 if let Some(dir) = err.path() {
110 if set_readable(dir).unwrap_or(false) {
111 return self.rm_rf(&path, reporter, skip_locked_file);
114 }
115 }
116 }
117 }
118
119 let entry = entry?;
120
121 if skip_locked_file
123 && entry.file_name() == ".lock"
124 && entry
125 .path()
126 .strip_prefix(&path)
127 .is_ok_and(|suffix| suffix == Path::new(".lock"))
128 {
129 continue;
130 }
131
132 if entry.file_type().is_symlink() && {
133 #[cfg(windows)]
134 {
135 use std::os::windows::fs::FileTypeExt;
136 entry.file_type().is_symlink_dir()
137 }
138 #[cfg(not(windows))]
139 {
140 false
141 }
142 } {
143 self.num_files += 1;
144 remove_dir(entry.path())?;
145 } else if entry.file_type().is_dir() {
146 if skip_locked_file && entry.path() == path.as_ref() {
148 continue;
149 }
150
151 self.num_dirs += 1;
152
153 remove_dir_all(entry.path())?;
157 } else {
158 self.num_files += 1;
159
160 if let Ok(meta) = entry.metadata() {
162 self.total_bytes += meta.len();
163 }
164 remove_file(entry.path())?;
165 }
166
167 reporter.map(CleanReporter::on_clean);
168 }
169
170 reporter.map(CleanReporter::on_complete);
171
172 Ok(())
173 }
174}
175
176impl std::ops::AddAssign for Removal {
177 fn add_assign(&mut self, other: Self) {
178 self.num_files += other.num_files;
179 self.num_dirs += other.num_dirs;
180 self.total_bytes += other.total_bytes;
181 }
182}
183
184#[cfg_attr(windows, allow(unused_variables, clippy::unnecessary_wraps))]
186fn set_readable(path: &Path) -> io::Result<bool> {
187 #[cfg(unix)]
188 {
189 use std::os::unix::fs::PermissionsExt;
190 let mut perms = fs_err::metadata(path)?.permissions();
191 if perms.mode() & 0o500 == 0 {
192 perms.set_mode(perms.mode() | 0o500);
193 fs_err::set_permissions(path, perms)?;
194 return Ok(true);
195 }
196 }
197 Ok(false)
198}
199
200fn set_not_readonly(path: &Path) -> io::Result<bool> {
202 let mut perms = fs_err::metadata(path)?.permissions();
203 if !perms.readonly() {
204 return Ok(false);
205 }
206
207 #[expect(clippy::permissions_set_readonly_false)]
209 perms.set_readonly(false);
210
211 fs_err::set_permissions(path, perms)?;
212
213 Ok(true)
214}
215
216fn remove_file(path: &Path) -> io::Result<()> {
219 match fs_err::remove_file(path) {
220 Ok(()) => Ok(()),
221 Err(err)
222 if err.kind() == io::ErrorKind::PermissionDenied
223 && set_not_readonly(path).unwrap_or(false) =>
224 {
225 fs_err::remove_file(path)
226 }
227 Err(err) => Err(err),
228 }
229}
230
231fn remove_dir(path: &Path) -> io::Result<()> {
234 match fs_err::remove_dir(path) {
235 Ok(()) => Ok(()),
236 Err(err)
237 if err.kind() == io::ErrorKind::PermissionDenied
238 && set_readable(path).unwrap_or(false) =>
239 {
240 fs_err::remove_dir(path)
241 }
242 Err(err) => Err(err),
243 }
244}
245
246fn remove_dir_all(path: &Path) -> io::Result<()> {
249 match fs_err::remove_dir_all(path) {
250 Ok(()) => Ok(()),
251 Err(err)
252 if err.kind() == io::ErrorKind::PermissionDenied
253 && set_readable(path).unwrap_or(false) =>
254 {
255 fs_err::remove_dir_all(path)
256 }
257 Err(err) => Err(err),
258 }
259}