use std::io;
use std::path::{Path, PathBuf};
use crate::unique_name;
#[derive(Debug)]
pub struct NamedTempFile {
path: PathBuf,
cleanup_on_drop: bool,
}
impl NamedTempFile {
pub fn new() -> io::Result<Self> {
let name = unique_name(12);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!(".tmpfile-{pid}-{name}"));
std::fs::File::create(&path)?;
Ok(Self {
path,
cleanup_on_drop: true,
})
}
pub fn with_prefix(prefix: &str) -> io::Result<Self> {
let name = unique_name(12);
let path = std::env::temp_dir().join(format!("{prefix}-{name}"));
std::fs::File::create(&path)?;
Ok(Self {
path,
cleanup_on_drop: true,
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn persist(mut self) -> PathBuf {
self.cleanup_on_drop = false;
self.path.clone()
}
pub fn cleanup_on_drop(&self) -> bool {
self.cleanup_on_drop
}
pub fn persist_atomic(
mut self,
target: impl AsRef<Path>,
) -> Result<PathBuf, PersistAtomicError> {
let target = target.as_ref();
match std::fs::OpenOptions::new().write(true).open(&self.path) {
Ok(handle) => {
if let Err(error) = handle.sync_all() {
return Err(PersistAtomicError { error, file: self });
}
}
Err(error) => return Err(PersistAtomicError { error, file: self }),
}
if let Err(error) = std::fs::rename(&self.path, target) {
return Err(PersistAtomicError { error, file: self });
}
if let Some(parent) = target.parent() {
let _ = sync_directory(parent);
}
self.cleanup_on_drop = false;
Ok(target.to_path_buf())
}
}
#[derive(Debug)]
pub struct PersistAtomicError {
pub error: io::Error,
pub file: NamedTempFile,
}
impl std::fmt::Display for PersistAtomicError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "atomic persist failed: {}", self.error)
}
}
impl std::error::Error for PersistAtomicError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
impl From<PersistAtomicError> for io::Error {
fn from(e: PersistAtomicError) -> Self {
e.error
}
}
#[cfg(unix)]
fn sync_directory(path: &Path) -> io::Result<()> {
let dir = std::fs::File::open(path)?;
dir.sync_all()
}
#[cfg(windows)]
fn sync_directory(path: &Path) -> io::Result<()> {
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
let dir = std::fs::OpenOptions::new()
.write(true)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)?;
dir.sync_all()
}
#[cfg(not(any(unix, windows)))]
fn sync_directory(_path: &Path) -> io::Result<()> {
Ok(())
}
impl Drop for NamedTempFile {
fn drop(&mut self) {
if self.cleanup_on_drop {
let _ = std::fs::remove_file(&self.path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creates_file() {
let f = NamedTempFile::new().unwrap();
assert!(f.path().exists());
assert!(f.path().is_file());
}
#[test]
fn auto_cleanup() {
let path = {
let f = NamedTempFile::new().unwrap();
f.path().to_path_buf()
};
assert!(!path.exists());
}
#[test]
fn persist_disables_cleanup() {
let f = NamedTempFile::new().unwrap();
let path = f.persist();
assert!(path.exists());
std::fs::remove_file(&path).unwrap();
}
#[test]
fn with_prefix_works() {
let f = NamedTempFile::with_prefix("named").unwrap();
let name = f.path().file_name().unwrap().to_string_lossy();
assert!(name.starts_with("named-"));
}
#[test]
fn two_files_unique() {
let a = NamedTempFile::new().unwrap();
let b = NamedTempFile::new().unwrap();
assert_ne!(a.path(), b.path());
}
}