Repository: Stebalien/tempfile
Pull request #353 (2025-05-10 16:35:56)

feat: allow creating spooled temporary files in a specific directory


Patch:
diff --git a/src/lib.rs b/src/lib.rs
index 9fadcfe..701f402 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -215,7 +215,7 @@ pub use crate::dir::{tempdir, tempdir_in, TempDir};
 pub use crate::file::{
     tempfile, tempfile_in, NamedTempFile, PathPersistError, PersistError, TempPath,
 };
-pub use crate::spooled::{spooled_tempfile, SpooledData, SpooledTempFile};
+pub use crate::spooled::{spooled_tempfile, spooled_tempfile_in, SpooledData, SpooledTempFile};
 
 /// Create a new temporary file or directory with custom options.
 #[derive(Debug, Clone, Eq, PartialEq)]
diff --git a/src/spooled.rs b/src/spooled.rs
index e114827..5bd17b9 100644
--- a/src/spooled.rs
+++ b/src/spooled.rs
@@ -1,6 +1,8 @@
 use crate::file::tempfile;
+use crate::tempfile_in;
 use std::fs::File;
 use std::io::{self, Cursor, Read, Seek, SeekFrom, Write};
+use std::path::{Path, PathBuf};
 
 /// A wrapper for the two states of a `SpooledTempFile`.
 #[derive(Debug)]
@@ -16,6 +18,7 @@ pub enum SpooledData {
 #[derive(Debug)]
 pub struct SpooledTempFile {
     max_size: usize,
+    dir: Option<PathBuf>,
     inner: SpooledData,
 }
 
@@ -55,19 +58,46 @@ pub fn spooled_tempfile(max_size: usize) -> SpooledTempFile {
     SpooledTempFile::new(max_size)
 }
 
+/// Construct a new [`SpooledTempFile`], backed by a file in the specified directory. Use this when,
+/// e.g., you need the temporary file to be backed by a specific filesystem (e.g., when your default
+/// temporary directory is in-memory).
+///
+/// **NOTE:** The specified path isn't checked until the temporary file is "rolled over" into a real
+/// temporary file. If the specified directory isn't writable, writes to the temporary file will
+/// fail once the `max_size` is reached.
+#[inline]
+pub fn spooled_tempfile_in<P: AsRef<Path>>(max_size: usize, dir: P) -> SpooledTempFile {
+    SpooledTempFile::new_in(max_size, dir)
+}
+
 /// Write a cursor into a temporary file, returning the temporary file.
-fn cursor_to_tempfile(cursor: &Cursor<Vec<u8>>) -> io::Result<File> {
-    let mut file = tempfile()?;
+fn cursor_to_tempfile(cursor: &Cursor<Vec<u8>>, p: &Option<PathBuf>) -> io::Result<File> {
+    let mut file = match p {
+        Some(p) => tempfile_in(p)?,
+        None => tempfile()?,
+    };
     file.write_all(cursor.get_ref())?;
     file.seek(SeekFrom::Start(cursor.position()))?;
     Ok(file)
 }
 
 impl SpooledTempFile {
+    /// Construct a new [`SpooledTempFile`].
     #[must_use]
     pub fn new(max_size: usize) -> SpooledTempFile {
         SpooledTempFile {
             max_size,
+            dir: None,
+            inner: SpooledData::InMemory(Cursor::new(Vec::new())),
+        }
+    }
+
+    /// Construct a new [`SpooledTempFile`], backed by a file in the specified directory.
+    #[must_use]
+    pub fn new_in<P: AsRef<Path>>(max_size: usize, dir: P) -> SpooledTempFile {
+        SpooledTempFile {
+            max_size,
+            dir: Some(dir.as_ref().to_owned()),
             inner: SpooledData::InMemory(Cursor::new(Vec::new())),
         }
     }
@@ -85,7 +115,7 @@ impl SpooledTempFile {
     /// if already rolled over.
     pub fn roll(&mut self) -> io::Result<()> {
         if let SpooledData::InMemory(cursor) = &mut self.inner {
-            self.inner = SpooledData::OnDisk(cursor_to_tempfile(cursor)?);
+            self.inner = SpooledData::OnDisk(cursor_to_tempfile(cursor, &self.dir)?);
         }
         Ok(())
     }
@@ -113,7 +143,7 @@ impl SpooledTempFile {
     /// Convert into a regular unnamed temporary file, writing it to disk if necessary.
     pub fn into_file(self) -> io::Result<File> {
         match self.inner {
-            SpooledData::InMemory(cursor) => cursor_to_tempfile(&cursor),
+            SpooledData::InMemory(cursor) => cursor_to_tempfile(&cursor, &self.dir),
             SpooledData::OnDisk(file) => Ok(file),
         }
     }


Test patch:
diff --git a/tests/spooled.rs b/tests/spooled.rs
index 8871eca..21cfeb7 100644
--- a/tests/spooled.rs
+++ b/tests/spooled.rs
@@ -2,7 +2,7 @@
 
 use std::io::{Read, Seek, SeekFrom, Write};
 
-use tempfile::{spooled_tempfile, SpooledTempFile};
+use tempfile::{env, spooled_tempfile, spooled_tempfile_in, SpooledTempFile};
 
 /// For the wasi platforms, `std::env::temp_dir` will panic. For those targets, configure the /tmp
 /// directory instead as the base directory for temp files.
@@ -38,6 +38,23 @@ fn test_automatic_rollover() {
     assert!(t.is_rolled());
 }
 
+#[test]
+fn test_custom_dir() {
+    configure_wasi_temp_dir();
+
+    {
+        let mut t = spooled_tempfile_in(10, env::temp_dir());
+        t.roll()
+            .expect("failed to roll temp file in a specified directory");
+    }
+
+    {
+        let mut t = spooled_tempfile_in(10, "/does-not-exist/");
+        t.roll()
+            .expect_err("should fail to roll the temporary file into a nonexistent tempdir");
+    }
+}
+
 #[test]
 fn test_explicit_rollover() {
     configure_wasi_temp_dir();