tempfile/
spooled.rs

1use crate::file::tempfile;
2use crate::tempfile_in;
3use std::fs::File;
4use std::io::{self, Cursor, Read, Seek, SeekFrom, Write};
5use std::path::{Path, PathBuf};
6
7/// A wrapper for the two states of a [`SpooledTempFile`]. Either:
8///
9/// 1. An in-memory [`Cursor`] representing the state of the file.
10/// 2. A temporary [`File`].
11#[derive(Debug)]
12pub enum SpooledData {
13    InMemory(Cursor<Vec<u8>>),
14    OnDisk(File),
15}
16
17/// An object that behaves like a regular temporary file, but keeps data in
18/// memory until it reaches a configured size, at which point the data is
19/// written to a temporary file on disk, and further operations use the file
20/// on disk.
21#[derive(Debug)]
22pub struct SpooledTempFile {
23    max_size: usize,
24    dir: Option<PathBuf>,
25    inner: SpooledData,
26}
27
28/// Create a new [`SpooledTempFile`]. Also see [`spooled_tempfile_in`].
29///
30/// # Security
31///
32/// This variant is secure/reliable in the presence of a pathological temporary
33/// file cleaner.
34///
35/// # Backing Storage
36///
37/// By default, the underlying temporary file will be created in your operating system's temporary
38/// file directory which is _often_ an in-memory filesystem. You may want to consider using
39/// [`spooled_tempfile_in`] instead, passing a storage-backed filesystem (e.g., `/var/tmp` on
40/// Linux).
41///
42/// # Resource Leaking
43///
44/// The temporary file will be automatically removed by the OS when the last
45/// handle to it is closed. This doesn't rely on Rust destructors being run, so
46/// will (almost) never fail to clean up the temporary file.
47///
48/// # Examples
49///
50/// ```
51/// use tempfile::spooled_tempfile;
52/// use std::io::Write;
53///
54/// let mut file = spooled_tempfile(15);
55///
56/// writeln!(file, "short line")?;
57/// assert!(!file.is_rolled());
58///
59/// // as a result of this write call, the size of the data will exceed
60/// // `max_size` (15), so it will be written to a temporary file on disk,
61/// // and the in-memory buffer will be dropped
62/// writeln!(file, "marvin gardens")?;
63/// assert!(file.is_rolled());
64/// # Ok::<(), std::io::Error>(())
65/// ```
66#[inline]
67pub fn spooled_tempfile(max_size: usize) -> SpooledTempFile {
68    SpooledTempFile::new(max_size)
69}
70
71/// Construct a new [`SpooledTempFile`], backed by a file in the specified directory. Use this when,
72/// e.g., you need the temporary file to be backed by a specific filesystem (e.g., when your default
73/// temporary directory is in-memory). Also see [`spooled_tempfile`].
74///
75/// **NOTE:** The specified path isn't checked until the temporary file is "rolled over" into a real
76/// temporary file. If the specified directory isn't writable, writes to the temporary file will
77/// fail once the `max_size` is reached.
78#[inline]
79pub fn spooled_tempfile_in<P: AsRef<Path>>(max_size: usize, dir: P) -> SpooledTempFile {
80    SpooledTempFile::new_in(max_size, dir)
81}
82
83/// Write a cursor into a temporary file, returning the temporary file.
84fn cursor_to_tempfile(cursor: &Cursor<Vec<u8>>, p: &Option<PathBuf>) -> io::Result<File> {
85    let mut file = match p {
86        Some(p) => tempfile_in(p)?,
87        None => tempfile()?,
88    };
89    file.write_all(cursor.get_ref())?;
90    file.seek(SeekFrom::Start(cursor.position()))?;
91    Ok(file)
92}
93
94impl SpooledTempFile {
95    /// Construct a new [`SpooledTempFile`].
96    #[must_use]
97    pub fn new(max_size: usize) -> SpooledTempFile {
98        SpooledTempFile {
99            max_size,
100            dir: None,
101            inner: SpooledData::InMemory(Cursor::new(Vec::new())),
102        }
103    }
104
105    /// Construct a new [`SpooledTempFile`], backed by a file in the specified directory.
106    #[must_use]
107    pub fn new_in<P: AsRef<Path>>(max_size: usize, dir: P) -> SpooledTempFile {
108        SpooledTempFile {
109            max_size,
110            dir: Some(dir.as_ref().to_owned()),
111            inner: SpooledData::InMemory(Cursor::new(Vec::new())),
112        }
113    }
114
115    /// Returns true if the file has been rolled over to disk.
116    #[must_use]
117    pub fn is_rolled(&self) -> bool {
118        match self.inner {
119            SpooledData::InMemory(_) => false,
120            SpooledData::OnDisk(_) => true,
121        }
122    }
123
124    /// Rolls over to a file on disk, regardless of current size. Does nothing
125    /// if already rolled over.
126    pub fn roll(&mut self) -> io::Result<()> {
127        if let SpooledData::InMemory(cursor) = &mut self.inner {
128            self.inner = SpooledData::OnDisk(cursor_to_tempfile(cursor, &self.dir)?);
129        }
130        Ok(())
131    }
132
133    /// Truncate the file to the specified size.
134    pub fn set_len(&mut self, size: u64) -> Result<(), io::Error> {
135        if size > self.max_size as u64 {
136            self.roll()?; // does nothing if already rolled over
137        }
138        match &mut self.inner {
139            SpooledData::InMemory(cursor) => {
140                cursor.get_mut().resize(size as usize, 0);
141                Ok(())
142            }
143            SpooledData::OnDisk(file) => file.set_len(size),
144        }
145    }
146
147    /// Consumes and returns the inner `SpooledData` type.
148    #[must_use]
149    pub fn into_inner(self) -> SpooledData {
150        self.inner
151    }
152
153    /// Convert into a regular unnamed temporary file, writing it to disk if necessary.
154    pub fn into_file(self) -> io::Result<File> {
155        match self.inner {
156            SpooledData::InMemory(cursor) => cursor_to_tempfile(&cursor, &self.dir),
157            SpooledData::OnDisk(file) => Ok(file),
158        }
159    }
160}
161
162impl Read for SpooledTempFile {
163    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
164        match &mut self.inner {
165            SpooledData::InMemory(cursor) => cursor.read(buf),
166            SpooledData::OnDisk(file) => file.read(buf),
167        }
168    }
169
170    fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result<usize> {
171        match &mut self.inner {
172            SpooledData::InMemory(cursor) => cursor.read_vectored(bufs),
173            SpooledData::OnDisk(file) => file.read_vectored(bufs),
174        }
175    }
176
177    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
178        match &mut self.inner {
179            SpooledData::InMemory(cursor) => cursor.read_to_end(buf),
180            SpooledData::OnDisk(file) => file.read_to_end(buf),
181        }
182    }
183
184    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
185        match &mut self.inner {
186            SpooledData::InMemory(cursor) => cursor.read_to_string(buf),
187            SpooledData::OnDisk(file) => file.read_to_string(buf),
188        }
189    }
190
191    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
192        match &mut self.inner {
193            SpooledData::InMemory(cursor) => cursor.read_exact(buf),
194            SpooledData::OnDisk(file) => file.read_exact(buf),
195        }
196    }
197}
198
199impl Write for SpooledTempFile {
200    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
201        // roll over to file if necessary
202        if matches! {
203            &self.inner, SpooledData::InMemory(cursor)
204            if cursor.position().saturating_add(buf.len() as u64) > self.max_size as u64
205        } {
206            self.roll()?;
207        }
208
209        // write the bytes
210        match &mut self.inner {
211            SpooledData::InMemory(cursor) => cursor.write(buf),
212            SpooledData::OnDisk(file) => file.write(buf),
213        }
214    }
215
216    fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
217        if matches! {
218            &self.inner, SpooledData::InMemory(cursor)
219            // Borrowed from the rust standard library.
220            if bufs
221                .iter()
222                .fold(cursor.position(), |a, b| a.saturating_add(b.len() as u64))
223                > self.max_size as u64
224        } {
225            self.roll()?;
226        }
227        match &mut self.inner {
228            SpooledData::InMemory(cursor) => cursor.write_vectored(bufs),
229            SpooledData::OnDisk(file) => file.write_vectored(bufs),
230        }
231    }
232
233    #[inline]
234    fn flush(&mut self) -> io::Result<()> {
235        match &mut self.inner {
236            SpooledData::InMemory(cursor) => cursor.flush(),
237            SpooledData::OnDisk(file) => file.flush(),
238        }
239    }
240}
241
242impl Seek for SpooledTempFile {
243    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
244        match &mut self.inner {
245            SpooledData::InMemory(cursor) => cursor.seek(pos),
246            SpooledData::OnDisk(file) => file.seek(pos),
247        }
248    }
249}