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