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§
Sourcefn write(
&self,
path: &Path,
contents: &[u8],
) -> impl Future<Output = Result<(), Error>>
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.
Sourcefn create_dir_all(&self, path: &Path) -> impl Future<Output = Result<(), Error>>
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.
Sourcefn remove_file(&self, path: &Path) -> impl Future<Output = Result<(), Error>>
fn remove_file(&self, path: &Path) -> impl Future<Output = Result<(), Error>>
Remove a regular file. Mirrors std::fs::remove_file.
Sourcefn remove_dir_all(&self, path: &Path) -> impl Future<Output = Result<(), Error>>
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.
Sourcefn rename(
&self,
from: &Path,
to: &Path,
) -> impl Future<Output = Result<(), Error>>
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§
Sourcefn create_new(
&self,
path: &Path,
contents: &[u8],
) -> impl Future<Output = Result<(), Error>>
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.
Sourcefn copy_permissions(
&self,
from: &Path,
to: &Path,
) -> impl Future<Output = Result<(), Error>>
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.
Sourcefn set_executable(
&self,
path: &Path,
executable: bool,
) -> impl Future<Output = Result<(), Error>>
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.
Sourcefn set_link(
&self,
path: &Path,
target: &Path,
) -> impl Future<Output = Result<(), Error>>
fn set_link( &self, path: &Path, target: &Path, ) -> impl Future<Output = Result<(), Error>>
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.
Sourcefn capabilities(&self) -> Capabilities
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.
Sourcefn sync(
&self,
path: &Path,
need: Durability,
) -> impl Future<Output = Result<(), Error>>
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.
Sourcefn replace(
&self,
path: &Path,
contents: &[u8],
) -> impl Future<Output = Result<(), Error>>
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:
- write the bytes to a temporary sibling;
syncthat siblingOrdered, so the rename cannot be reordered ahead of the bytes it publishes;copy_permissionsfrom the target onto that sibling, so the replacement carries the target’s access permissions rather than a fresh file’s defaults;renameit 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).
Sourcefn write_atomic(
&self,
path: &Path,
contents: &[u8],
) -> impl Future<Output = Result<(), Error>>
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".