1use std::path::{Path, PathBuf};
4
5use rskit_errors::{AppError, AppResult, ErrorCode};
6use rskit_fs::sync_io::file;
7
8#[derive(Debug)]
10pub struct TempFile {
11 inner: tempfile::NamedTempFile,
12}
13
14impl TempFile {
15 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 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 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 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 pub fn path(&self) -> &Path {
70 self.inner.path()
71 }
72
73 pub fn into_source(self) -> super::FileSource {
75 super::FileSource::Temp(self)
76 }
77
78 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 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
98pub struct TempDir {
100 inner: tempfile::TempDir,
101}
102
103impl TempDir {
104 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 pub fn path(&self) -> &Path {
117 self.inner.path()
118 }
119
120 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 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}