simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit a0d7b85a3154cf137f810b6f774e4324298b8fed
parent 866227f55e4edfc11bea767e83ef15c37d3df14e
Author: Silas Brack <silasbrack@gmail.com>
Date:   Tue, 21 Apr 2026 22:01:54 +0200

fix: embed static files with rust-embed

Serves pre-compressed .gz files with proper content-encoding header.
Single binary, no working directory dependency.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
MCargo.lock | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
MCargo.toml | 4+++-
Msrc/routes.rs | 39+++++++++++++++++++++++++++++++++++++--
3 files changed, 104 insertions(+), 16 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -734,12 +734,6 @@ dependencies = [ ] [[package]] -name = "http-range-header" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" - -[[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1363,6 +1357,40 @@ dependencies = [ ] [[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1375,6 +1403,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1519,6 +1556,8 @@ dependencies = [ "chrono", "dotenvy", "futures", + "mime_guess", + "rust-embed", "serde", "sqlx", "thiserror", @@ -1947,15 +1986,8 @@ dependencies = [ "bitflags", "bytes", "futures-core", - "futures-util", "http", "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", "pin-project-lite", "tokio", "tokio-util", @@ -2114,6 +2146,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2181,6 +2223,15 @@ dependencies = [ ] [[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -9,7 +9,9 @@ askama = { version = "0.12", features = ["with-axum"] } askama_axum = "0.4" sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono"] } tokio = { version = "1", features = ["full"] } -tower-http = { version = "0.6", features = ["fs", "trace", "compression-gzip"] } +tower-http = { version = "0.6", features = ["trace", "compression-gzip"] } +rust-embed = "8" +mime_guess = "2" chrono = { version = "0.4", features = ["serde"] } serde = { version = "1", features = ["derive"] } dotenvy = "0.15" diff --git a/src/routes.rs b/src/routes.rs @@ -1,12 +1,47 @@ use axum::{ Router, + body::Body, + extract::Path, + http::{StatusCode, header}, + response::{IntoResponse, Response}, routing::{get, post}, }; -use tower_http::services::ServeDir; +use rust_embed::Embed; use crate::handlers; use crate::state::AppState; +#[derive(Embed)] +#[folder = "static/"] +struct Assets; + +async fn static_handler(Path(path): Path<String>) -> Response { + // Try .gz version first if it exists + let gz_path = format!("{}.gz", path); + if let Some(file) = Assets::get(&gz_path) { + let mime = mime_guess::from_path(&path).first_or_octet_stream(); + return Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, mime.as_ref()) + .header(header::CONTENT_ENCODING, "gzip") + .body(Body::from(file.data.into_owned())) + .unwrap(); + } + + // Fall back to uncompressed + match Assets::get(&path) { + Some(file) => { + let mime = mime_guess::from_path(&path).first_or_octet_stream(); + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, mime.as_ref()) + .body(Body::from(file.data.into_owned())) + .unwrap() + } + None => StatusCode::NOT_FOUND.into_response(), + } +} + pub fn create_router(state: AppState) -> Router { Router::new() .route("/", get(handlers::show_home_page)) @@ -14,6 +49,6 @@ pub fn create_router(state: AppState) -> Router { .route("/search", post(handlers::search)) .route("/settings", get(handlers::open_settings)) .route("/settings/tab", get(handlers::open_settings_tab)) - .nest_service("/static", ServeDir::new("static").precompressed_gzip()) + .route("/static/{*path}", get(static_handler)) .with_state(state) }