use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
pub struct TempFile {
path: PathBuf,
persisted: bool,
}
impl TempFile {
pub fn new(prefix: &str, suffix: &str) -> io::Result<Self> {
let path = unique_temp_path(prefix, suffix);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)?;
Ok(Self {
path,
persisted: false,
})
}
pub fn with_content(prefix: &str, suffix: &str, content: &[u8]) -> io::Result<Self> {
let mut file = Self::new(prefix, suffix)?;
file.write_all(content)?;
Ok(file)
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub fn read_all(&self) -> io::Result<Vec<u8>> {
fs::read(&self.path)
}
pub fn write_all(&mut self, content: &[u8]) -> io::Result<()> {
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(&self.path)?;
file.write_all(content)
}
pub fn persist(mut self, destination: impl AsRef<Path>) -> io::Result<PathBuf> {
let destination = destination.as_ref().to_path_buf();
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)?;
}
fs::rename(&self.path, &destination)?;
self.persisted = true;
Ok(destination)
}
}
impl Drop for TempFile {
fn drop(&mut self) {
if !self.persisted {
let _ = fs::remove_file(&self.path);
}
}
}
fn unique_temp_path(prefix: &str, suffix: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be after epoch")
.as_nanos();
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let file_name = format!("{prefix}-{}-{nanos}-{counter}{suffix}", process::id());
std::env::temp_dir().join(file_name)
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use super::TempFile;
#[test]
fn temp_file_new_creates_file_on_disk() {
let temp_file = TempFile::new("rjango-temp", ".tmp").expect("create temp file");
assert!(temp_file.path().exists());
}
#[test]
fn temp_file_with_content_reads_back_bytes() {
let temp_file = TempFile::with_content("rjango-temp", ".txt", b"hello")
.expect("create temp file with content");
assert_eq!(temp_file.read_all().expect("read temp file"), b"hello");
}
#[test]
fn temp_file_write_all_replaces_existing_content() {
let mut temp_file = TempFile::with_content("rjango-temp", ".txt", b"before")
.expect("create temp file with content");
temp_file
.write_all(b"after")
.expect("write replacement content");
assert_eq!(temp_file.read_all().expect("read temp file"), b"after");
}
#[test]
fn temp_file_persist_moves_file_to_destination() {
let temp_file = TempFile::with_content("rjango-temp", ".txt", b"persist")
.expect("create temp file with content");
let destination = std::env::temp_dir().join("rjango-persisted-file.txt");
if destination.exists() {
fs::remove_file(&destination).expect("remove stale persisted file");
}
let persisted_path = temp_file.persist(&destination).expect("persist temp file");
assert_eq!(persisted_path, destination);
assert_eq!(
fs::read(&persisted_path).expect("read persisted file"),
b"persist"
);
fs::remove_file(persisted_path).expect("cleanup persisted file");
}
#[test]
fn dropping_non_persisted_temp_file_removes_it() {
let path = {
let temp_file = TempFile::new("rjango-temp", ".tmp").expect("create temp file");
temp_file.path().to_path_buf()
};
assert!(!path.exists());
let _ = PathBuf::from(path);
}
}