nd_util/
drop_remove_path.rs

1use std::mem::ManuallyDrop;
2use std::ops::Deref;
3use std::path::Path;
4use std::path::PathBuf;
5
6/// Asyncronously remove a file at a path on drop.
7///
8/// Currently, this only supports files, NOT directories.
9#[derive(Debug)]
10pub struct DropRemovePath {
11    /// The path
12    path: PathBuf,
13
14    /// Whether dropping this should remove the file.
15    should_remove: bool,
16}
17
18impl DropRemovePath {
19    /// Make a new [`DropRemovePath`].
20    pub fn new<P>(path: P) -> Self
21    where
22        P: AsRef<Path>,
23    {
24        Self {
25            path: path.as_ref().into(),
26            should_remove: true,
27        }
28    }
29
30    /// Persist the file at this path.
31    pub fn persist(&mut self) {
32        self.should_remove = false;
33    }
34
35    /// Try to drop this file path, removing it if needed.
36    ///
37    /// # Return
38    /// Returns an error if the file could not be removed.
39    /// Returns Ok(true) if the file was removed.
40    /// Returns Ok(false) if the file was not removed.
41    pub async fn try_drop(self) -> Result<bool, (Self, std::io::Error)> {
42        let wrapper = ManuallyDrop::new(self);
43        let should_remove = wrapper.should_remove;
44
45        if should_remove {
46            tokio::fs::remove_file(&wrapper.path)
47                .await
48                .map_err(|e| (ManuallyDrop::into_inner(wrapper), e))?;
49        }
50
51        Ok(should_remove)
52    }
53}
54
55impl AsRef<Path> for DropRemovePath {
56    fn as_ref(&self) -> &Path {
57        self.path.as_ref()
58    }
59}
60
61impl Deref for DropRemovePath {
62    type Target = Path;
63
64    fn deref(&self) -> &Self::Target {
65        &self.path
66    }
67}
68
69impl Drop for DropRemovePath {
70    fn drop(&mut self) {
71        let should_remove = self.should_remove;
72        let path = std::mem::take(&mut self.path);
73
74        // Try to remove the path.
75        tokio::spawn(async move {
76            if should_remove {
77                if let Err(error) = tokio::fs::remove_file(path).await {
78                    let message = format!("failed to delete file: '{error}'");
79                    if std::thread::panicking() {
80                        eprintln!("{message}");
81                    } else {
82                        panic!("{message}");
83                    }
84                }
85            }
86        });
87    }
88}
89
90#[cfg(test)]
91mod test {
92    use super::*;
93    use tokio::io::AsyncWriteExt;
94
95    #[tokio::test]
96    async fn drop_remove_tokio_file_sanity_check() {
97        tokio::fs::create_dir_all("test_tmp")
98            .await
99            .expect("failed to create tmp dir");
100
101        let file_path: &Path = "test_tmp/test.txt".as_ref();
102        let file_data = b"testing 1 2 3";
103
104        {
105            let mut file = tokio::fs::File::create(&file_path)
106                .await
107                .expect("failed to create file");
108            let drop_remove_path = DropRemovePath::new(file_path);
109
110            file.write_all(file_data)
111                .await
112                .expect("failed to write data");
113
114            drop(file);
115            drop_remove_path
116                .try_drop()
117                .await
118                .expect("failed to close file");
119        }
120        let exists = file_path.exists();
121        assert!(!exists, "nonpersisted file exists");
122
123        {
124            let mut file = tokio::fs::File::create(&file_path)
125                .await
126                .expect("failed to create file");
127            let mut drop_remove_path = DropRemovePath::new(file_path);
128
129            file.write_all(file_data)
130                .await
131                .expect("failed to write data");
132
133            drop_remove_path.persist();
134
135            drop(file);
136            drop_remove_path
137                .try_drop()
138                .await
139                .expect("failed to close file");
140        }
141
142        let exists = file_path.exists();
143        assert!(exists, "persisted file does not exist");
144
145        // Failed cleanup does not matter
146        let _ = tokio::fs::remove_file(file_path).await.is_ok();
147    }
148}