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:
storecanonicalizes the parent directory and rejects writes whose parent isn’tstarts_with(root). Closes the obvious “shard pre-created as a symlink before any write” case but not the post-canonicalize swap.storefalls back on rename failure to reading the existing file and verifying its content hash against the expectedBlobRef. 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
impl FileSystemAdapter
Sourcepub fn new(id: impl Into<String>, root: impl Into<PathBuf>) -> Self
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.
Sourcepub fn with_concurrency(self, cap: usize) -> Self
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
impl BlobAdapter for FileSystemAdapter
Source§fn adapter_id(&self) -> &str
fn adapter_id(&self) -> &str
Source§fn accepted_schemes(&self) -> &[&str]
fn accepted_schemes(&self) -> &[&str]
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,
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,
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,
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,
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 moreSource§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,
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,
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 moreSource§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,
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,
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,
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,
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 moreSource§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,
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,
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 moreSource§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,
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,
net blob stat CLI + the metrics exporters; surfaces size,
replica counts (where the adapter knows), encoding, etc. Read moreSource§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,
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,
Vec<u8> and forwards to Self::store;
adapters with real streaming write paths (S3 multipart
upload, chunked filesystem write) should override. Read moreSource§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,
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,
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 moreSource§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,
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,
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 moreSource§fn supports_list(&self) -> bool
fn supports_list(&self) -> bool
Self::list returns an authoritative enumeration. Read moreSource§impl Clone for FileSystemAdapter
impl Clone for FileSystemAdapter
Source§fn clone(&self) -> FileSystemAdapter
fn clone(&self) -> FileSystemAdapter
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more