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};
67/// 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}
1617/// 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}
2728/// 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}
7071/// 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}
8283/// 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> {
85let mut file = match p {
86Some(p) => tempfile_in(p)?,
87None => tempfile()?,
88 };
89 file.write_all(cursor.get_ref())?;
90 file.seek(SeekFrom::Start(cursor.position()))?;
91Ok(file)
92}
9394impl SpooledTempFile {
95/// Construct a new [`SpooledTempFile`].
96#[must_use]
97pub fn new(max_size: usize) -> SpooledTempFile {
98 SpooledTempFile {
99 max_size,
100 dir: None,
101 inner: SpooledData::InMemory(Cursor::new(Vec::new())),
102 }
103 }
104105/// Construct a new [`SpooledTempFile`], backed by a file in the specified directory.
106#[must_use]
107pub 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 }
114115/// Returns true if the file has been rolled over to disk.
116#[must_use]
117pub fn is_rolled(&self) -> bool {
118match self.inner {
119 SpooledData::InMemory(_) => false,
120 SpooledData::OnDisk(_) => true,
121 }
122 }
123124/// Rolls over to a file on disk, regardless of current size. Does nothing
125 /// if already rolled over.
126pub fn roll(&mut self) -> io::Result<()> {
127if let SpooledData::InMemory(cursor) = &mut self.inner {
128self.inner = SpooledData::OnDisk(cursor_to_tempfile(cursor, &self.dir)?);
129 }
130Ok(())
131 }
132133/// Truncate the file to the specified size.
134pub fn set_len(&mut self, size: u64) -> Result<(), io::Error> {
135if size > self.max_size as u64 {
136self.roll()?; // does nothing if already rolled over
137}
138match &mut self.inner {
139 SpooledData::InMemory(cursor) => {
140 cursor.get_mut().resize(size as usize, 0);
141Ok(())
142 }
143 SpooledData::OnDisk(file) => file.set_len(size),
144 }
145 }
146147/// Consumes and returns the inner `SpooledData` type.
148#[must_use]
149pub fn into_inner(self) -> SpooledData {
150self.inner
151 }
152153/// Convert into a regular unnamed temporary file, writing it to disk if necessary.
154pub fn into_file(self) -> io::Result<File> {
155match self.inner {
156 SpooledData::InMemory(cursor) => cursor_to_tempfile(&cursor, &self.dir),
157 SpooledData::OnDisk(file) => Ok(file),
158 }
159 }
160}
161162impl Read for SpooledTempFile {
163fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
164match &mut self.inner {
165 SpooledData::InMemory(cursor) => cursor.read(buf),
166 SpooledData::OnDisk(file) => file.read(buf),
167 }
168 }
169170fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result<usize> {
171match &mut self.inner {
172 SpooledData::InMemory(cursor) => cursor.read_vectored(bufs),
173 SpooledData::OnDisk(file) => file.read_vectored(bufs),
174 }
175 }
176177fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
178match &mut self.inner {
179 SpooledData::InMemory(cursor) => cursor.read_to_end(buf),
180 SpooledData::OnDisk(file) => file.read_to_end(buf),
181 }
182 }
183184fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
185match &mut self.inner {
186 SpooledData::InMemory(cursor) => cursor.read_to_string(buf),
187 SpooledData::OnDisk(file) => file.read_to_string(buf),
188 }
189 }
190191fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
192match &mut self.inner {
193 SpooledData::InMemory(cursor) => cursor.read_exact(buf),
194 SpooledData::OnDisk(file) => file.read_exact(buf),
195 }
196 }
197}
198199impl Write for SpooledTempFile {
200fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
201// roll over to file if necessary
202if matches! {
203&self.inner, SpooledData::InMemory(cursor)
204if cursor.position().saturating_add(buf.len() as u64) > self.max_size as u64
205 } {
206self.roll()?;
207 }
208209// write the bytes
210match &mut self.inner {
211 SpooledData::InMemory(cursor) => cursor.write(buf),
212 SpooledData::OnDisk(file) => file.write(buf),
213 }
214 }
215216fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
217if matches! {
218&self.inner, SpooledData::InMemory(cursor)
219// Borrowed from the rust standard library.
220if bufs
221 .iter()
222 .fold(cursor.position(), |a, b| a.saturating_add(b.len() as u64))
223 > self.max_size as u64
224 } {
225self.roll()?;
226 }
227match &mut self.inner {
228 SpooledData::InMemory(cursor) => cursor.write_vectored(bufs),
229 SpooledData::OnDisk(file) => file.write_vectored(bufs),
230 }
231 }
232233#[inline]
234fn flush(&mut self) -> io::Result<()> {
235match &mut self.inner {
236 SpooledData::InMemory(cursor) => cursor.flush(),
237 SpooledData::OnDisk(file) => file.flush(),
238 }
239 }
240}
241242impl Seek for SpooledTempFile {
243fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
244match &mut self.inner {
245 SpooledData::InMemory(cursor) => cursor.seek(pos),
246 SpooledData::OnDisk(file) => file.seek(pos),
247 }
248 }
249}