winbrew_core\fs/
cleanup.rs1use super::FsError;
6use std::fs;
7use std::io::ErrorKind;
8use std::path::{Path, PathBuf};
9use std::process;
10use std::sync::atomic::{AtomicUsize, Ordering};
11
12#[cfg(windows)]
13use winbrew_windows::fs::inspect_path as winfs_inspect_path;
14
15static DEFERRED_DELETE_SUFFIX: AtomicUsize = AtomicUsize::new(0);
16
17type BoxedResult<T> = std::result::Result<T, Box<FsError>>;
18
19#[derive(Debug, Clone, Copy)]
20pub(super) struct CleanupPathInfo {
21 pub(super) is_directory: bool,
22 pub(super) is_reparse_point: bool,
23}
24
25pub(super) fn inspect_path(path: &Path) -> std::io::Result<CleanupPathInfo> {
26 #[cfg(windows)]
27 {
28 let info = winfs_inspect_path(path)?;
29 Ok(CleanupPathInfo {
30 is_directory: info.is_directory,
31 is_reparse_point: info.is_reparse_point,
32 })
33 }
34
35 #[cfg(not(windows))]
36 {
37 let metadata = fs::symlink_metadata(path)?;
38 Ok(CleanupPathInfo {
39 is_directory: metadata.is_dir(),
40 is_reparse_point: false,
41 })
42 }
43}
44
45pub fn cleanup_path(path: &Path) -> BoxedResult<()> {
51 let info = match inspect_path(path) {
52 Ok(metadata) => metadata,
53 Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
54 Err(err) => return Err(Box::new(FsError::inspect(path, err))),
55 };
56
57 let removal_result = if info.is_reparse_point {
58 fs::remove_dir(path).or_else(|original_err| fs::remove_file(path).map_err(|_| original_err))
59 } else if info.is_directory {
60 fs::remove_dir_all(path)
61 } else {
62 fs::remove_file(path)
63 };
64
65 match removal_result {
66 Ok(()) => Ok(()),
67 Err(err) => {
68 if let Some(deferred_path) = deferred_delete_path(path) {
69 if fs::rename(path, &deferred_path).is_ok() {
70 return Ok(());
71 }
72
73 let _ = cleanup_path(&deferred_path);
74
75 if fs::rename(path, &deferred_path).is_ok() {
76 return Ok(());
77 }
78
79 return Err(Box::new(FsError::remove_and_defer(
80 path,
81 &deferred_path,
82 err,
83 )));
84 }
85
86 Err(Box::new(FsError::remove(path, err)))
87 }
88 }
89}
90
91fn deferred_delete_path(path: &Path) -> Option<PathBuf> {
92 let file_name = path.file_name()?.to_string_lossy();
93 let suffix = DEFERRED_DELETE_SUFFIX.fetch_add(1, Ordering::Relaxed);
94
95 Some(path.with_file_name(format!("{file_name}.deleted.{}.{}", process::id(), suffix)))
96}
97
98#[cfg(test)]
99mod tests {
100 use super::cleanup_path;
101 use std::fs;
102 use tempfile::tempdir;
103
104 #[test]
105 fn cleanup_path_is_noop_when_path_missing() {
106 let temp_dir = tempdir().expect("temp dir");
107 let missing = temp_dir.path().join("missing");
108
109 assert!(cleanup_path(&missing).is_ok());
110 assert!(!missing.exists());
111 }
112
113 #[test]
114 fn cleanup_path_removes_directory() {
115 let temp_dir = tempdir().expect("temp dir");
116 let dir = temp_dir.path().join("test_dir");
117
118 fs::create_dir(&dir).expect("create dir");
119
120 assert!(cleanup_path(&dir).is_ok());
121 assert!(!dir.exists());
122 }
123}