Skip to main content

zlayer_types/
scratch.rs

1//! RAII scratch directory and file types anchored at the `ZLayer` data dir's
2//! `tmp/` subdirectory. Constructed via [`zlayer_paths::ZLayerDirs::scratch_dir`]
3//! and [`zlayer_paths::ZLayerDirs::scratch_file`]. These wrap `tempfile`'s
4//! handles so call sites can name their return type without pulling in
5//! `tempfile` directly.
6
7use std::path::{Path, PathBuf};
8
9/// RAII-cleaned scratch directory anchored under the `ZLayer` data dir's `tmp/`.
10/// Drop removes the directory and its contents. Call [`Self::into_path`] to
11/// keep the directory on disk past the guard's lifetime.
12#[derive(Debug)]
13pub struct Scratch {
14    inner: tempfile::TempDir,
15}
16
17impl Scratch {
18    /// Wrap an existing `tempfile::TempDir`. Prefer
19    /// [`zlayer_paths::ZLayerDirs::scratch_dir`] over calling this directly.
20    #[must_use]
21    pub fn from_tempdir(inner: tempfile::TempDir) -> Self {
22        Self { inner }
23    }
24
25    /// Path of the scratch directory on disk.
26    #[must_use]
27    pub fn path(&self) -> &Path {
28        self.inner.path()
29    }
30
31    /// Consume the guard and keep the directory on disk.
32    #[must_use]
33    pub fn into_path(self) -> PathBuf {
34        self.inner.keep()
35    }
36
37    /// Explicitly remove the directory now (vs. on Drop).
38    ///
39    /// # Errors
40    ///
41    /// Returns the underlying filesystem error if removal fails.
42    pub fn close(self) -> std::io::Result<()> {
43        self.inner.close()
44    }
45}
46
47impl AsRef<Path> for Scratch {
48    fn as_ref(&self) -> &Path {
49        self.inner.path()
50    }
51}
52
53/// RAII-cleaned scratch file anchored under the `ZLayer` data dir's `tmp/`.
54/// Drop removes the file.
55#[derive(Debug)]
56pub struct ScratchFile {
57    inner: tempfile::NamedTempFile,
58}
59
60impl ScratchFile {
61    /// Wrap an existing `tempfile::NamedTempFile`. Prefer
62    /// [`zlayer_paths::ZLayerDirs::scratch_file`] over calling this directly.
63    #[must_use]
64    pub fn from_named(inner: tempfile::NamedTempFile) -> Self {
65        Self { inner }
66    }
67
68    /// Path of the scratch file on disk.
69    #[must_use]
70    pub fn path(&self) -> &Path {
71        self.inner.path()
72    }
73
74    /// Read-only reference to the underlying file handle.
75    #[must_use]
76    pub fn as_file(&self) -> &std::fs::File {
77        self.inner.as_file()
78    }
79
80    /// Mutable reference to the underlying file handle.
81    pub fn as_file_mut(&mut self) -> &mut std::fs::File {
82        self.inner.as_file_mut()
83    }
84
85    /// Open a fresh handle to the same on-disk file.
86    ///
87    /// # Errors
88    ///
89    /// Returns the underlying filesystem error if opening fails.
90    pub fn reopen(&self) -> std::io::Result<std::fs::File> {
91        self.inner.reopen()
92    }
93
94    /// Persist the file to a permanent location, consuming the guard.
95    ///
96    /// # Errors
97    ///
98    /// Returns the underlying `tempfile::PersistError` if the rename fails.
99    pub fn persist(self, dest: impl AsRef<Path>) -> Result<std::fs::File, tempfile::PersistError> {
100        self.inner.persist(dest)
101    }
102
103    /// Consume the guard and return the underlying `TempPath` (still
104    /// RAII-cleaned, but without an open file handle).
105    #[must_use]
106    pub fn into_temp_path(self) -> tempfile::TempPath {
107        self.inner.into_temp_path()
108    }
109}
110
111impl AsRef<Path> for ScratchFile {
112    fn as_ref(&self) -> &Path {
113        self.inner.path()
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn scratch_path_under_parent_and_drop_removes_dir() {
123        let parent = tempfile::tempdir().expect("parent tempdir");
124        let inner = tempfile::Builder::new()
125            .prefix("scratch-test-")
126            .tempdir_in(parent.path())
127            .expect("inner tempdir");
128        let inner_path = inner.path().to_path_buf();
129        assert!(inner_path.starts_with(parent.path()));
130        let s = Scratch::from_tempdir(inner);
131        assert!(s.path().is_dir());
132        let kept = s.path().to_path_buf();
133        drop(s);
134        assert!(!kept.exists(), "Scratch Drop must remove the directory");
135    }
136
137    #[test]
138    fn scratch_file_path_under_parent_and_drop_removes_file() {
139        let parent = tempfile::tempdir().expect("parent tempdir");
140        let nf = tempfile::Builder::new()
141            .prefix("scratch-file-test-")
142            .tempfile_in(parent.path())
143            .expect("inner tempfile");
144        let nf_path = nf.path().to_path_buf();
145        assert!(nf_path.starts_with(parent.path()));
146        let f = ScratchFile::from_named(nf);
147        assert!(f.path().is_file());
148        let kept = f.path().to_path_buf();
149        drop(f);
150        assert!(!kept.exists(), "ScratchFile Drop must remove the file");
151    }
152
153    #[test]
154    fn scratch_into_path_keeps_dir() {
155        let parent = tempfile::tempdir().expect("parent tempdir");
156        let inner = tempfile::Builder::new()
157            .prefix("scratch-keep-")
158            .tempdir_in(parent.path())
159            .expect("inner tempdir");
160        let s = Scratch::from_tempdir(inner);
161        let kept = s.into_path();
162        assert!(kept.is_dir(), "into_path() must leave the dir on disk");
163        std::fs::remove_dir_all(&kept).ok();
164    }
165}