Skip to main content

FileSystemAdapter

Struct FileSystemAdapter 

Source
pub struct FileSystemAdapter { /* private fields */ }
Expand description

Filesystem-backed blob adapter. Content-addressed by BLAKE3 hash under a caller-supplied root directory.

§Threat model

The adapter assumes the configured root directory is writable only by the substrate process (and any process running with the same uid). Operators MUST enforce this contract via filesystem permissions — typically chown <daemon-user> <root> plus mode 0700 on Unix, or an equivalent ACL on Windows.

Cross-process write access inside root by a non-substrate user enables a symlink-swap window between the in-store canonicalize check and the rename(tmp, path) system call. An attacker who can pre-create or replace <root>/<shard>/ between those two operations can redirect the rename target outside the root.

In-code defenses are defense-in-depth, not a complete sandbox:

  • store canonicalizes the parent directory and rejects writes whose parent isn’t starts_with(root). Closes the obvious “shard pre-created as a symlink before any write” case but not the post-canonicalize swap.
  • store falls back on rename failure to reading the existing file and verifying its content hash against the expected BlobRef. Mitigates the case where a concurrent legitimate writer landed first but not the case where an attacker swaps the parent under us.

If a deployment ever needs to host the root in a shared-scratch environment, adopt platform-specific path-confinement primitives (Linux openat2 with RESOLVE_BENEATH, Windows FILE_FLAG_OPEN_REPARSE_POINT) behind a feature flag rather than relying on the documented exclusive-ownership contract.

Implementations§

Source§

impl FileSystemAdapter

Source

pub fn new(id: impl Into<String>, root: impl Into<PathBuf>) -> Self

Construct an adapter rooted at root. The directory is created on the first store if absent; fetch against an unprepared root surfaces BlobError::NotFound. Concurrency defaults to DEFAULT_FS_ADAPTER_CONCURRENCY; override via Self::with_concurrency.

Source

pub fn with_concurrency(self, cap: usize) -> Self

Override the per-adapter spawn_blocking concurrency cap. Floor 1 — zero would deadlock the adapter.

Trait Implementations§

Source§

impl BlobAdapter for FileSystemAdapter

Source§

fn adapter_id(&self) -> &str

Stable identifier for this adapter instance. The registry rejects re-registrations with the same id.
Source§

fn accepted_schemes(&self) -> &[&str]

URI schemes this adapter accepts on inbound BlobRefs. The substrate’s blob-dispatch layer routes by channel- configured blob_adapter_id; before invoking the adapter it checks the inbound URI’s scheme against this list and rejects with BlobError::UnsupportedScheme when the URI scheme isn’t accepted. Default returns an empty slice, which means “accept anything” — adapters in trusted / single-tenant deployments may leave this as-is, but adapters that have authority over a privileged backend (FS adapter, host-side keys, etc.) should override and list the schemes they actually serve so a publisher with append rights cannot dictate arbitrary URIs the adapter then resolves.
Source§

fn store<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, blob_ref: &'life1 BlobRef, bytes: &'life2 [u8], ) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Persist bytes at the URI carried in blob_ref. Most adapters will derive the URI from blob_ref.hash (content- addressing) and ignore the inbound URI; some (e.g. FileSystemAdapter) honor it directly. The hash on blob_ref is the source of truth — the substrate computes it before calling this method.
Source§

fn fetch<'life0, 'life1, 'async_trait>( &'life0 self, blob_ref: &'life1 BlobRef, ) -> Pin<Box<dyn Future<Output = Result<Bytes, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Fetch the full content at blob_ref.uri. The substrate runs BlobRef::verify on the returned bytes; on a mismatch the call as a whole fails with BlobError::HashMismatch. Read more
Source§

fn fetch_range<'life0, 'life1, 'async_trait>( &'life0 self, blob_ref: &'life1 BlobRef, range: Range<u64>, ) -> Pin<Box<dyn Future<Output = Result<Bytes, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Fetch a byte range. range.start <= range.end and both bounded by blob_ref.size; out-of-range queries surface as BlobError::Backend from the adapter. The substrate does NOT verify partial fetches against the full-content hash; callers using range fetch are accepting that trade-off. Read more
Source§

fn exists<'life0, 'life1, 'async_trait>( &'life0 self, blob_ref: &'life1 BlobRef, ) -> Pin<Box<dyn Future<Output = Result<bool, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Probe for existence without fetching. Adapters that cannot answer cheaply may emulate by fetch + drop; the trait makes no efficiency promise.
Source§

fn fetch_stream<'life0, 'life1, 'async_trait>( &'life0 self, blob_ref: &'life1 BlobRef, ) -> Pin<Box<dyn Future<Output = Result<BlobByteStream, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Stream the blob content as a sequence of byte chunks. Default impl routes through Self::fetch and emits the whole payload as a single chunk — fine for adapters that hold blobs in RAM or pull them in one shot anyway (S3 GetObject with no Range, IPFS). Adapters with real streaming backends (chunked HTTP, mmap’d local files, range-fetched S3) should override to yield progressively. Read more
Source§

fn delete<'life0, 'life1, 'async_trait>( &'life0 self, blob_ref: &'life1 BlobRef, ) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Best-effort delete. The substrate calls this on the GC sweep path (v0.2 MeshBlobAdapter); external-storage adapters (S3 / IPFS) typically defer durability decisions to the backend’s own lifecycle policies and may treat this as a no-op. Read more
Source§

fn stat<'life0, 'life1, 'async_trait>( &'life0 self, blob_ref: &'life1 BlobRef, ) -> Pin<Box<dyn Future<Output = Result<BlobStat, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Return an operational snapshot of the blob. Used by the net blob stat CLI + the metrics exporters; surfaces size, replica counts (where the adapter knows), encoding, etc. Read more
Source§

fn store_stream<'life0, 'life1, 'async_trait>( &'life0 self, blob_ref: &'life1 BlobRef, stream: BlobByteStream, size_hint: Option<u64>, ) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Store from a stream of byte chunks. Default impl drains the stream into a Vec<u8> and forwards to Self::store; adapters with real streaming write paths (S3 multipart upload, chunked filesystem write) should override. Read more
Source§

fn prefetch<'life0, 'life1, 'async_trait>( &'life0 self, _blob_ref: &'life1 BlobRef, ) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Hint to the adapter that blob_ref’s bytes will likely be fetched soon — kick off any background pre-population (cross-node replication, prefetch from cold storage, warm-cache load) without blocking on completion. The returned Ok(()) means “the prefetch was initiated”, not “the bytes are now local”. Read more
Source§

fn list<'life0, 'life1, 'async_trait>( &'life0 self, _opts: &'life1 BlobListOptions, ) -> Pin<Box<dyn Future<Output = Result<Vec<BlobInventoryEntry>, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Enumerate blob chunks the adapter has observed. Powers the operator-facing “Blob & Artifact Explorer” surface (DECK_PLAN.md § Deferred work § Blob & Artifact Explorer) — adapters that can cheaply enumerate (Mesh, fs) override; adapters with prohibitive enumeration cost (S3 with millions of keys, IPFS) leave the default “empty” so consumers don’t accidentally rack up backend charges. Read more
Source§

fn supports_list(&self) -> bool

Whether Self::list returns an authoritative enumeration. Read more
Source§

impl Clone for FileSystemAdapter

Source§

fn clone(&self) -> FileSystemAdapter

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for FileSystemAdapter

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

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> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
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> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
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.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more