Skip to main content

Fs

Struct Fs 

Source
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>

Source

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.

Source

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.

Source

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.

Source

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 off

Use 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.

Source

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.

Source

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.

Source

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.

Source

pub fn tag(&self) -> u8

The tag this handle last used, to hand back to whatever owns the channel’s counter.

Source

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).

Source

pub fn list_dir<F>( &mut self, path: impl AsRef<[u8]>, f: F, ) -> Result<Listing, Error>
where F: FnMut(DirEntry<'_>) -> bool,

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 with complete: 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.
Source

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.

Source

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.

Source

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.

Source

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 (rights did not include cap::READ).
  • Error::Failed - no such file, a directory, a path longer than fs’s 96-byte open table allows, too many files open, or fs replied without a capability. fs sends this one WITHOUT a reason, so reason is empty and the cause is in fs’s log only. Retrying an open is safe.
Source

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.

Source

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.

Source

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.

Source

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 - with reason saying why: “not found” for an absent path (NOT Error::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.
Source

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.

Source

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.

Source

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.

Source

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/fs answers every read failure with FS_NOTFOUND, so a damaged file reads as an absent one. stat tells 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.
Source

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 for read_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 from stat.
  • A read is idempotent, so every no-answer error here may safely be retried.
Source

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.)

Source

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.

Source

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”.

Source

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.

Source

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.

Source

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.

Source

pub fn exists(&mut self, path: impl AsRef<[u8]>) -> Result<bool, Error>

Does this path exist? A convenience over stat, and read-only.

Auto Trait Implementations§

§

impl<'a> Freeze for Fs<'a>

§

impl<'a> !RefUnwindSafe for Fs<'a>

§

impl<'a> !Send for Fs<'a>

§

impl<'a> !Sync for Fs<'a>

§

impl<'a> Unpin for Fs<'a>

§

impl<'a> UnsafeUnpin for Fs<'a>

§

impl<'a> !UnwindSafe for Fs<'a>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.