winbrew_core\fs/
cleanup.rs

1//! Path cleanup utilities with Windows reparse point awareness.
2//!
3//! Provides safe deletion with deferred-cleanup fallback for locked files.
4
5use 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
45/// Removes `path` if it exists.
46///
47/// If immediate deletion fails and the path has a file name, the item is moved
48/// aside to a deferred-delete path so cleanup can continue later. On Windows,
49/// directory reparse points are removed without recursively walking their target.
50pub 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}