pub struct AtomicFile { /* private fields */ }Expand description
A scratch file next to an eventual destination, deleted unless it is persisted.
Gives a write its all-or-nothing behaviour: the file is built here and only takes the destination’s name once it is complete. Any earlier return drops the guard, which removes the partial file.
The destination is fixed at create and the scratch path is a sibling of
it, so publishing stays on one filesystem: never EXDEV, and atomic on POSIX. (Windows
documents no atomicity guarantee when the rename replaces an existing file.)
commit replaces the destination rather than truncating it. Its
permission bits are carried over on Unix, but its ownership and hard links cannot be: the
published file is a new inode. commit_new never replaces anything
and fails if the destination exists.
§Example
let destination = std::path::Path::new("model.myfmt");
// The handle is dropped here because this writer opens the path itself; keep it instead
// when you mean to write through it.
let (scratch, _reserved) = AtomicFile::create(destination)?;
write_my_format(scratch.path())?;
scratch.commit()?;Implementations§
Source§impl AtomicFile
impl AtomicFile
Sourcepub fn create(destination: &Path) -> Result<(Self, File), Error>
pub fn create(destination: &Path) -> Result<(Self, File), Error>
Create a scratch file alongside destination and hand back a handle onto it.
The guard and its file are handed back together so neither can exist without the other: no path is reserved that nothing will clean up, and no file is created that no guard is watching. A caller that writes through its own handle (because the library doing the writing opens the path itself) can drop the returned one; the exclusive create has already done its job by then.
The guard keeps destination, so commit and
commit_new can only publish to the path the scratch file was
placed beside. That is what makes the same-filesystem publish a property of the type
rather than a contract the caller has to honour.
The file is opened with create_new, which refuses a path that already exists and
does not follow a symlink planted there - a plain File::create would truncate
whatever such a link points at. The scratch name is predictable (pid + counter), so a
stale leftover from a killed run, pid reuse, or a deliberately pre-created link all
surface as AlreadyExists, answered by moving on to the next counter value.
Sourcepub fn path(&self) -> &Path
pub fn path(&self) -> &Path
Where to write. Nothing may be written to the destination itself until
commit or commit_new.
Sourcepub fn commit(self) -> Result<(), Error>
pub fn commit(self) -> Result<(), Error>
Flush the completed file to disk and move it onto the destination.
For a caller that wrote through a handle it opened itself and has since closed. The
scratch file is reopened here only to sync it, which is what orders the data before
the rename: a crash must not leave the destination pointing at bytes that never
reached the platter. The reopen asks for write access even though nothing is written
through it, because Windows backs sync_all with FlushFileBuffers, which returns
Access is denied on a read-only handle.
A caller that still holds the handle it wrote through should sync and drop that handle first: a deferred write error (NFS over quota, a failing disk) is reported to the handle that wrote the bytes and is lost once that handle is closed, so the sync here cannot stand in for it.
Sourcepub fn commit_new(self) -> Result<(), Error>
pub fn commit_new(self) -> Result<(), Error>
Like commit, but refuses to replace an existing destination.
Returns Error::AlreadyExists if anything is at the destination when the file is
published, and the guard removes the scratch file. Checking for the destination
beforehand cannot give this guarantee: another writer can create it between the check
and the rename, which would then silently replace it.
The file is published with a hard link, which refuses an existing destination in the same call that creates it. On filesystems without hard links (FAT, exFAT, some network mounts) this falls back to a check followed by a rename, which leaves a short window.