Skip to main content

Storage

Trait Storage 

Source
pub trait Storage: ReadStorage {
Show 13 methods // Required methods fn write( &self, path: &Path, contents: &[u8], ) -> impl Future<Output = Result<(), Error>>; fn create_dir_all( &self, path: &Path, ) -> impl Future<Output = Result<(), Error>>; fn remove_file( &self, path: &Path, ) -> impl Future<Output = Result<(), Error>>; fn remove_dir_all( &self, path: &Path, ) -> impl Future<Output = Result<(), Error>>; fn rename( &self, from: &Path, to: &Path, ) -> impl Future<Output = Result<(), Error>>; // Provided methods fn create_new( &self, path: &Path, contents: &[u8], ) -> impl Future<Output = Result<(), Error>> { ... } fn copy_permissions( &self, from: &Path, to: &Path, ) -> impl Future<Output = Result<(), Error>> { ... } fn set_executable( &self, path: &Path, executable: bool, ) -> impl Future<Output = Result<(), Error>> { ... } fn set_link( &self, path: &Path, target: &Path, ) -> impl Future<Output = Result<(), Error>> { ... } fn capabilities(&self) -> Capabilities { ... } fn sync( &self, path: &Path, need: Durability, ) -> impl Future<Output = Result<(), Error>> { ... } fn replace( &self, path: &Path, contents: &[u8], ) -> impl Future<Output = Result<(), Error>> { ... } fn write_atomic( &self, path: &Path, contents: &[u8], ) -> impl Future<Output = Result<(), Error>> { ... }
}
Expand description

An async filesystem backend a transaction can drive — ReadStorage plus everything that changes bytes on disk.

Each method mirrors the std::fs function of the same name. Backends implement the write/mutate/durability surface here and the read surface on ReadStorage.

Required Methods§

Source

fn write( &self, path: &Path, contents: &[u8], ) -> impl Future<Output = Result<(), Error>>

Write a file, replacing it if it already exists. Mirrors std::fs::write.

Source

fn create_dir_all(&self, path: &Path) -> impl Future<Output = Result<(), Error>>

Create a directory and all missing parents. Mirrors std::fs::create_dir_all.

Source

fn remove_file(&self, path: &Path) -> impl Future<Output = Result<(), Error>>

Remove a regular file. Mirrors std::fs::remove_file.

Source

fn remove_dir_all(&self, path: &Path) -> impl Future<Output = Result<(), Error>>

Recursively remove a directory and its contents. Mirrors std::fs::remove_dir_all.

Source

fn rename( &self, from: &Path, to: &Path, ) -> impl Future<Output = Result<(), Error>>

Rename or move a file or directory. Mirrors std::fs::rename — and the load-bearing half of the mirror is that an occupied destination file is replaced, as std::fs::rename replaces one on every platform this crate targets. The default write_atomic publishes by renaming a staged sibling over the target, so a backend whose rename refuses an occupied file cannot take that default and must override write_atomic with its own atomic replacement. A directory destination is another matter — std::fs::rename itself is platform-divergent there — and nothing in this crate renames onto one.

Provided Methods§

Source

fn create_new( &self, path: &Path, contents: &[u8], ) -> impl Future<Output = Result<(), Error>>

Create a file that must not already exist, and write contents to it. Mirrors std::fs::File::create_new followed by a full write.

The create and the test-for-existence are one operation, and that indivisibility is the entire point: of two writers racing to the same name, exactly one succeeds and the other is told AlreadyExists — with no window between a check and a create for either to slip a half-written file through. AlreadyExists is therefore a load-bearing answer, not a failure to smooth over: a write-once consumer branches on it (typically by reading back what is there and confirming it is what it meant to write), so an implementation must report that kind and no other for an occupied path.

Two things this deliberately does not do, both the caller’s to ask for: parents are not created (create_dir_all first, as std::fs::File::create_new would demand), and nothing is flushed — a caller that needs the new file to survive a crash pairs this with sync, which is what lets it choose the weakest durability that is correct where a built-in flush would impose the strongest everywhere.

The default refuses with Unsupported, and Capabilities::exclusive_create defaults to false to match: a backend that can keep the exclusivity promise declares it and overrides this, and one that cannot must not paper over the difference with a check-then-write — the window in that emulation is exactly what a caller reaching for this method cannot tolerate.

Source

fn copy_permissions( &self, from: &Path, to: &Path, ) -> impl Future<Output = Result<(), Error>>

Give to the same access permissions from has. A from that does not exist is not an error — there is no prior state to carry over, so the call has nothing to do.

This exists for write_atomic, which publishes its bytes by renaming a freshly-created sibling over the target. A new file is born with the backend’s default permissions, and a rename carries those onto the name it replaces — so without this step, replacing a document the user had deliberately restricted (chmod 600 on a private journal entry) silently widens it to whatever the umask allows. A content replacement must not be a permission change.

What this does not close is the window before it: the sibling holds the new contents under default permissions from the moment it is written until this call narrows it. Shutting that window means creating the file with the final mode already on it, which is not something write — a std::fs::write mirror — can express. The sibling lives in the target’s own directory throughout, so whatever gates access to the document gates access to it too.

The default is a no-op, which is the correct behavior for a backend with no permission model at all — InMemoryFs, OPFS, IndexedDB. There is nothing there to preserve, and nothing is lost by not preserving it.

Source

fn set_executable( &self, path: &Path, executable: bool, ) -> impl Future<Output = Result<(), Error>>

Make the file at path runnable, or not. One bit, not a mode: nothing else about the file changes.

The write half of executable, and the default follows from its decline: a backend whose executable answers None has nothing to set, so doing nothing is the honest implementation — the same reasoning as copy_permissions, where a backend with no permission model loses nothing by not preserving one. A backend that does model the bit overrides both members together; answering one without the other would let a change set read a bit it cannot restore, or restore one it cannot read.

Place a symbolic link at path pointing at target, replacing whatever is there.

The write half of read_link. Nothing here opens or resolves target: a link may point outside the tree, at nothing, or at itself, and the only consequence is an honest symlink pointing where symlinks are allowed to point. The replacement addresses the entry itself, never the entry’s referent — a link at path is removed and remade rather than written through, or a plain file there gives way to the link.

The default refuses with Unsupported, where set_executable’s default no-ops — the asymmetry is deliberate. An execute bit not modeled costs nothing to leave unset; a link not modeled has no honest substitute, because a plain file holding the target’s text would invent content nothing asked to write. A backend with no links must say so, and the caller decides what its absence means.

Source

fn capabilities(&self) -> Capabilities

What durability guarantees this backend can make. Defaults to Capabilities::NONE — a backend promises a guarantee only by saying so, so an adapter that forgets to override this degrades to the most defensive path rather than to a false promise.

Source

fn sync( &self, path: &Path, need: Durability, ) -> impl Future<Output = Result<(), Error>>

Flush path — and nothing else — to the strength need asks for.

path names one object, and only that object is flushed. To make a directory entry durable (the naming half of a create or a rename), sync the directory itself: on a POSIX filesystem a directory is a thing that can be opened and fsynced, and this crate’s own write_atomic does exactly that after its rename. Folding the parent into every call instead would flush twice as much as any single step needs, and would leave the caller unable to say which of the two it actually meant.

need is the weakest guarantee that is still correct at the call site, not a wish. Durability::Ordered asks only that everything written to path before this call land before anything written after it — enough to stop a rename overtaking the bytes it publishes, and on some platforms far cheaper than the real thing. Durability::Durable asks that the bytes survive power loss. A backend may always answer with something stronger than it was asked for; it may never answer with something weaker.

The default is a no-op, which is the correct behavior for any backend whose capabilities report SyncGuarantee::None: it cannot make the promise, so it must not pretend to. A backend that can flush must both override this and report the strongest request it genuinely honors — the two always travel together, and SyncGuarantee::satisfies is how a caller asks.

Source

fn replace( &self, path: &Path, contents: &[u8], ) -> impl Future<Output = Result<(), Error>>

Replace path’s contents with contents atomically — and only atomically: no observer ever sees a splice of old and new bytes, but nothing here outlives a power cut until the caller flushes it. The atomic half of write_atomic, split out so that a protocol landing many replacements can batch one drain instead of paying one per file — which is exactly what ChangeSet and OrderedBatch do, settling the whole set’s flush debt once before the state is certified.

The default composes the primitives into the staging protocol, whenever capabilities report atomic_replace:

  1. write the bytes to a temporary sibling;
  2. sync that sibling Ordered, so the rename cannot be reordered ahead of the bytes it publishes;
  3. copy_permissions from the target onto that sibling, so the replacement carries the target’s access permissions rather than a fresh file’s defaults;
  4. rename it over the target — this is the atomic instant.

Steps 2 and 3 are in that order because a backend may implement sync by opening the path, and a mode faithfully copied from the target can be one that forbids opening it to read — 0o200 is replaceable but not readable. The cost is that the mode change lands after the flush and so is not itself durable: a crash in that window can leave the new contents under the default permissions. That is precisely the outcome every write had before step 3 existed, so the window is a smaller bad case, never a new one.

One flush, and it is a barrier, not a promise of survival: what it rules out is the rename being seen before the bytes it publishes. The bytes are never flushed under their final name, because a rename does not move an inode — the file the target now names is the very one step 2 flushed. The sibling’s own directory entry is never flushed either, because nobody is owed a temporary that survives a crash. What is owed afterwards — the parent directory’s entry, and durability itself — is the caller’s to settle, per file (write_atomic) or batched.

A backend that cannot rename atomically falls back to a plain, unflushed write, which is not crash-atomic; the caller was told by capabilities, and still owns every flush. A backend whose atomic replacement is native — a locked in-memory swap, a transactional store — overrides this method, and write_atomic’s default composes on top of the override. An override claiming atomic_replace inherits step 2’s obligation along with the method: its bytes must be ordered ahead of whatever publishes them before the call returns, because both write_atomic’s composed default and the batched protocols add only directory flushes afterwards, never a second look at the bytes.

The temporary is removed on any failure, so a torn attempt leaves the target exactly as it was and no litter behind. It is a dotted sibling in the target’s own directory, so the follow-up rename stays within one filesystem (a cross-device rename is neither atomic nor, often, even permitted).

Source

fn write_atomic( &self, path: &Path, contents: &[u8], ) -> impl Future<Output = Result<(), Error>>

Replace path’s contents with contents atomically and durably: no observer — concurrent reader or post-crash survivor — ever sees a splice of old and new bytes, and once this returns the new contents outlive a power loss.

replace plus the flushes it leaves to its caller, composed through replace itself so a backend’s override carries: the replacement lands, the parent directory is flushed Durable to carry the rename through a power cut, and on a backend without atomic_replace the plainly-written bytes are flushed durable too, since there was no rename to fold their naming into. The right call for a standalone save; a protocol landing many files reaches for replace and batches the flushes instead.

A backend that overrode this method wholesale under the old guidance should move that override to replace: the crate’s own protocols now reach for replace directly, and an override living only here is bypassed by every one of them.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementations on Foreign Types§

Source§

impl<S> Storage for &S
where S: Storage + ?Sized,

Source§

async fn write(&self, path: &Path, contents: &[u8]) -> Result<(), Error>

Source§

async fn create_new(&self, path: &Path, contents: &[u8]) -> Result<(), Error>

Source§

async fn create_dir_all(&self, path: &Path) -> Result<(), Error>

Source§

async fn remove_file(&self, path: &Path) -> Result<(), Error>

Source§

async fn remove_dir_all(&self, path: &Path) -> Result<(), Error>

Source§

async fn rename(&self, from: &Path, to: &Path) -> Result<(), Error>

Source§

async fn copy_permissions(&self, from: &Path, to: &Path) -> Result<(), Error>

Source§

async fn set_executable( &self, path: &Path, executable: bool, ) -> Result<(), Error>

Source§

fn capabilities(&self) -> Capabilities

Source§

async fn sync(&self, path: &Path, need: Durability) -> Result<(), Error>

Source§

async fn replace(&self, path: &Path, contents: &[u8]) -> Result<(), Error>

Source§

async fn write_atomic(&self, path: &Path, contents: &[u8]) -> Result<(), Error>

Source§

impl<S> Storage for Arc<S>
where S: Storage + ?Sized,

Source§

async fn write(&self, path: &Path, contents: &[u8]) -> Result<(), Error>

Source§

async fn create_new(&self, path: &Path, contents: &[u8]) -> Result<(), Error>

Source§

async fn create_dir_all(&self, path: &Path) -> Result<(), Error>

Source§

async fn remove_file(&self, path: &Path) -> Result<(), Error>

Source§

async fn remove_dir_all(&self, path: &Path) -> Result<(), Error>

Source§

async fn rename(&self, from: &Path, to: &Path) -> Result<(), Error>

Source§

async fn copy_permissions(&self, from: &Path, to: &Path) -> Result<(), Error>

Source§

async fn set_executable( &self, path: &Path, executable: bool, ) -> Result<(), Error>

Source§

fn capabilities(&self) -> Capabilities

Source§

async fn sync(&self, path: &Path, need: Durability) -> Result<(), Error>

Source§

async fn replace(&self, path: &Path, contents: &[u8]) -> Result<(), Error>

Source§

async fn write_atomic(&self, path: &Path, contents: &[u8]) -> Result<(), Error>

Implementors§