dryice/temp.rs
1//! Owned temporary files for filesystem-backed `dryice` workflows.
2//!
3//! The core reader and writer APIs stay generic over [`std::io::Read`] and
4//! [`std::io::Write`]. This module adds a small ownership layer for workflows
5//! where `dryice` itself should create the backing file and clean it up when it
6//! is no longer needed.
7//!
8//! Cleanup is best-effort on drop: failures are logged with `log::warn!` and
9//! never panic. Call [`TempDryIceFile::cleanup`] when cleanup errors need to be
10//! handled explicitly.
11
12use std::{
13 fs::{self, File, OpenOptions},
14 io,
15 path::{Path, PathBuf},
16 sync::atomic::{AtomicU64, Ordering},
17 time::{SystemTime, UNIX_EPOCH},
18};
19
20use crate::DryIceError;
21
22static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
23
24/// An owned temporary `dryice` file that evaporates by default.
25///
26/// `TempDryIceFile` owns a filesystem path created by `dryice`. The file is
27/// removed when [`cleanup`](Self::cleanup) is called, or on drop as a
28/// best-effort fallback. Use [`persist`](Self::persist) to move the file into a
29/// caller-owned location and disable automatic cleanup.
30pub struct TempDryIceFile {
31 path: PathBuf,
32 cleanup_on_drop: bool,
33}
34
35impl TempDryIceFile {
36 /// Create a temporary `dryice` file in the system temporary directory.
37 ///
38 /// # Errors
39 ///
40 /// Returns an error if a temporary file cannot be created.
41 pub fn new() -> Result<Self, DryIceError> {
42 Self::new_in(std::env::temp_dir())
43 }
44
45 /// Create a temporary `dryice` file in `directory`.
46 ///
47 /// The directory must already exist. The file is created with exclusive
48 /// creation semantics to avoid reusing an existing path.
49 ///
50 /// # Errors
51 ///
52 /// Returns an error if the directory does not exist, if permissions prevent
53 /// file creation, or if a unique temporary path cannot be created after
54 /// repeated attempts.
55 pub fn new_in<P: AsRef<Path>>(directory: P) -> Result<Self, DryIceError> {
56 let directory = directory.as_ref();
57 let path = create_unique_temp_file(directory)?;
58 Ok(Self {
59 path,
60 cleanup_on_drop: true,
61 })
62 }
63
64 /// Return the owned temporary file path.
65 #[must_use]
66 pub fn path(&self) -> &Path {
67 &self.path
68 }
69
70 /// Open the owned temporary file for reading and writing.
71 ///
72 /// The returned [`File`] is a normal Rust file handle. It can be passed into
73 /// [`DryIceWriter`](crate::DryIceWriter), returned by `writer.finish()`,
74 /// rewound, and then passed into [`DryIceReader`](crate::DryIceReader):
75 ///
76 /// ```
77 /// use std::io::{Seek, SeekFrom};
78 ///
79 /// use dryice::{DryIceWriter, SeqRecord, TempDryIceFile};
80 ///
81 /// # fn example() -> Result<(), dryice::DryIceError> {
82 /// let temp = TempDryIceFile::new()?;
83 /// let file = temp.open()?;
84 /// let mut writer = DryIceWriter::builder().inner(file).build();
85 /// let record = SeqRecord::new(b"r1".to_vec(), b"ACGT".to_vec(), b"!!!!".to_vec())?;
86 /// writer.write_record(&record)?;
87 /// let mut file = writer.finish()?;
88 /// file.seek(SeekFrom::Start(0))?;
89 /// temp.cleanup()?;
90 /// # Ok(())
91 /// # }
92 /// ```
93 ///
94 /// # Errors
95 ///
96 /// Returns an error if the temporary file cannot be opened.
97 pub fn open(&self) -> Result<File, DryIceError> {
98 Ok(OpenOptions::new().read(true).write(true).open(&self.path)?)
99 }
100
101 /// Remove the temporary file now.
102 ///
103 /// Missing files are treated as already cleaned up. If the file has already
104 /// been persisted, cleanup is a no-op because `dryice` no longer owns the
105 /// file lifecycle.
106 ///
107 /// # Errors
108 ///
109 /// Returns an error if removing the temporary file fails for reasons other
110 /// than the file already being absent.
111 pub fn cleanup(mut self) -> Result<(), DryIceError> {
112 if !self.cleanup_on_drop {
113 return Ok(());
114 }
115
116 remove_temp_file(&self.path)?;
117 self.cleanup_on_drop = false;
118 Ok(())
119 }
120
121 /// Move the temporary file into a caller-owned path.
122 ///
123 /// After a successful persist, `dryice` no longer owns the file lifecycle
124 /// and will not remove the destination on drop. The destination must not
125 /// already exist.
126 ///
127 /// # Errors
128 ///
129 /// Returns an error if `path` already exists or if the temporary file cannot
130 /// be renamed to `path`.
131 pub fn persist<P: AsRef<Path>>(&mut self, path: P) -> Result<PathBuf, DryIceError> {
132 let destination = path.as_ref().to_path_buf();
133 if destination.exists() {
134 return Err(DryIceError::Io(io::Error::new(
135 io::ErrorKind::AlreadyExists,
136 "persist destination already exists",
137 )));
138 }
139
140 fs::rename(&self.path, &destination)?;
141 self.cleanup_on_drop = false;
142 self.path.clone_from(&destination);
143 Ok(destination)
144 }
145}
146
147impl Drop for TempDryIceFile {
148 fn drop(&mut self) {
149 if !self.cleanup_on_drop {
150 return;
151 }
152
153 if let Err(error) = remove_temp_file(&self.path) {
154 log::warn!(
155 "failed to clean up temporary dryice file `{}`: {error}",
156 self.path.display()
157 );
158 }
159 }
160}
161
162fn create_unique_temp_file(directory: &Path) -> Result<PathBuf, DryIceError> {
163 let pid = std::process::id();
164 let nanos = SystemTime::now()
165 .duration_since(UNIX_EPOCH)
166 .unwrap_or_default()
167 .as_nanos();
168
169 for _ in 0..100 {
170 let counter = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
171 let candidate = directory.join(format!("dryice-{pid}-{nanos}-{counter}.dryice"));
172
173 match OpenOptions::new()
174 .write(true)
175 .create_new(true)
176 .open(&candidate)
177 {
178 Ok(file) => {
179 drop(file);
180 return Ok(candidate);
181 },
182 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {},
183 Err(error) => return Err(DryIceError::Io(error)),
184 }
185 }
186
187 Err(DryIceError::Io(io::Error::new(
188 io::ErrorKind::AlreadyExists,
189 "could not create a unique temporary dryice file",
190 )))
191}
192
193fn remove_temp_file(path: &Path) -> Result<(), DryIceError> {
194 match fs::remove_file(path) {
195 Ok(()) => Ok(()),
196 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
197 Err(error) => Err(DryIceError::Io(error)),
198 }
199}