How to work with temporary files
Short answer: std::env::temp_dir() gives you the directory, OpenOptions::create_new(true) claims a name nobody else can take, and a Drop impl deletes it when you’re done. Drop-based cleanup dies with the process — after a crash the file stays — so sweep your old files at the next startup.
Where temporary files live
Section titled “Where temporary files live”Start from a fresh project — cargo new imgtool, replace the contents of src/main.rs with the block below, and cargo run. The path it prints is this machine’s temp directory; yours will differ — macOS gives every user their own directory under /var/folders/…, Linux uses $TMPDIR or falls back to /tmp, and Windows goes through GetTempPath. Ask, don’t hard-code:
use std::env;use std::fs::{self, OpenOptions};use std::io::Write;
fn main() -> std::io::Result<()> { let path = env::temp_dir().join("demo-notes.txt"); let _ = fs::remove_file(&path); // so a rerun doesn't die on the next line
let mut file = OpenOptions::new() .write(true) .create_new(true) // refuses to open an existing name .open(&path)?; file.write_all(b"scratch data")?; println!("wrote {}", path.display());
fs::remove_file(&path)?; // nothing deletes this for you Ok(())}wrote /var/folders/9x/qv6cmc_x7qj8pbzj846pb4vc0000gn/T/demo-notes.txtcreate_new(true) is the load-bearing line. The std docs for temp_dir warn that the directory “may be shared among users, or between processes with different privileges” and that a file with “a fixed or predictable name” invites an “insecure temporary file” vulnerability. create_new opens the file only if the name isn’t taken; when another process got there first, you get Os { code: 17, kind: AlreadyExists, message: "File exists" }. The remove_file above it is what makes reruns work — without it, the second run dies on create_new, the same race two instances of your program would hit.
A name nobody else can take
Section titled “A name nobody else can take”A fixed name works for a demo and fails in production, where two instances run at once. The usual shape is prefix + process id + a nanosecond timestamp, with create_new arbitrating and a retry loop for the rare collision. Cleanup lives in Drop:
use std::env;use std::fs::{self, File, OpenOptions};use std::io::{self, Write};use std::path::PathBuf;use std::process;use std::time::{SystemTime, UNIX_EPOCH};
struct TempFile { file: File, path: PathBuf,}
impl TempFile { fn new(prefix: &str) -> io::Result<Self> { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("clock before 1970") .as_nanos(); for counter in 0u32.. { let name = format!("{prefix}-{}-{nanos}-{counter}", process::id()); let path = env::temp_dir().join(name); match OpenOptions::new().write(true).create_new(true).open(&path) { Ok(file) => return Ok(TempFile { file, path }), Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue, Err(e) => return Err(e), } } unreachable!() }}
impl Drop for TempFile { fn drop(&mut self) { let _ = fs::remove_file(&self.path); }}
fn main() -> io::Result<()> { let path; { let mut temp = TempFile::new("imgtool")?; path = temp.path.clone(); temp.file.write_all(b"scratch")?; println!("inside scope: {}", path.exists()); } println!("after scope: {}", path.exists()); Ok(())}inside scope: trueafter scope: falseWhen temp leaves scope the file is gone. A panic unwinds too — unwinding runs destructors.
What a crash does to that
Section titled “What a crash does to that”Drop is a promise the process has to be alive to keep. std::process::exit never returns, and its docs say plainly that “no destructors on the current stack or any other thread’s stack will be run.” A crash is the same hole — abort’s docs list what gets skipped (“no destructors … will be run, Rust IO buffers … will not be flushed”) — and kill -9 doesn’t run your code at all. Watch it happen:
use std::env;use std::fs::OpenOptions;use std::io::Write;use std::process;use std::time::{SystemTime, UNIX_EPOCH};
fn main() { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("clock before 1970") .as_nanos(); let path = env::temp_dir().join(format!("imgtool-{}-{nanos}", process::id())); let mut file = OpenOptions::new() .write(true) .create_new(true) .open(&path) .expect("create failed"); file.write_all(b"scratch").expect("write failed");
println!("created {}", path.display()); process::exit(0); // skips Drop: the file stays behind}$ cargo run -qcreated /var/folders/9x/qv6cmc_x7qj8pbzj846pb4vc0000gn/T/imgtool-22348-1792538486639309000$ ls /var/folders/9x/qv6cmc_x7qj8pbzj846pb4vc0000gn/T/imgtool-22348-1792538486639309000/var/folders/9x/qv6cmc_x7qj8pbzj846pb4vc0000gn/T/imgtool-22348-1792538486639309000Exit code 0, file still on disk. The numbers in the name are the process id and a nanosecond timestamp, so yours will differ.
Cleanup that survives a crash can’t live in the crashing process — someone else has to do it. The cheap version is a sweep at the next startup: give every file a prefix you own, delete the stale ones before creating new ones.
use std::env;use std::fs::{self, OpenOptions};use std::io;use std::time::{Duration, SystemTime};
fn main() -> io::Result<()> { // stand-in for a leftover from a crashed run: two hours old let leftover = env::temp_dir().join("imgtool-stale-demo"); let _ = fs::remove_file(&leftover); let f = OpenOptions::new().write(true).create(true).open(&leftover)?; let two_hours_ago = SystemTime::now() - Duration::from_secs(2 * 60 * 60); f.set_modified(two_hours_ago)?;
let max_age = Duration::from_secs(60 * 60); for entry in fs::read_dir(env::temp_dir())? { let Ok(entry) = entry else { continue }; if !entry.file_name().to_string_lossy().starts_with("imgtool-") { continue; } let stale = match entry.metadata().and_then(|m| m.modified()) { Ok(t) => t.elapsed().map(|age| age > max_age).unwrap_or(false), Err(_) => false, }; if stale { let _ = fs::remove_file(entry.path()); } } println!("stale-demo gone: {}", !leftover.exists()); Ok(())}stale-demo gone: trueThe block plants a fake leftover two hours old (set_modified backdates the timestamp), then sweeps anything with the imgtool- prefix older than an hour. The age cutoff matters: a file a minute old probably belongs to an instance that’s still running. Removal failures are ignored on purpose — the file may be gone already, and a sweeper that dies over a lost race isn’t much of a sweeper.
The tempfile crate
Section titled “The tempfile crate”If you’d rather not hand-roll this, tempfile is the crate the temp_dir docs point to: NamedTempFile::new() creates a uniquely named file in temp_dir(), TempDir::new() a whole directory, both deleted when the value drops, keep() if you want one to stay (checked against tempfile 3.27.0). The crash hole is the same — a TempDir held across process::exit is left behind — so the sweep stays your job.
Related
Section titled “Related”- How to write a file atomically — its temp file sits beside the target on purpose, because
fs::renameneeds one filesystem - How to delete a file or directory — what
remove_filedoes when the file isn’t there - Reading and writing files — the rest of the chapter