Creating Filestash plugins in Rust
Filestash is built around plugins. Its core interfaces let you customise how the file manager works, from storage and authentication to search, thumbnails, and more.
We’ve recently added WebAssembly support to extend the server through runtime plugins and enforce strict security boundaries with explicit filesystem and network permissions. This opens the door to languages beyond Go, and in this post we’ll focus on Rust.
So what does a Filestash plugin look like in Rust? The examples below were taken straight from our cookbook on GitHub. They all follow the same pattern: implement the interface you need, expose it with register!, compile it, and reference the generated WASM file in manifest.json.
Creating HTTP Endpoints
Let’s start by registering a very simple HTML page at /api/example:
use filestash::*;
#[derive(Default)]
pub struct Page;
impl Http for Page {
fn routes(r: &mut Router<Self>) {
r.get("/api/example", &["index_headers"], Page::index);
}
}
impl Page {
fn index(&self, _ctx: &impl Context, _req: &impl Request, res: &mut impl Response) {
res.header("Content-Type", "text/html");
res.write(b"<h1>Hello from a Filestash plugin</h1>");
}
}
register!(Page: Http);
Authorisation
As the name implies, this restricts the operations we allow. In our example, we allow a read-only view and deny any access to paths containing a top_secret folder:
use filestash::*;
#[derive(Default)]
pub struct Plugin;
impl Authorisation for Plugin {
fn ls(&self, _ctx: &impl Context, path: &str) -> Decision {
self.check(path)
}
fn cat(&self, _ctx: &impl Context, path: &str) -> Decision {
self.check(path)
}
fn stat(&self, _ctx: &impl Context, path: &str) -> Decision {
self.check(path)
}
}
impl Plugin {
fn check(&self, path: &str) -> Decision {
if path.split("/").any(|segment| segment == "top_secret") {
log::warn!("[TOPSECRET] access denied !!");
return Decision::Deny;
}
Decision::Allow
}
}
register!(Plugin: Authorisation);
Authentication
This very simple example grants anyone access to your storage by signing them in as anonymous:
use filestash::*;
use std::collections::HashMap;
#[derive(Default)]
pub struct Plugin;
impl Authentication for Plugin {
fn setup() -> Vec<Form> {
vec![
Form {
label: String::from("type"),
kind: String::from("hidden"),
value: String::from("test"),
..Default::default()
},
Form {
label: String::from("banner"),
kind: String::from("text"),
value: String::from("An authentication plugin that always says yes"),
..Default::default()
},
]
}
fn entrypoint(_idp: HashMap<String, String>, _req: &impl Request, res: &mut impl Response) -> Result<(), Error> {
res.header("Content-Type", "text/html");
res.write(b"<form method=\"post\"><button>CONNECT</button></form>");
Ok(())
}
fn callback(_form: HashMap<String, String>, _idp: HashMap<String, String>, _resp: &mut impl Response) -> Result<HashMap<String, String>, Error> {
let mut h = HashMap::new();
h.insert("username".to_string(), "anonymous".to_string());
Ok(h)
}
}
register!(Plugin: Authentication);
Lifecycle
Want to react when the server boots up, exits, or the config gets updated? Simple:
use filestash::*;
#[derive(Default)]
pub struct Plugin;
impl Lifecycle for Plugin {
fn on_init(&self) {
log::info!("[runtime::plugin::oninit] server is ready");
}
fn on_changes(&self) {
log::info!("[runtime::plugin::onchange] config has changed");
}
fn on_destroy(&self) {
log::info!("[runtime::plugin::onquit] server is shutting down");
}
}
register!(Plugin: OnInit + OnChanges + OnDestroy);
Conclusion
We have assembled a runtime plugin cookbook as a starting point for each interface currently exposed to the WebAssembly engine. The repo will keep expanding as runtime plugins gain more capabilities over time.