Skip to main content

rskit_storage/
temp.rs

1//! Managed temporary files and directories with auto-cleanup.
2
3use std::path::{Path, PathBuf};
4
5use rskit_errors::{AppError, AppResult, ErrorCode};
6use rskit_fs::sync_io::file;
7
8/// Managed temporary file. Deleted when the inner handle is dropped.
9#[derive(Debug)]
10pub struct TempFile {
11    inner: tempfile::NamedTempFile,
12}
13
14impl TempFile {
15    /// Create a new temporary file in the system temp directory.
16    pub fn new() -> AppResult<Self> {
17        let inner = tempfile::NamedTempFile::new().map_err(|e| {
18            AppError::new(
19                ErrorCode::Internal,
20                format!("failed to create temp file: {e}"),
21            )
22        })?;
23        Ok(Self { inner })
24    }
25
26    /// Create a temporary file with the given extension.
27    pub fn with_extension(ext: &str) -> AppResult<Self> {
28        let inner = tempfile::Builder::new()
29            .suffix(&format!(".{ext}"))
30            .tempfile()
31            .map_err(|e| {
32                AppError::new(
33                    ErrorCode::Internal,
34                    format!("failed to create temp file with extension .{ext}: {e}"),
35                )
36            })?;
37        Ok(Self { inner })
38    }
39
40    /// Create a temporary file in the given directory.
41    pub fn in_dir(dir: &Path) -> AppResult<Self> {
42        let inner = tempfile::NamedTempFile::new_in(dir).map_err(|e| {
43            AppError::new(
44                ErrorCode::Internal,
45                format!("failed to create temp file in {}: {e}", dir.display()),
46            )
47        })?;
48        Ok(Self { inner })
49    }
50
51    /// Create a temporary file in the given directory with the given extension.
52    pub fn in_dir_with_extension(dir: &Path, ext: &str) -> AppResult<Self> {
53        let inner = tempfile::Builder::new()
54            .suffix(&format!(".{ext}"))
55            .tempfile_in(dir)
56            .map_err(|e| {
57                AppError::new(
58                    ErrorCode::Internal,
59                    format!(
60                        "failed to create temp file in {} with extension .{ext}: {e}",
61                        dir.display()
62                    ),
63                )
64            })?;
65        Ok(Self { inner })
66    }
67
68    /// The path to this temporary file.
69    pub fn path(&self) -> &Path {
70        self.inner.path()
71    }
72
73    /// Convert this temp file into a [`super::FileSource`].
74    pub fn into_source(self) -> super::FileSource {
75        super::FileSource::Temp(self)
76    }
77
78    /// Create an independent copy of this temporary file.
79    pub fn try_clone(&self) -> AppResult<Self> {
80        let new = TempFile::new()?;
81        file::copy(self.path(), new.path()).map_err(|error| error.context("clone temp file"))?;
82        Ok(new)
83    }
84
85    /// Persist this temporary file to the given target path. The file will no longer be auto-deleted.
86    pub fn persist(self, target: impl AsRef<Path>) -> AppResult<PathBuf> {
87        let target = target.as_ref().to_path_buf();
88        self.inner.persist(&target).map_err(|e| {
89            AppError::new(
90                ErrorCode::Internal,
91                format!("failed to persist temp file to {}: {e}", target.display()),
92            )
93        })?;
94        Ok(target)
95    }
96}
97
98/// Managed temporary directory. All contents are cleaned up on drop.
99pub struct TempDir {
100    inner: tempfile::TempDir,
101}
102
103impl TempDir {
104    /// Create a new temporary directory.
105    pub fn new() -> AppResult<Self> {
106        let inner = tempfile::TempDir::new().map_err(|e| {
107            AppError::new(
108                ErrorCode::Internal,
109                format!("failed to create temp dir: {e}"),
110            )
111        })?;
112        Ok(Self { inner })
113    }
114
115    /// The path to this temporary directory.
116    pub fn path(&self) -> &Path {
117        self.inner.path()
118    }
119
120    /// Create a named file inside this temp directory.
121    pub fn create_file(&self, name: &str) -> AppResult<TempFile> {
122        let inner = tempfile::Builder::new()
123            .prefix(name)
124            .tempfile_in(self.path())
125            .map_err(|e| {
126                AppError::new(
127                    ErrorCode::Internal,
128                    format!("failed to create file {name} in temp dir: {e}"),
129                )
130            })?;
131        Ok(TempFile { inner })
132    }
133
134    /// Create a file with the given extension inside this temp directory.
135    pub fn create_file_with_extension(&self, ext: &str) -> AppResult<TempFile> {
136        TempFile::in_dir_with_extension(self.path(), ext)
137    }
138}
139
140impl std::fmt::Debug for TempDir {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.debug_struct("TempDir")
143            .field("path", &self.inner.path())
144            .finish()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn temp_file_clone_source_and_persist_keep_expected_contents() {
154        let file = TempFile::with_extension("txt").unwrap();
155        std::fs::write(file.path(), b"hello").unwrap();
156
157        let clone = file.try_clone().unwrap();
158        assert_eq!(std::fs::read(clone.path()).unwrap(), b"hello");
159        assert!(matches!(clone.into_source(), crate::FileSource::Temp(_)));
160
161        let dir = tempfile::tempdir().unwrap();
162        let target = dir.path().join("persisted.txt");
163        let persisted = file.persist(&target).unwrap();
164        assert_eq!(persisted, target);
165        assert_eq!(std::fs::read(&target).unwrap(), b"hello");
166    }
167
168    #[test]
169    fn temp_dir_creates_files_and_debug_exposes_path() {
170        let dir = TempDir::new().unwrap();
171        let named = dir.create_file("named").unwrap();
172        let with_ext = dir.create_file_with_extension("bin").unwrap();
173        let in_dir = TempFile::in_dir(dir.path()).unwrap();
174        let in_dir_with_ext = TempFile::in_dir_with_extension(dir.path(), "txt").unwrap();
175
176        assert!(named.path().starts_with(dir.path()));
177        assert!(with_ext.path().starts_with(dir.path()));
178        assert!(in_dir.path().starts_with(dir.path()));
179        assert_eq!(
180            in_dir_with_ext
181                .path()
182                .extension()
183                .and_then(|ext| ext.to_str()),
184            Some("txt")
185        );
186        assert!(
187            format!("{dir:?}").contains(dir.path().file_name().unwrap().to_string_lossy().as_ref())
188        );
189    }
190
191    #[test]
192    fn temp_file_new_creates_existing_path() {
193        let file = TempFile::new().unwrap();
194        assert!(file.path().exists());
195    }
196
197    #[test]
198    fn temp_file_reports_invalid_directory_errors() {
199        let missing =
200            std::env::temp_dir().join(format!("rskit-storage-missing-{}", std::process::id()));
201        let error = TempFile::in_dir(&missing).unwrap_err();
202
203        assert_eq!(error.code(), ErrorCode::Internal);
204        assert!(error.to_string().contains("failed to create temp file"));
205    }
206}