From ce8b147ee2403f052a1011fb37bd7f78aacdedff Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Mon, 29 Jul 2024 15:56:37 +0000 Subject: [PATCH] feat: add the ability to override the default temporary directory (#286) This adds a new `env` module with `override_temp_dir` and `temp_dir` functions. - `temp_dir` will defer to `std::env::temp_dir` by default unless the temporary directory has been overridden. - `override_temp_dir` allows the user to override the default temporary directory ONCE. Once this value has been set, it cannot be changed Care should be taken to ensure that the chosen directory is actually writable. This API is designed for use by the application author when the application may run in an environment without a reliable global temporary directory (e.g., Android). fixes #285 --- CHANGELOG.md | 4 ++++ Cargo.toml | 2 ++ src/dir/mod.rs | 16 +++++++++------- src/env.rs | 43 ++++++++++++++++++++++++++++++++++++++++++ src/file/imp/unix.rs | 3 +-- src/file/mod.rs | 17 ++++++----------- src/lib.rs | 19 ++++++++++--------- tests/namedtempfile.rs | 9 ++++----- tests/tempdir.rs | 3 +-- 9 files changed, 80 insertions(+), 36 deletions(-) create mode 100644 src/env.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1955a7ed9..82f650079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.11.0 + +- Add the ability to override the default temporary directory. This API shouldn't be used in general, but there are some cases where it's unavoidable. + ## 3.10.1 - Handle potential integer overflows in 32-bit systems when seeking/truncating "spooled" temporary files past 4GiB (2³²). diff --git a/Cargo.toml b/Cargo.toml index 0bfa28e55..503f15296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ description = "A library for managing temporary files and directories." [dependencies] cfg-if = "1" fastrand = "2.0.1" +# Not available in stdlib until 1.70, but we support 1.63 to support Debian stable. +once_cell = { version = "1.19.0", default-features = false, features = ["std"] } [target.'cfg(any(unix, target_os = "wasi"))'.dependencies] rustix = { version = "0.38.31", features = ["fs"] } diff --git a/src/dir/mod.rs b/src/dir/mod.rs index ba1b0db16..ea9017d4c 100644 --- a/src/dir/mod.rs +++ b/src/dir/mod.rs @@ -17,6 +17,9 @@ use std::{fmt, io}; use crate::error::IoResultExt; use crate::Builder; +#[cfg(doc)] +use crate::env; + /// Create a new temporary directory. /// /// The `tempdir` function creates a directory in the file system @@ -39,7 +42,7 @@ use crate::Builder; /// use std::fs::File; /// use std::io::Write; /// -/// // Create a directory inside of `std::env::temp_dir()` +/// // Create a directory inside of `env::temp_dir()` /// let tmp_dir = tempdir()?; /// /// let file_path = tmp_dir.path().join("my-temporary-note.txt"); @@ -109,7 +112,7 @@ pub fn tempdir_in>(dir: P) -> io::Result { /// `TempDir` creates a new directory with a randomly generated name. /// /// The default constructor, [`TempDir::new()`], creates directories in -/// the location returned by [`std::env::temp_dir()`], but `TempDir` +/// the location returned by [`env::temp_dir()`], but `TempDir` /// can be configured to manage a temporary directory in any location /// by constructing with a [`Builder`]. /// @@ -144,7 +147,7 @@ pub fn tempdir_in>(dir: P) -> io::Result { /// use std::io::Write; /// use tempfile::TempDir; /// -/// // Create a directory inside of `std::env::temp_dir()` +/// // Create a directory inside of `env::temp_dir()` /// let tmp_dir = TempDir::new()?; /// # Ok::<(), std::io::Error>(()) /// ``` @@ -156,7 +159,7 @@ pub fn tempdir_in>(dir: P) -> io::Result { /// use std::io::Write; /// use tempfile::Builder; /// -/// // Create a directory inside of `std::env::temp_dir()`, +/// // Create a directory inside of `env::temp_dir()`, /// // whose name will begin with 'example'. /// let tmp_dir = Builder::new().prefix("example").tempdir()?; /// # Ok::<(), std::io::Error>(()) @@ -170,7 +173,6 @@ pub fn tempdir_in>(dir: P) -> io::Result { /// [`TempDir::new()`]: struct.TempDir.html#method.new /// [`TempDir::path()`]: struct.TempDir.html#method.path /// [`TempDir`]: struct.TempDir.html -/// [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html /// [`std::fs`]: http://doc.rust-lang.org/std/fs/index.html /// [`std::process::exit()`]: http://doc.rust-lang.org/std/process/fn.exit.html pub struct TempDir { @@ -196,7 +198,7 @@ impl TempDir { /// use std::io::Write; /// use tempfile::TempDir; /// - /// // Create a directory inside of `std::env::temp_dir()` + /// // Create a directory inside of `env::temp_dir()` /// let tmp_dir = TempDir::new()?; /// /// let file_path = tmp_dir.path().join("my-temporary-note.txt"); @@ -378,7 +380,7 @@ impl TempDir { /// use std::io::Write; /// use tempfile::TempDir; /// - /// // Create a directory inside of `std::env::temp_dir()`. + /// // Create a directory inside of `env::temp_dir()`. /// let tmp_dir = TempDir::new()?; /// let file_path = tmp_dir.path().join("my-temporary-note.txt"); /// let mut tmp_file = File::create(file_path)?; diff --git a/src/env.rs b/src/env.rs new file mode 100644 index 000000000..86d51992a --- /dev/null +++ b/src/env.rs @@ -0,0 +1,43 @@ +use std::env; +use std::path::{Path, PathBuf}; + +// Once rust 1.70 is wide-spread (Debian stable), we can use OnceLock from stdlib. +use once_cell::sync::OnceCell as OnceLock; + +static DEFAULT_TEMPDIR: OnceLock = OnceLock::new(); + +/// Override the default temporary directory (defaults to [`std::env::temp_dir`]). This function +/// changes the _global_ default temporary directory for the entire program and should not be called +/// except in exceptional cases where it's not configured correctly by the platform. +/// +/// Only the first call to this function will succeed. All further calls will fail with `Err(path)` +/// where `path` is previously set default temporary directory override. +/// +/// **NOTE:** This function does not check if the specified directory exists and/or is writable. +pub fn override_temp_dir(path: &Path) -> Result<(), PathBuf> { + let mut we_set = false; + let val = DEFAULT_TEMPDIR.get_or_init(|| { + we_set = true; + path.to_path_buf() + }); + if we_set { + Ok(()) + } else { + Err(val.to_owned()) + } +} + +/// Returns the default temporary directory, used for both temporary directories and files if no +/// directory is explicitly specified. +/// +/// This function simply delegates to [`std::env::temp_dir`] unless the default temporary directory +/// has been override by a call to [`override_temp_dir`]. +/// +/// **NOTE:** This function does check if the returned directory exists and/or is writable. +pub fn temp_dir() -> PathBuf { + DEFAULT_TEMPDIR + .get() + .map(|p| p.to_owned()) + // Don't cache this in case the user uses std::env::set to change the temporary directory. + .unwrap_or_else(env::temp_dir) +} diff --git a/src/file/imp/unix.rs b/src/file/imp/unix.rs index 5f2cb5dcb..2a3cd9ab1 100644 --- a/src/file/imp/unix.rs +++ b/src/file/imp/unix.rs @@ -1,4 +1,3 @@ -use std::env; use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io; @@ -40,7 +39,7 @@ fn create_unlinked(path: &Path) -> io::Result { // shadow this to decrease the lifetime. It can't live longer than `tmp`. let mut path = path; if !path.is_absolute() { - let cur_dir = env::current_dir()?; + let cur_dir = std::env::current_dir()?; tmp = cur_dir.join(path); path = &tmp; } diff --git a/src/file/mod.rs b/src/file/mod.rs index 2aad4cd9b..2780ae14e 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -1,4 +1,3 @@ -use std::env; use std::error; use std::ffi::OsStr; use std::fmt; @@ -14,6 +13,7 @@ use std::os::wasi::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, RawHandle}; use std::path::{Path, PathBuf}; +use crate::env; use crate::error::IoResultExt; use crate::Builder; @@ -21,7 +21,7 @@ mod imp; /// Create a new temporary file. /// -/// The file will be created in the location returned by [`std::env::temp_dir()`]. +/// The file will be created in the location returned by [`env::temp_dir()`]. /// /// # Security /// @@ -42,14 +42,12 @@ mod imp; /// use tempfile::tempfile; /// use std::io::Write; /// -/// // Create a file inside of `std::env::temp_dir()`. +/// // Create a file inside of `env::temp_dir()`. /// let mut file = tempfile()?; /// /// writeln!(file, "Brian was here. Briefly.")?; /// # Ok::<(), std::io::Error>(()) /// ``` -/// -/// [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html pub fn tempfile() -> io::Result { tempfile_in(env::temp_dir()) } @@ -59,7 +57,7 @@ pub fn tempfile() -> io::Result { /// # Security /// /// This variant is secure/reliable in the presence of a pathological temporary file cleaner. -/// If the temporary file isn't created in [`std::env::temp_dir()`] then temporary file cleaners aren't an issue. +/// If the temporary file isn't created in [`env::temp_dir()`] then temporary file cleaners aren't an issue. /// /// # Resource Leaking /// @@ -82,8 +80,6 @@ pub fn tempfile() -> io::Result { /// writeln!(file, "Brian was here. Briefly.")?; /// # Ok::<(), std::io::Error>(()) /// ``` -/// -/// [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html pub fn tempfile_in>(dir: P) -> io::Result { imp::create(dir.as_ref()) } @@ -363,7 +359,7 @@ impl AsRef for TempPath { /// A named temporary file. /// /// The default constructor, [`NamedTempFile::new()`], creates files in -/// the location returned by [`std::env::temp_dir()`], but `NamedTempFile` +/// the location returned by [`env::temp_dir()`], but `NamedTempFile` /// can be configured to manage a temporary file in any location /// by constructing with [`NamedTempFile::new_in()`]. /// @@ -440,7 +436,6 @@ impl AsRef for TempPath { /// [`tempfile()`]: fn.tempfile.html /// [`NamedTempFile::new()`]: #method.new /// [`NamedTempFile::new_in()`]: #method.new_in -/// [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html /// [`std::process::exit()`]: http://doc.rust-lang.org/std/process/fn.exit.html /// [`lazy_static`]: https://github.com/rust-lang-nursery/lazy-static.rs/issues/62 pub struct NamedTempFile { @@ -1017,7 +1012,7 @@ pub(crate) fn create_named( // Make the path absolute. Otherwise, changing directories could cause us to // delete the wrong file. if !path.is_absolute() { - path = env::current_dir()?.join(path) + path = std::env::current_dir()?.join(path) } imp::create_named(&path, open_options, permissions) .with_err_path(|| path.clone()) diff --git a/src/lib.rs b/src/lib.rs index 4e78037bd..057b82b31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,7 @@ //! use tempfile::tempdir; //! use std::process::Command; //! -//! // Create a directory inside of `std::env::temp_dir()`. +//! // Create a directory inside of `env::temp_dir()`. //! let temp_dir = tempdir()?; //! //! // Spawn the `touch` command inside the temporary directory and collect the exit status @@ -67,7 +67,7 @@ //! use tempfile::tempfile; //! use std::io::Write; //! -//! // Create a file inside of `std::env::temp_dir()`. +//! // Create a file inside of `env::temp_dir()`. //! let mut file = tempfile()?; //! //! writeln!(file, "Brian was here. Briefly.")?; @@ -82,7 +82,7 @@ //! //! let text = "Brian was here. Briefly."; //! -//! // Create a file inside of `std::env::temp_dir()`. +//! // Create a file inside of `env::temp_dir()`. //! let mut file1 = NamedTempFile::new()?; //! //! // Re-open it. @@ -105,7 +105,7 @@ //! use std::fs::File; //! use std::io::Write; //! -//! // Create a directory inside of `std::env::temp_dir()`. +//! // Create a directory inside of `env::temp_dir()`. //! let dir = tempdir()?; //! //! let file_path = dir.path().join("my-temporary-note.txt"); @@ -126,7 +126,6 @@ //! [`tempdir()`]: fn.tempdir.html //! [`TempDir`]: struct.TempDir.html //! [`NamedTempFile`]: struct.NamedTempFile.html -//! [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html //! [`lazy_static`]: https://github.com/rust-lang-nursery/lazy-static.rs/issues/62 #![doc( @@ -147,8 +146,8 @@ const NUM_RAND_CHARS: usize = 6; use std::ffi::OsStr; use std::fs::OpenOptions; +use std::io; use std::path::Path; -use std::{env, io}; mod dir; mod error; @@ -156,6 +155,8 @@ mod file; mod spooled; mod util; +pub mod env; + pub use crate::dir::{tempdir, tempdir_in, TempDir}; pub use crate::file::{ tempfile, tempfile_in, NamedTempFile, PathPersistError, PersistError, TempPath, @@ -469,7 +470,7 @@ impl<'a, 'b> Builder<'a, 'b> { ) } - /// Attempts to make a temporary directory inside of `env::temp_dir()` whose + /// Attempts to make a temporary directory inside of [`env::temp_dir()`] whose /// name will have the prefix, `prefix`. The directory and /// everything inside it will be automatically deleted once the /// returned `TempDir` is destroyed. @@ -522,7 +523,7 @@ impl<'a, 'b> Builder<'a, 'b> { let storage; let mut dir = dir.as_ref(); if !dir.is_absolute() { - let cur_dir = env::current_dir()?; + let cur_dir = std::env::current_dir()?; storage = cur_dir.join(dir); dir = &storage; } @@ -540,7 +541,7 @@ impl<'a, 'b> Builder<'a, 'b> { /// Attempts to create a temporary file (or file-like object) using the /// provided closure. The closure is passed a temporary file path and /// returns an [`std::io::Result`]. The path provided to the closure will be - /// inside of [`std::env::temp_dir()`]. Use [`Builder::make_in`] to provide + /// inside of [`env::temp_dir()`]. Use [`Builder::make_in`] to provide /// a custom temporary directory. If the closure returns one of the /// following errors, then another randomized file path is tried: /// - [`std::io::ErrorKind::AlreadyExists`] diff --git a/tests/namedtempfile.rs b/tests/namedtempfile.rs index 4b940b6b8..34ddc44bc 100644 --- a/tests/namedtempfile.rs +++ b/tests/namedtempfile.rs @@ -1,11 +1,10 @@ #![deny(rust_2018_idioms)] -use std::env; use std::ffi::{OsStr, OsString}; use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; -use tempfile::{tempdir, Builder, NamedTempFile, TempPath}; +use tempfile::{env, tempdir, Builder, NamedTempFile, TempPath}; fn exists>(path: P) -> bool { std::fs::metadata(path.as_ref()).is_ok() @@ -276,10 +275,10 @@ fn test_write_after_close() { #[test] fn test_change_dir() { - env::set_current_dir(env::temp_dir()).unwrap(); + std::env::set_current_dir(env::temp_dir()).unwrap(); let tmpfile = NamedTempFile::new_in(".").unwrap(); - let path = env::current_dir().unwrap().join(tmpfile.path()); - env::set_current_dir("/").unwrap(); + let path = std::env::current_dir().unwrap().join(tmpfile.path()); + std::env::set_current_dir("/").unwrap(); drop(tmpfile); assert!(!exists(path)) } diff --git a/tests/tempdir.rs b/tests/tempdir.rs index 8ca7984ac..2bc4001df 100644 --- a/tests/tempdir.rs +++ b/tests/tempdir.rs @@ -10,7 +10,6 @@ #![deny(rust_2018_idioms)] -use std::env; use std::fs; use std::path::Path; use std::sync::mpsc::channel; @@ -149,7 +148,7 @@ where F: FnOnce(), { let tmpdir = TempDir::new().unwrap(); - assert!(env::set_current_dir(tmpdir.path()).is_ok()); + assert!(std::env::set_current_dir(tmpdir.path()).is_ok()); f(); }