Temporary file
A temporary file is scratch space with a name: somewhere to put data too big for memory, or a file to hand to another program. Two things make it harder than opening any file. The name must not clash with another file, and must not be guessable. And the file must not be left behind when the program is done with it — not even when the program leaves early through a failure.
A name nobody can predict
If another program could predict the name, it could create that name first — perhaps as a link to some other file — and this program would write straight through it. So TemporaryFile::Create builds the name from a prefix plus 16 random hex digits, and creates the file only if nothing by that name exists yet; a collision just means the next random name is tried.
func Create(allocator, directory: Path, prefix: char8[..]) -> TemporaryFile ! IoError
var scratch = TemporaryFile::Create(allocator, directory, "draft-")?;
WriteAll(scratch.file, "work in progress")?;
PrintLine("created {}", scratch.AsPath().FileName() ?? OsStringView());
The name comes out as draft- and sixteen hex digits, different on every run. scratch.file is an ordinary open File, opened for both reading and writing, and AsPath lends the full path.
The directory is up to you. FileSystem::TemporaryDirectory returns the system's own folder for such files; this program uses the package's Bin/ folder instead, so you can watch what happens there.
Three ways to finish
A TemporaryFile owns both the open file and its name, so it decides what happens to them:
flowchart LR
c["TemporaryFile::Create"] --> use["write, read,<br/>hand on the name"]
use --> close["Close(allocator)?"]
use --> drop["dropped without Close:<br/>an early return, a failure"]
use --> keep["Keep()?"]
close --> gone1["closed and deleted;<br/>a failure is reported"]
drop --> gone2["the destructor deletes it;<br/>a failure goes unreported"]
keep --> stays["closed, and the file stays;<br/>the value gives up its name"]| Ending | The file afterwards | A failed delete is… |
|---|---|---|
Close(allocator)? | deleted | reported, as an IoError |
| dropped | deleted | silently ignored |
Keep()? | kept | — nothing is deleted |
Exists checks the outcome. It asks for the file's metadata and turns the fallible into a bool with Core::Succeeded — true for any success — met in Generic outcome:
func Exists(allocator: Allocator, path: Path) -> bool {
return Succeeded(MetadataOf(allocator, path, true));
}
Closing: remember the name first
After Close, the value no longer has a name — its AsPath is empty. To check that the file is gone, the program copies the name into a PathBuffer of its own before closing:
var name = PathBuffer::FromPath(allocator, scratch.AsPath())?;
scratch.Close(allocator)?;
PrintLine("exists after Close {}", Exists(allocator, name.AsPath()));
Close closes the file, deletes it, and reports whether the deletion worked.
Dropping: the destructor cleans up
Forgotten creates a temporary file, writes to it, and returns without closing it:
func Forgotten(allocator: Allocator, directory: Path) -> PathBuffer ! IoError | TextError {
var scratch = TemporaryFile::Create(allocator, directory, "draft-")?;
WriteAll(scratch.file, "never finished")?;
var name = PathBuffer::FromPath(allocator, scratch.AsPath())?;
return <-name;
}
When Forgotten returns, scratch goes out of scope and its destructor deletes the file. The same happens if any ? in the function passes a failure on, which is the point: no path out of the function leaves the file behind. The name comes back as a PathBuffer moved out with <-, as in Move, and Main confirms the file no longer exists.
The safety net has the same limit as the one in Buffered I/O: a destructor cannot report a failure, so a delete that fails there goes unnoticed. Call Close when you want to know.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A temporary file is scratch space with a name: somewhere to put data that is too big for
// memory, or a name to hand to another program. Two things make that harder than it sounds.
//
// The name must not clash, and must not be guessable. If another program could predict it, it
// could create that name first, perhaps as a link to some other file, and this program would
// write through it. `TemporaryFile::Create` builds the name from a prefix plus 16 random hex
// digits, and creates the file only if nothing by that name exists yet.
//
// func Create(allocator, directory: Path, prefix: char8[..]) -> TemporaryFile ! IoError
//
// And the file must not be left behind. A `TemporaryFile` owns both the open file and its name.
// `Close` closes and deletes it, and reports whether the deletion worked. If the value is
// dropped without `Close`, its destructor deletes the file anyway, on early returns and failure
// paths too, but a destructor cannot report a failure, so it stays quiet about one. `Keep`
// closes the file and gives up the name, for a file that should outlive the value.
//
// `FileSystem::TemporaryDirectory` returns the system's folder for such files. This program
// uses this package's own `Bin/` folder instead. The name is random, so it differs on every run.
import Allocator::{ Allocator, SystemAllocator };
import Core::Succeeded;
import FileSystem::{ MetadataOf, TemporaryFile };
import Io::{ IoError, PrintLine, WriteAll };
import Path::{ OsString, OsStringView, Path, PathBuffer };
import Text::TextError;
func Exists(allocator: Allocator, path: Path) -> bool {
return Succeeded(MetadataOf(allocator, path, true));
}
// Creates a temporary file, writes to it, and returns without closing it.
func Forgotten(allocator: Allocator, directory: Path) -> PathBuffer ! IoError | TextError {
var scratch = TemporaryFile::Create(allocator, directory, "draft-")?;
WriteAll(scratch.file, "never finished")?;
var name = PathBuffer::FromPath(allocator, scratch.AsPath())?;
return <-name;
}
func Main() -> ! IoError | TextError {
var system = SystemAllocator();
let allocator: Allocator = system;
var holder = OsString::FromText(allocator, "Bin")?;
let directory = Path::FromView(holder.View());
var scratch = TemporaryFile::Create(allocator, directory, "draft-")?;
// `file` is an ordinary open `File`, opened for reading and writing.
WriteAll(scratch.file, "work in progress")?;
PrintLine("created {}", scratch.AsPath().FileName() ?? OsStringView());
PrintLine("exists while held {}", Exists(allocator, scratch.AsPath()));
// Remember the name, because after `Close` the value no longer has one.
var name = PathBuffer::FromPath(allocator, scratch.AsPath())?;
scratch.Close(allocator)?;
PrintLine("exists after Close {}", Exists(allocator, name.AsPath()));
let forgotten = Forgotten(allocator, directory)?;
PrintLine("exists after drop {}", Exists(allocator, forgotten.AsPath()));
}
Besides Io, its Rux.toml lists Allocator, Core, FileSystem, Path and Text under [Dependencies].
Run it
cd Examples/Files/TemporaryFile
rux run
created draft-08bf264c6b30a0cc
exists while held true
exists after Close false
exists after drop false
Common mistakes
If
Forgotten returned scratch.AsPath() as a Path, it would compile — and crash when Main used it. A Path only borrows its units, and these belong to scratch, which is destroyed as the function returns. Copy the name into a PathBuffer that owns its units, as the lesson does.<-.return name; fails with error: move-only value 'name' requires an explicit '<-' in return, and a note that 'PathBuffer' prohibits copying. Write return <-name;.Close.Once closed, the value has given its name up:
scratch.AsPath() is an empty path. Copy the name first if you still need it.let.let scratch = TemporaryFile::Create(…)?; makes writing fail with error: argument 1 to 'WriteAll' cannot borrow a part of immutable 'scratch' as '&var Writer', and Close with cannot call 'Close' on immutable 'scratch'.Try it yourself
- Replace
scratch.Close(allocator)?withscratch.Keep()?. Does the file survive the program? Look inBin/, and delete it by hand afterwards. - Create the file in the system's folder instead:
var temporary = TemporaryDirectory(allocator)?;gives aPathBuffer, andtemporary.AsPath()is the directory. - Make
Forgottenfail on purpose after writing — for example withfail IoError::Of(IoErrorKind::Other);— so that the program ends with status 1. Is adraft-file left inBin/afterwards? - Create two temporary files with the same prefix and print both names.
Learn more
- Destructor — what runs when
scratchgoes out of scope - Move — returning the
PathBufferwith<- - Metadata — how
Existsasks whether a file is there - Atomic file — a temporary file put to work: replacing another file whole