| //! A temporary named file that will be deleted on drop, and on operating systems that support that, | |
| //! also when the process exits (including being killed). | |
| use std::{ | |
| fs::File, | |
| io, | |
| path::{Path, PathBuf}, | |
| }; | |
| pub struct NamedTempFile { | |
| _file: Option<File>, | |
| path: PathBuf, | |
| delete_on_drop: bool, | |
| } | |
| impl NamedTempFile { | |
| pub fn new(prefix: &str) -> io::Result<NamedTempFile> { | |
| imp::create(prefix) | |
| } | |
| /// Creates a new `NamedTempFile` that is a copy of an existing file. | |
| pub fn new_from_existing(prefix: &str, existing: &Path) -> io::Result<NamedTempFile> { | |
| let result = NamedTempFile::new(prefix)?; | |
| io::copy(&mut File::open(existing)?, &mut File::options().write(true).open(&result.path)?)?; | |
| Ok(result) | |
| } | |
| /// Creates a `NamedTempFile` from a path, without deleting it on drop. | |
| #[inline] | |
| pub fn from_path(path: PathBuf) -> NamedTempFile { | |
| NamedTempFile { _file: None, path, delete_on_drop: false } | |
| } | |
| #[inline] | |
| pub fn path(&self) -> &Path { | |
| &self.path | |
| } | |
| } | |
| impl Drop for NamedTempFile { | |
| fn drop(&mut self) { | |
| if self.delete_on_drop && std::fs::remove_file(&self.path).is_err() { | |
| tracing::info!("cannot remove temporary file {}", self.path.display()); | |
| } | |
| } | |
| } | |
| pub struct NamedTempDir { | |
| path: PathBuf, | |
| } | |
| impl NamedTempDir { | |
| pub fn new(prefix: &str) -> io::Result<NamedTempDir> { | |
| general_imp::create(prefix, |_options, path| std::fs::create_dir(path)) | |
| .map(|((), path)| NamedTempDir { path }) | |
| } | |
| #[inline] | |
| pub fn path(&self) -> &Path { | |
| &self.path | |
| } | |
| } | |
| impl Drop for NamedTempDir { | |
| fn drop(&mut self) { | |
| if std::fs::remove_dir_all(&self.path).is_err() { | |
| tracing::info!("cannot remove temporary directory {}", self.path.display()); | |
| } | |
| } | |
| } | |
| mod general_imp { | |
| use std::{ | |
| fs::OpenOptions, | |
| io::{self, ErrorKind}, | |
| path::{Path, PathBuf}, | |
| sync::atomic::{AtomicU32, Ordering}, | |
| }; | |
| static INTERNAL_COUNTER: AtomicU32 = AtomicU32::new(0); | |
| pub(super) fn create<T>( | |
| prefix: &str, | |
| mut create: impl FnMut(OpenOptions, &Path) -> io::Result<T>, | |
| ) -> io::Result<(T, PathBuf)> { | |
| let temp_dir = std::env::temp_dir().canonicalize()?; | |
| let pid = std::process::id(); | |
| loop { | |
| let path = temp_dir.join(format!( | |
| "{prefix}{pid:x}-{:x}", | |
| INTERNAL_COUNTER.fetch_add(1, Ordering::AcqRel), | |
| )); | |
| let mut open_options = OpenOptions::new(); | |
| open_options.write(true).create_new(true); | |
| match create(open_options, &path) { | |
| Err(e) if e.kind() == ErrorKind::AlreadyExists => {} | |
| Err(e) => { | |
| return Err(io::Error::new( | |
| e.kind(), | |
| format!("error creating directory {path:?}: {e}"), | |
| )); | |
| } | |
| Ok(file) => { | |
| return Ok((file, path)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| #[cfg(any( | |
| target_os = "linux", | |
| target_os = "freebsd", | |
| target_os = "openbsd", | |
| target_os = "netbsd", | |
| ))] | |
| mod imp { | |
| use std::{ | |
| ffi::CString, | |
| io, | |
| os::{ | |
| fd::{AsRawFd, RawFd}, | |
| unix::ffi::OsStrExt, | |
| }, | |
| }; | |
| use super::*; | |
| #[cfg(target_os = "linux")] | |
| fn path_after_unlink(fd: RawFd) -> PathBuf { | |
| PathBuf::from(format!("/proc/self/fd/{fd}")) | |
| } | |
| #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))] | |
| fn path_after_unlink(fd: RawFd) -> PathBuf { | |
| PathBuf::from(format!("/dev/fd/{fd}")) | |
| } | |
| pub(super) fn create(prefix: &str) -> io::Result<NamedTempFile> { | |
| let (file, mut path) = general_imp::create(prefix, |options, path| options.open(path))?; | |
| let mut delete_on_drop = true; | |
| if let Ok(original_path) = CString::new(path.as_os_str().as_bytes()) { | |
| // Unlinking the file will *not* remove it per the POSIX specification since it is open. | |
| // We cannot use `std::fs::remove_file()`, since, while currently using `unlink()`, it does | |
| // not guarantee it will use it. | |
| if unsafe { libc::unlink(original_path.as_ptr()) } == 0 { | |
| path = path_after_unlink(file.as_raw_fd()); | |
| delete_on_drop = false; | |
| } | |
| } | |
| Ok(NamedTempFile { _file: Some(file), path, delete_on_drop }) | |
| } | |
| } | |
| #[cfg(windows)] | |
| mod imp { | |
| use std::os::windows::fs::OpenOptionsExt; | |
| use super::*; | |
| const FILE_ATTRIBUTE_TEMPORARY: u32 = 0x100; | |
| const FILE_FLAG_DELETE_ON_CLOSE: u32 = 0x04000000; | |
| const GENERIC_READ: u32 = 0x80000000; | |
| pub(super) fn create(prefix: &str) -> io::Result<NamedTempFile> { | |
| let (file, path) = general_imp::create(prefix, |mut options, path| { | |
| options | |
| .access_mode(GENERIC_READ) | |
| .attributes(FILE_ATTRIBUTE_TEMPORARY) | |
| .custom_flags(FILE_FLAG_DELETE_ON_CLOSE) | |
| .open(path) | |
| })?; | |
| Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: false }) | |
| } | |
| } | |
| #[cfg(not(any( | |
| target_os = "linux", | |
| target_os = "freebsd", | |
| target_os = "openbsd", | |
| target_os = "netbsd", | |
| windows, | |
| )))] | |
| mod imp { | |
| use super::*; | |
| pub(super) fn create(prefix: &str) -> io::Result<NamedTempFile> { | |
| let (file, path) = general_imp::create(prefix, |options, path| options.open(path))?; | |
| Ok(NamedTempFile { _file: Some(file), path, delete_on_drop: true }) | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use std::fs; | |
| use super::*; | |
| #[test] | |
| fn named_temp_file_new_from_existing_copies_file() { | |
| let dir = NamedTempDir::new("test-").unwrap(); | |
| let source = dir.path().join("source"); | |
| fs::write(&source, b"contents").unwrap(); | |
| let file = NamedTempFile::new_from_existing("test-copy-", &source).unwrap(); | |
| assert_eq!(fs::read(file.path()).unwrap(), b"contents"); | |
| fs::write(&source, b"changed").unwrap(); | |
| assert_eq!(fs::read(file.path()).unwrap(), b"contents"); | |
| let path = file.path().to_owned(); | |
| drop(file); | |
| assert!(!fs::exists(path).unwrap()); | |
| assert_eq!(fs::read(source).unwrap(), b"changed"); | |
| } | |
| #[cfg(windows)] | |
| #[test] | |
| fn named_temp_file_allows_read_only_sharing() { | |
| use std::os::windows::fs::OpenOptionsExt; | |
| const FILE_SHARE_READ: u32 = 1; | |
| const FILE_SHARE_DELETE: u32 = 4; | |
| let file = NamedTempFile::new("test-").unwrap(); | |
| let _reader = fs::OpenOptions::new() | |
| .read(true) | |
| .share_mode(FILE_SHARE_READ | FILE_SHARE_DELETE) | |
| .open(file.path()) | |
| .unwrap(); | |
| } | |
| #[test] | |
| fn named_temp_file_new_creates_file() { | |
| let file = NamedTempFile::new("test-").unwrap(); | |
| assert!(file.path().exists()); | |
| let path = file.path().as_os_str().to_owned(); | |
| drop(file); | |
| assert!(!fs::exists(path).unwrap()); | |
| } | |
| } |