Write a custom asset source¶
The three built-in layers — embedded, directory, in-memory — cover the
cases a CLI tool usually has. When yours is different (an in-process
archive, a remote-fetched theme pack, files synthesised at startup),
implement AssetSource and register it with .source().
Implement the trait¶
Three methods, no associated types, no error channel:
use std::sync::Arc;
use rtb_assets::{AssetSource, Assets};
/// Serves one generated file and nothing else.
struct GeneratedSource {
path: String,
bytes: Vec<u8>,
}
impl AssetSource for GeneratedSource {
fn read(&self, path: &str) -> Option<Vec<u8>> {
(path == self.path).then(|| self.bytes.clone())
}
fn list(&self, dir: &str) -> Vec<String> {
let prefix = if dir.is_empty() || dir == "." {
String::new()
} else if dir.ends_with('/') {
dir.to_string()
} else {
format!("{dir}/")
};
let Some(rest) = self.path.strip_prefix(prefix.as_str()) else {
return Vec::new();
};
if rest.is_empty() {
return Vec::new();
}
// Immediate child only: keep the segment before the first '/'.
let head = rest.find('/').map_or(rest, |i| &rest[..i]);
vec![head.to_string()]
}
fn name(&self) -> &str {
"generated"
}
}
Register it lowest-priority-first like any other layer:
let source = Arc::new(GeneratedSource {
path: "build/version.txt".to_string(),
bytes: b"0.6.3".to_vec(),
});
let assets = Assets::builder().source(source).build();
assets.open_text("build/version.txt"); // Ok("0.6.3")
assets.list_dir("build"); // ["version.txt"]
assets.list_dir(""); // ["build"]
Get list right — it is the easy one to get wrong¶
list returns the immediate children of a directory, with the
directory prefix stripped. Three details the built-in sources all
handle, and a hand-written source usually forgets:
""and"."both mean the root.- A trailing slash is optional:
"d"and"d/"are the same request. - Deeper paths contribute their first segment only. A source holding
build/version.txtmust return["build"]for the root and["version.txt"]forbuild, not the full path in either case.
The prefix-and-head-segment block above is the pattern the built-in sources use; copying it is the fastest way to be consistent with them.
Satisfy the trait's bounds¶
AssetSource requires Send + Sync + 'static. Assets stores every
layer as Arc<dyn AssetSource> and is itself Send + Sync, so a
source holding an Rc, a RefCell, or a borrowed lifetime will not
compile. Interior mutability needs Mutex or RwLock.
There is no &mut self anywhere on the trait. A source that needs to
mutate — a cache, a lazily-populated map — does it behind a lock.
Follow the three rules of a well-behaved layer¶
Never panic, and never block for long. read runs on the caller's
thread, and a merge across several layers calls it several times in
sequence. A source that reaches the network will stall every one of
those calls.
Return None for anything you cannot serve. The trait has no error
channel by design: None means "not here", and the next layer down
gets its turn. Swallowing your own errors as None is the correct
behaviour, not a compromise — but it does mean you must handle and log
them yourself if anyone is to know.
Make list agree with read. A name returned by list should
either be readable as dir/name or be a directory. A listing that
promises files which cannot be read is the one inconsistency the
overlay cannot paper over, because callers iterate listings and read
what they find.
Give it a meaningful name¶
name is diagnostic-only — nothing looks a layer up by it — but it is
what appears in a YAML or JSON parse error and in the Debug output of
Assets:
That line is the first thing anyone debugging precedence will look at. An empty string is allowed and unhelpful.
Caching a remote layer¶
If the layer's content comes from somewhere slow, fetch it before
building the overlay and serve from memory afterwards. Two reasons: a
read inside a merge has no way to signal "try again later", and a
failed fetch mid-run would make the overlay's answers change from one
call to the next, which nothing in the crate expects.
If the content must be refreshed, refresh it into a new source, build a
new Assets, and swap it in. Construction copies no bytes.
Related¶
- Asset source reference — the trait's full contract and the built-in sources' behaviour.
- How layer precedence works — where your layer sits in the stack.