blob: 1498f0322bb5086fc0329eef5f3e33eeb5ea9ee4 [file] [edit]
//! 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());
}
}