pub struct Fs<'a> { /* private fields */ }Expand description
A handle to the filesystem service.
§Authority
Constructed from a &ServiceContext, and carries no authority of its own. Holding one does not
grant filesystem access: every call goes through the context’s existing send capability for
fs, and a task whose spawn request granted none gets Error::Unreachable, exactly as it
would for any other peer it cannot reach. This handle makes the filesystem convenient to use,
never available where it was not granted.
§Why it owns a counter
The request tag has to differ between consecutive requests, and it may not live in a static:
unowned global mutable state is forbidden (invariant 9), and the shell’s own tag counter was
moved out of a static AtomicU8 for exactly that reason (audit finding C6-1). So the counter
lives here, owned by the handle, which is also why this type takes &mut self on operations
that send.
§A missing path is not always Error::NotFound
services/fs answers FS_NOTFOUND only from its READS - stat, read and list. Every operation
that changes something (write, create, delete, rename, move, seal, open) reports an absent path
as FS_ERR with a reason such as “not found”, “path not found” or “source not found”, which this
library returns as Error::Failed with that text in reason - except
open, whose FS_ERR carries no reason at all. Ask
exists first where the difference matters.
Implementations§
Source§impl<'a> Fs<'a>
impl<'a> Fs<'a>
Sourcepub fn new(ctx: &'a ServiceContext) -> Fs<'a>
pub fn new(ctx: &'a ServiceContext) -> Fs<'a>
Take a filesystem handle from a context.
Cheap, allocates nothing, and grants nothing. See the type’s authority note.
Sourcepub fn with_notice(ctx: &'a ServiceContext, notice: &'a dyn Fn()) -> Fs<'a>
pub fn with_notice(ctx: &'a ServiceContext, notice: &'a dyn Fn()) -> Fs<'a>
A handle that calls notice when a request has been waiting a while.
The callback takes nothing and returns nothing, and that is the whole of its contract: it
means only “you have been waiting”. It cannot cancel, cannot inspect the request, and
the library learns nothing about consoles from it - the shell prints [q] quit there, and
something headless prints nothing at all.
Use this for anything a person is waiting at. A filesystem call can block on a device
that is settling after a replug, and a command that goes quiet for twenty seconds with no
way out is one the operator has to reboot out of (utilities/0_conventions.md rule 9). A
request the operator then aborts returns Error::Cancelled, which is NOT a fault - report
it as the deliberate act it was.
Sourcepub fn patience_secs(self, secs: i64) -> Fs<'a>
pub fn patience_secs(self, secs: i64) -> Fs<'a>
Wait secs for an ordinary operation instead of call::DEFAULT_SECS.
This is the CALLER’s patience, not a promise about the service. Use it where waiting the
default would be worse than giving up: a shell loading its history at startup must not sit on
a prompt because fs respawned and is still re-mounting, and a background marker write is
not worth a long wait at all.
A short deadline does not make a failure smaller. Giving up on a WRITE leaves
Error::OutcomeUnknown, and shortening the wait makes that outcome MORE likely, not less -
so a caller that sets this must have something sensible to do with “it may have happened”.
The right use is a write nobody is waiting on; the wrong one is shortening a write somebody
will act on the result of.
Operations that carry their own budget because they do more work - delete_all
walks a subtree - are NOT shortened by this. A caller cannot set a deadline under the work.
Sourcepub fn from_tag(ctx: &'a ServiceContext, tag: u8) -> Fs<'a>
pub fn from_tag(ctx: &'a ServiceContext, tag: u8) -> Fs<'a>
Take a handle that CONTINUES an existing tag sequence for this channel.
For a caller that still makes some fs requests by hand: lend the handle your counter, and
take it back with tag when you are done.
let mut fs = Fs::from_tag(ctx, my_fs_tag.get());
let r = fs.list_dir("/data", |e| { .. })?;
my_fs_tag.set(fs.tag()); // hand-rolled calls resume where the handle left offUse this whenever anything else in the same task also talks to fs directly. Two
counters on one channel can mint the same tag for two in-flight requests, and the result is
not a loud rejection - it is a stale reply silently accepted as the answer to the current
question. See TAG_START.
Sourcepub fn noticing(self, notice: &'a dyn Fn()) -> Fs<'a>
pub fn noticing(self, notice: &'a dyn Fn()) -> Fs<'a>
Lend this handle a waiting-notice, as with_notice describes.
Separate from from_tag rather than a fourth constructor taking both,
because a caller that shares a tag counter usually also wants the notice and the
combinations multiply faster than the constructors are worth.
Sourcepub fn keeping(self, keep: &'a dyn Fn(u8)) -> Fs<'a>
pub fn keeping(self, keep: &'a dyn Fn(u8)) -> Fs<'a>
Hand every key a noticing wait reads, other than a quit key, to keep - in order - instead of
discarding it. A wait that watches the console for q takes what is typed; without this,
typing ahead while a command waits loses those keys (backlog/fixed/80 H16). Only matters with a
notice, since only a noticing wait reads the console.
Sourcepub fn watching(self, watch: &'a dyn Watch) -> Fs<'a>
pub fn watching(self, watch: &'a dyn Watch) -> Fs<'a>
Lend this handle the command’s watch (call::Watch): every request then shows the command’s
[q] quit once the COMMAND has run 2 s, and stops at the next request after a quit. Takes
precedence over a notice; a watch that is not active leaves the handle’s own notice, if any, else the
plain wait.
Sourcepub fn tag(&self) -> u8
pub fn tag(&self) -> u8
The tag this handle last used, to hand back to whatever owns the channel’s counter.
Sourcepub fn reason(&self) -> &str
pub fn reason(&self) -> &str
Why the last call failed, IN THE SERVICE’S OWN WORDS, or "" if it did not fail or said
nothing.
This is for reporting, never for control flow. Branch on the Error; print this.
It is free-text owned by services/fs and may be reworded at any time, so a caller that
matched on its content would break silently the next time somebody improved a message.
It exists because the service already sends it and this library used to discard it:
FS_ERR became Error::Failed and “name already exists” was lost between the two. An
operator reading fs: failed has to go to the service log for a sentence the reply was
already carrying, which makes a loud failure quieter for no reason (26.7).
Sourcepub fn list_dir<F>(
&mut self,
path: impl AsRef<[u8]>,
f: F,
) -> Result<Listing, Error>
pub fn list_dir<F>( &mut self, path: impl AsRef<[u8]>, f: F, ) -> Result<Listing, Error>
Visit every entry of a directory, in the order services/fs stores them.
f is called once per entry and returns true to continue or false to stop early.
let mut files = 0;
let r = fs.list_dir("/data", |e| { if !e.is_dir { files += 1; } true })?;
if !r.complete { io::println(ctx, " (listing truncated - there are more)"); }§This is where a hand-rolled version goes wrong
The reply is ONE PAGE: [count, more, next] plus the entries that fit in a block. A caller
that reads count entries and stops has silently listed a PREFIX of a large directory, and
nothing anywhere reports a problem. This follows next until more is clear, so the
closure sees the whole directory or the call returns an error - never a quiet partial.
Blocks, once per page. Authority: the caller’s existing fs capability.
§Errors
Error::NotFound- no such path, or it is a file rather than a directory. Returned only for the FIRST page: a later page that fails ends the walk withcomplete: false, because entries were already delivered and reporting “no such directory” for a read error part way through would name the wrong fault.Error::Malformed- the service claimed more entries without advancing. Listing is read-only, so every no-answer error here is safe to retry.
Sourcepub fn create_dir_all(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
pub fn create_dir_all(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
Create a directory and any missing parents, like mkdir -p.
Succeeds if the directory already exists, which is what makes it usable at start-up: a
service that ensures its own data directory should not have to care whether it is the first
to run. Use create_dir where the path’s absence is itself the thing being asserted.
Blocks up to call::DEFAULT_SECS, or this handle’s
patience_secs if it was given one. Authority: the caller’s existing fs capability.
§Errors
Error::Failed with reason naming the step that failed (a parent that is
a file, say). A no-answer error must NOT be blindly retried - see Error::retry_is_safe;
creating directories is idempotent, but the transaction may have committed unseen, so
re-issuing is only safe once you accept that it is a second attempt rather than the first.
Sourcepub fn move_to(
&mut self,
path: impl AsRef<[u8]>,
dest: impl AsRef<[u8]>,
) -> Result<(), Error>
pub fn move_to( &mut self, path: impl AsRef<[u8]>, dest: impl AsRef<[u8]>, ) -> Result<(), Error>
Move a file or directory to dest, ACROSS directories.
This is the different operation from rename, which changes a name within one
parent. dest is a full path, and services/fs refuses a move that would put a directory
inside itself - the tree stays a tree, and that rule is the service’s to enforce, not the
caller’s to remember.
Blocks up to call::DEFAULT_SECS, or this handle’s
patience_secs if it was given one. Authority: the caller’s existing fs capability.
§Errors
Error::Failed with reason - “source not found” for an absent source
(NOT Error::NotFound; see the type’s note), or the refusal (“name already exists”,
“cannot move root”, a destination inside the source). A move MUTATES - do not retry on a no-answer error without
first checking what happened.
Sourcepub fn delete_all(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
pub fn delete_all(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
Delete a file, or a directory AND EVERYTHING INSIDE IT.
The recursive one. delete removes a single file and refuses a non-empty
directory; this removes the subtree. The names differ deliberately - a caller reaching for
the destructive one should have to type something that says so.
Blocks up to SWEEP_SECS, not the ordinary deadline: removing a subtree walks it, and
a bound shorter than the work turns a slow success into Error::OutcomeUnknown - which is
worse than waiting, because an unknown outcome forbids the retry that would have fixed it.
(This doc previously said “up to DEFAULT_SECS, and longer for a large tree”, which cannot
happen: a call does not block past its own deadline, it gives up.)
Authority: the caller’s existing fs capability.
§Errors
Error::Failed with reason, “not found” included (NOT
Error::NotFound; see the type’s note). Never retry this on
Error::OutcomeUnknown: the service unlinks the subtree in one transaction and then
frees its blocks in many, so a deadline that passes mid-delete may leave the entry already
gone, and a retry then fails with “not found” for work that succeeded. A failure DURING the
freeing is also reported as Error::Failed although the unlink has committed; the blocks
it did not free are leaked until check rebuilds the bitmap.
Sourcepub fn open<'f>(
&'f mut self,
path: impl AsRef<[u8]>,
rights: u8,
) -> Result<File<'f, 'a>, Error>
pub fn open<'f>( &'f mut self, path: impl AsRef<[u8]>, rights: u8, ) -> Result<File<'f, 'a>, Error>
Open a file as a CAPABILITY (CLAUDE.md 7.10), rather than acting on it by path.
The returned File holds an unforgeable, revocable, non-escalating
kernel capability to exactly this file. A read-only one cannot write, and the refusal comes
from the KERNEL before fs is reached - which is the difference between a capability and a
handle a service merely agrees to honour.
rights is a mask of cap::READ, cap::WRITE and
cap::APPEND. Check
File::rights on the result: it is what the capability
carries, read from the kernel. fs narrows rather than refuses in one case - it will not mint
a writable capability to a SEALED file, and hands back a read-only one instead.
§Why this borrows the handle
The File and this Fs share one correlation-tag counter, because they talk to one service
over one endpoint. The borrow is what makes that structural rather than a rule to remember.
See File for how to hold two files at once.
Blocks up to call::DEFAULT_SECS, or this handle’s
patience_secs if it was given one. Authority: the caller’s existing fs capability
- opening a file grants nothing the contract did not already grant.
§Errors
Error::PermissionDenied- a writable capability was asked for on a sealed file and no read-only fallback was available (rightsdid not includecap::READ).Error::Failed- no such file, a directory, a path longer thanfs’s 96-byte open table allows, too many files open, orfsreplied without a capability.fssends this one WITHOUT a reason, soreasonis empty and the cause is infs’s log only. Retrying an open is safe.
Sourcepub fn check(&mut self) -> Result<Check, Error>
pub fn check(&mut self) -> Result<Check, Error>
Rebuild the free-space bitmap from the file tree, and report what was found.
This is fsck. It WRITES - the bitmap is rebuilt from the tree, which is the irreducible
source (26.4: the bitmap and free count are derived views, and this is their repair path).
A read-only mount refuses it.
Blocks up to SWEEP_SECS. It walks the whole volume.
§Errors
Error::Failed on a read-only mount or a failed sweep, with an EMPTY
reason (fs logs why but sends a bare FS_ERR);
Error::NoFilesystem if nothing is mounted, or Error::Unavailable if storage is
present but unreadable. Rebuilding is idempotent - it derives the
bitmap from the tree either way - so a repeat is safe, though it costs another full sweep.
Sourcepub fn drive(&mut self) -> Result<Drive, Error>
pub fn drive(&mut self) -> Result<Drive, Error>
What storage fs is serving: the device’s size, whether a volume is mounted, how full it is
and its label (OP_DRIVES_INFO, the request behind the shell’s drives). Changes nothing,
except that fs drops a mount whose device has gone - which is it noticing, not this asking.
§Errors
Error::Malformed on a reply too short to be one; the no-answer errors of every request.
Sourcepub fn scrub(&mut self) -> Result<Scrub, Error>
pub fn scrub(&mut self) -> Result<Scrub, Error>
Verify every referenced block’s CRC and report. Changes nothing.
The read-only twin of check: that one repairs the bitmap, this one reads the
data and says what does not verify. Safe on a read-only mount, and safe to run at any time.
Blocks up to SWEEP_SECS. It reads the whole volume.
§Errors
Error::NoFilesystem if nothing is mounted (Error::Unavailable if storage is present
but unreadable); Error::Failed, with no reason, if the sweep itself failed. A scrub
writes nothing, so every no-answer
error here is safe to retry.
Sourcepub fn seal(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
pub fn seal(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
Freeze a file’s content. Permanently.
A sealed file can never be written again - not by you, not by anything holding a capability
to it, not after a restart. fs will not even mint a writable capability for one
(open narrows to read-only rather than handing back a capability it could not
honour).
There is no unseal, by design: a seal a holder can lift is a request rather than a guarantee. If you are offering this to a person, ask them first - every other destructive operation here can be undone by doing the opposite, and this one cannot.
Blocks up to call::DEFAULT_SECS, or this handle’s
patience_secs if it was given one. Authority: the caller’s existing fs capability.
§Errors
Error::Failed- withreasonsaying why: “not found” for an absent path (NOTError::NotFound; see the type’s note), or “only a file can be sealed”, because a directory cannot be.- Sealing twice is harmless - the second call finds it already sealed - so a no-answer error here may be retried, unlike most mutations.
Sourcepub fn quiesce(&mut self) -> Result<bool, Error>
pub fn quiesce(&mut self) -> Result<bool, Error>
Quiesce the filesystem for a shutdown (docs/shutdown.md 4.3): fs finishes the request in hand,
flushes the device, and then refuses every write for ten seconds - so nothing changes between the
flush and the power going off, and a shutdown that never comes leaves a filesystem that takes
writes again by itself. Ok(true) when the device confirmed the flush, Ok(false) when it did not.
Blocks up to call::DEFAULT_SECS, or this handle’s patience_secs - a
shutdown gives it about one. Authority: the caller’s existing fs capability.
Sourcepub fn resume(&mut self) -> Result<(), Error>
pub fn resume(&mut self) -> Result<(), Error>
End a quiesce early, for a shutdown that was cancelled after its tidy-up: fs takes writes
again at once rather than refusing them for the rest of its ten seconds. Authority: the caller’s fs
capability.
Sourcepub fn stat(&mut self, path: impl AsRef<[u8]>) -> Result<Stat, Error>
pub fn stat(&mut self, path: impl AsRef<[u8]>) -> Result<Stat, Error>
Ask whether a path exists, and what it is.
Blocks up to call::DEFAULT_SECS, or this handle’s
patience_secs if it was given one. Authority: the caller’s existing fs capability.
§Errors
Error::NotFound if the path is absent. See Error for the no-answer cases; a stat is
read-only, so retrying any of them is safe.
Sourcepub fn read_at(
&mut self,
path: impl AsRef<[u8]>,
offset: u64,
buf: &mut [u8],
) -> Result<usize, Error>
pub fn read_at( &mut self, path: impl AsRef<[u8]>, offset: u64, buf: &mut [u8], ) -> Result<usize, Error>
Read at most buf.len() bytes from offset, and say how many arrived.
The positional read. Use it to move a file larger than any buffer, or to resume where a
previous pass stopped; use read_into when the whole file fits and you want
it in one call.
Returns 0 at end of file. One call moves at most IO_CHUNK bytes however large buf is,
so a caller wanting more must loop - and the loop is the caller’s, because only the caller
knows whether a short read means “done” or “keep going”.
Blocks up to call::DEFAULT_SECS, or this handle’s
patience_secs if it was given one. Authority: the caller’s existing fs capability.
§Errors
Error::NotFound- no such file, or it is a directory. Also a data block that failed its CRC:services/fsanswers every read failure withFS_NOTFOUND, so a damaged file reads as an absent one.stattells the two apart.- A read changes nothing, so every no-answer error here is safe to retry - which is what
makes a resumable copy possible at all. See
Error::retry_is_safe.
Sourcepub fn read_into(
&mut self,
path: impl AsRef<[u8]>,
buf: &mut [u8],
) -> Result<usize, Error>
pub fn read_into( &mut self, path: impl AsRef<[u8]>, buf: &mut [u8], ) -> Result<usize, Error>
Read a whole file into buf, returning how many bytes were written.
Streams in IO_CHUNK pieces, so a file far larger than one IPC message reads correctly.
Blocks, once per chunk. Authority: the caller’s existing fs capability.
§Errors
Error::NotFound- no such file, or it is a directory, or (as forread_at) a data block failed its CRC part way through.Error::BufferTooSmall- the file does not fit. Nothing is written; call again with room, having learned the size fromstat.- A read is idempotent, so every no-answer error here may safely be retried.
Sourcepub fn write(
&mut self,
path: impl AsRef<[u8]>,
data: &[u8],
) -> Result<(), Error>
pub fn write( &mut self, path: impl AsRef<[u8]>, data: &[u8], ) -> Result<(), Error>
Create or replace a file with data.
Blocks. Authority: the caller’s existing fs capability.
§This operation CHANGES STATE
If it returns Error::OutcomeUnknown, the write may have happened. Do not call it again
to “make sure”: read the file back, or report the uncertainty. Error::retry_is_safe
answers this for you, and says false for that case on purpose.
§Larger than one message
data longer than IO_CHUNK is written as the file at its whole size first
(create_sized), then filled chunk by chunk from offset 0. A failure
part way leaves a file of the full size whose tail is not yet written, and is reported.
(Until 2026-10-10 the file was created from its first chunk alone, sized for that chunk, so
services/fs refused the second as “write past extent” - backlog/fixed/80 G1.)
Sourcepub fn create_dir(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
pub fn create_dir(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
Create a directory. Fails if the parent does not exist, or if the name already does
(Error::Failed, reason “path not found” or “already exists”).
Blocks. Changes state: see the note on write about Error::OutcomeUnknown.
Sourcepub fn delete(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
pub fn delete(&mut self, path: impl AsRef<[u8]>) -> Result<(), Error>
Delete one file or one empty directory.
Blocks. Destructive, and not idempotent in the way that matters: on
Error::OutcomeUnknown the file may already be gone, and a second delete would fail
(Error::Failed, reason “not found”) for work that succeeded. Report the uncertainty; do
not re-send. A non-empty directory is refused with reason “directory not empty”.
Sourcepub fn create_sized(
&mut self,
path: impl AsRef<[u8]>,
capacity: u64,
) -> Result<(), Error>
pub fn create_sized( &mut self, path: impl AsRef<[u8]>, capacity: u64, ) -> Result<(), Error>
Allocate a file of capacity bytes without writing content into it.
The extent is reserved up front, so later write_at calls land in space that is already
the file’s. That is what makes a long append-style writer bounded: it cannot run out of room
halfway and leave a half-file behind.
An existing file at path is REPLACED: services/fs truncates it to a fresh extent of
this size (a sealed file is refused). A directory there is refused.
Blocks. Changes state: on Error::OutcomeUnknown the file may exist. Use
exists to find out rather than calling this again, which would succeed and
replace the file a second time, discarding anything written into it in between.
Added because services/recorder needed it during migration. It is a real filesystem
operation, so it belongs in the typed surface rather than behind an opcode escape hatch.
Sourcepub fn write_at(
&mut self,
path: impl AsRef<[u8]>,
offset: u64,
data: &[u8],
) -> Result<(), Error>
pub fn write_at( &mut self, path: impl AsRef<[u8]>, offset: u64, data: &[u8], ) -> Result<(), Error>
Write data at a byte offset into an existing file.
Blocks. Changes state. Unusually among the write operations, a positional write at a FIXED offset into an already-allocated extent is idempotent: repeating it puts the same bytes in the same place. It is safe to re-send because it is positional, not appending, which is what makes a chunked write resumable at all.
offset must be a multiple of 508 (one data block’s payload; IO_CHUNK is seven of
them), and offset + data.len() must lie inside the file’s existing extent - fs does not
grow a file here. Either violation is Error::Failed, reason “unaligned offset” or
“write past extent”. Size the file first with create_sized.
Even so this returns Error::OutcomeUnknown honestly on a timeout, because the DECISION to
re-send belongs to the caller who knows the offset is fixed - not to a library that would be
guessing.
Sourcepub fn rename(
&mut self,
path: impl AsRef<[u8]>,
new_name: impl AsRef<[u8]>,
) -> Result<(), Error>
pub fn rename( &mut self, path: impl AsRef<[u8]>, new_name: impl AsRef<[u8]>, ) -> Result<(), Error>
Rename a file within its directory. new_name is a bare name, not a path.
Blocks. Changes state, and is NOT idempotent: a second rename after a successful one
fails with Error::Failed, reason “entry not found”, because the source is already gone. On
Error::OutcomeUnknown check with exists rather than re-sending - this is precisely
the case where a retry reports failure for work that succeeded.