Skip to main content

wowlab_fs/
temporary.rs

1//! Scoped temporary filesystem ownership.
2
3use crate::{
4    error::{Error, Operation, Result},
5    path::{Path, PathBuf},
6};
7
8/// A unique temporary file removed when dropped.
9#[derive(Debug)]
10pub struct File {
11    inner: tempfile::NamedTempFile,
12    path: PathBuf,
13}
14
15impl File {
16    /// Create a unique temporary file with diagnostic filename affixes.
17    ///
18    /// The file is removed when this guard is dropped.
19    /// Call [`Self::retain`] to preserve it as a regular open file.
20    ///
21    /// # Errors
22    ///
23    /// Returns a contextual error when the file cannot be created.
24    pub fn with_affixes(prefix: &str, suffix: &str) -> Result<Self> {
25        let inner = tempfile::Builder::new()
26            .prefix(prefix)
27            .suffix(suffix)
28            .tempfile()
29            .map_err(|source| {
30                Error::new(
31                    Operation::CreateFile,
32                    Path::new(std::env::temp_dir().as_os_str()),
33                    source,
34                )
35            })?;
36        let path = PathBuf::from(inner.path());
37
38        Ok(Self { inner, path })
39    }
40
41    /// Return the temporary file's unique path.
42    #[must_use]
43    pub fn path(&self) -> &Path {
44        &self.path
45    }
46
47    /// Retain the file after this temporary guard is consumed.
48    ///
49    /// The returned open file owns its custom path context.
50    /// Dropping that handle does not remove the filesystem entry.
51    /// Its caller is responsible for eventual deletion.
52    ///
53    /// # Errors
54    ///
55    /// Returns a contextual error when the platform cannot retain the file.
56    pub fn retain(self) -> Result<crate::file::File> {
57        let Self { inner, path } = self;
58        let (file, retained_path) = inner
59            .keep()
60            .map_err(|error| Error::new(Operation::RetainFile, &path, error.error))?;
61        let retained_path = PathBuf::from(retained_path);
62
63        debug_assert_eq!(
64            retained_path, path,
65            "retaining a temporary file must preserve its unique path"
66        );
67
68        Ok(crate::file::File::from_open_file(file, retained_path))
69    }
70}
71
72/// A unique temporary directory removed when dropped.
73#[derive(Debug)]
74pub struct Directory {
75    inner: tempfile::TempDir,
76    path: PathBuf,
77}
78
79impl Directory {
80    /// Create a unique temporary directory.
81    ///
82    /// # Errors
83    ///
84    /// Returns a contextual error when the directory cannot be created.
85    pub fn new() -> Result<Self> {
86        Self::with_prefix("wowlab-")
87    }
88
89    /// Create a unique temporary directory with a diagnostic prefix.
90    ///
91    /// # Errors
92    ///
93    /// Returns a contextual error when the directory cannot be created.
94    pub fn with_prefix(prefix: &str) -> Result<Self> {
95        let inner = tempfile::Builder::new()
96            .prefix(prefix)
97            .tempdir()
98            .map_err(|source| {
99                Error::new(
100                    Operation::CreateDirectory,
101                    Path::new(std::env::temp_dir().as_os_str()),
102                    source,
103                )
104            })?;
105        let path = PathBuf::from(inner.path());
106
107        Ok(Self { inner, path })
108    }
109
110    /// Return the owned temporary root.
111    #[must_use]
112    pub fn path(&self) -> &Path {
113        &self.path
114    }
115
116    /// Retain the directory after this guard is consumed.
117    ///
118    /// The caller becomes responsible for eventual recursive deletion.
119    #[must_use]
120    pub fn retain(self) -> PathBuf {
121        let path = PathBuf::from(self.inner.keep());
122
123        debug_assert_eq!(
124            path, self.path,
125            "retaining a temporary directory must preserve its unique path"
126        );
127
128        path
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use std::io::Write as _;
135
136    use googletest::prelude::*;
137
138    use super::{Directory, File};
139    use crate::{directory, file};
140
141    #[gtest]
142    fn temporary_file_is_removed_when_its_guard_is_dropped() -> Result<()> {
143        let path = {
144            let temporary = File::with_affixes("wowlab-fs-scoped-", ".log").or_fail()?;
145            let path = temporary.path().to_path_buf();
146
147            verify_that!(directory::inspect(&path).or_fail()?, some(anything()))?;
148
149            path
150        };
151
152        verify_that!(directory::inspect(&path).or_fail()?, none())
153    }
154
155    #[gtest]
156    fn retained_files_are_unique_and_remain_readable_after_handles_close() -> Result<()> {
157        let temporary_left = File::with_affixes("wowlab-fs-retained-", ".log").or_fail()?;
158        let temporary_right = File::with_affixes("wowlab-fs-retained-", ".log").or_fail()?;
159        let mut left = temporary_left.retain().or_fail()?;
160        let right = temporary_right.retain().or_fail()?;
161        let left_path = left.path().to_path_buf();
162        let right_path = right.path().to_path_buf();
163
164        left.write_all(b"trace").or_fail()?;
165        left.sync().or_fail()?;
166
167        verify_that!(left_path, not(eq(&right_path)))?;
168
169        drop(left);
170        drop(right);
171
172        verify_that!(file::read_bytes(&left_path).or_fail()?, eq(b"trace"))?;
173        verify_that!(directory::inspect(&right_path).or_fail()?, some(anything()))?;
174
175        directory::remove_file_if_exists(&left_path).or_fail()?;
176        directory::remove_file_if_exists(&right_path).or_fail()?;
177
178        Ok(())
179    }
180
181    #[gtest]
182    fn retained_directory_remains_until_its_owner_removes_it() -> Result<()> {
183        let retained = Directory::new().or_fail()?.retain();
184
185        verify_that!(directory::inspect(&retained).or_fail()?, some(anything()))?;
186
187        verify_true!(directory::remove_tree_if_exists(&retained).or_fail()?)
188    }
189}