Serve static content with axum
One of Rust’s nice properties is producing statically linked binaries making
deployment simple and straightforward. In some cases this is not enough and
additional data is required for proper function, for example static data for web
servers. With dependencies such as include_dir
and mime_guess
this is a
piece of cake to integrate into axum
though.
Using include_dir
we first declare variable that represents the data currently
located in the static
directory:
use include_dir::{include_dir, Dir};
static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static");
Now we define the static data route, passing *path
to denote we want to match
the entire remaining path.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = axum::Router::new()
.route("/static/*path", get(static_path));
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
Ok(())
}
Note that we cannot use the newly added typed path functionality in
axum-extra
. Now onto the actual route handler:
async fn static_path(Path(path): Path<String>) -> impl IntoResponse {
let path = path.trim_start_matches('/');
let mime_type = mime_guess::from_path(path).first_or_text_plain();
match STATIC_DIR.get_file(path) {
None => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(body::boxed(Empty::new()))
.unwrap(),
Some(file) => Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
HeaderValue::from_str(mime_type.as_ref()).unwrap(),
)
.body(body::boxed(Full::from(file.contents())))
.unwrap(),
}
}
As you can see we first strip the initial slash and then use the mime_guess
crate to guess a MIME type from it. If we are not able to do so, just assume
text/plain
however wrong that is. Then we try to locate the file path and
either return a 404 or a 200 with the actual file contents. Easy as pie.