Jan 03, 2025
5 min read
Rust,

Embedding Static Assets in Rust: Achieving Single Binary Distribution

Achieve single binary distribution of your application

In practical development, referencing static assets is often a tricky issue. By default, Rust does not package these resources into the binary file during compilation. This means that without additional processing, we need to carry these resources separately when distributing the program to ensure its normal operation. For web applications, this typically includes template files or front-end build products. Ideally, we want to deploy with just one binary package containing everything necessary.

Golang provides an official solution for this. Fortunately, Rust can also embed text or byte data simply using macros like include_str!() or include_bytes!(). However, when dealing with more complex directory structures, such as those produced by front-end build tools like Vite, which generate the following file tree:


❯ tree dist
dist
├── assets
│   ├── index-9rKZVvFu.js
│   ├── index-BKGZKLCa.css
│   ├── index-CDBrVPCG.js
│   ├── index-DsnN8gUL.js
│   └── settings-CfrUB0_5.js
├── index.html
├── icon.svg
└── vite.svg

To simplify this process, we can use the rust-embed crate to embed the entire directory structure.

Embedding Complex Directory Structures Using rust-embed

Adding Dependencies

First, add rust-embed as a dependency in your Cargo.toml file:

toml
[dependencies]
rust-embed = "8.4.0"

Defining a Struct

Next, define a struct and use attribute macros to specify the directory to be embedded. The directory should be relative to the project root:

rust
use rust_embed::RustEmbed;

#[derive(RustEmbed)]
#[folder = "dist/"]
struct Asset;

If you need to selectively include or exclude certain files, you can do so as follows:

rust
#[derive(RustEmbed)]
#[folder = "dist/"]
#[include = "*.html"]
#[include = "images/*"]
#[exclude = "*.txt"]
struct Asset;

Accessing File Content

Now, you can directly use the Asset::get method to access file content. For example, to get the content of index.html:

rust
let index_html = Asset::get("index.html").unwrap();

Additionally, rust-embed provides iterator functionality to allow you to iterate over all embedded files:

rust
for file in Asset::iter() {
println!("{}", file.as_ref());
}

Frontend and Backend Routing Planning

To ensure that frontend assets and backend API routes do not conflict, we should clearly distinguish between them. A common practice is to treat all paths starting with /api/ as backend API routes, while other unspecified paths are treated as frontend routes.

In backend frameworks, we need to implement 404 error handling logic to correctly serve frontend assets. Some frameworks, such as actix-web and axum, provide dedicated interfaces for serving static files and global 404 responses.

Handling Frontend Routes

If your frontend project (like Vue or React) uses frontend routing, you need to mimic Nginx-like proxy behavior in the backend. For example, when using Vue Router, the Nginx configuration tries to match specific files or directories, and if it doesn’t find them, it falls back to index.html, letting the frontend router take over.

The same logic can be implemented in a Rust backend framework:

rust
async fn handler_404(uri: &Uri) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/').to_string();

    // If the path starts with "dist/", remove the prefix
    if path.starts_with("dist/") {
        let new_path = path.replace("dist/", "");
        // If the path is empty, default to index.html
        let path = if new_path.is_empty() { "index.html" } else { &new_path };

        StaticFile(path)
    } else {
        // For non-frontend routes, return 404 or a custom response
        // Here we assume all frontend routes are handled by index.html
        StaticFile("index.html")
    }
}

Static File Handler

Finally, define a static file handler that looks up the corresponding embedded resource based on the request path and sets the correct MIME type before returning it to the client:

rust
use axum::{
body::Body,
http::{header, HeaderValue, Response, StatusCode, Uri},
response::IntoResponse,
};
use mime_guess::Mime;

pub struct StaticFile<T>(pub T);

impl<T> IntoResponse for StaticFile<T>
where
T: Into<String>,
{
fn into_response(self) -> Response<Body> {
let path = self.0.into();
// Directly check if the path file or directory exists, since the entry point is only index.html, consider only files here.
match Asset::get(&path) {
Some(content) => {
// Return file content, note that the Content-Type should be set according to different file types.
let mime = mime_guess::from_path(&path).first_or_octet_stream();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap())
.body(Body::from(content.data))
.unwrap()
}
None => {
// If not found, assume this route is a frontend route, default to index.html
// Frontend code will handle the routing automatically; we just need to point to index.html
let content = Asset::get("index.html").unwrap();
let mime = mime_guess::from_path("index.html").first_or_octet_stream();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap())
.body(Body::from(content.data))
.unwrap()
}
}
}
}

By doing this, we not only achieve seamless embedding of static resources but also ensure harmonious coexistence of frontend and backend routes, making the application easier to deploy and maintain.