Skip to main content

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}