diff --git a/examples/static-file-server/Cargo.toml b/examples/static-file-server/Cargo.toml index 7f17eca6..be33a847 100644 --- a/examples/static-file-server/Cargo.toml +++ b/examples/static-file-server/Cargo.toml @@ -8,6 +8,7 @@ publish = false axum = { path = "../../axum" } axum-extra = { path = "../../axum-extra", features = ["spa"] } tokio = { version = "1.0", features = ["full"] } +tower = { version = "0.4", features = ["util"] } tower-http = { version = "0.3.0", features = ["fs", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/examples/static-file-server/assets/index.html b/examples/static-file-server/assets/index.html new file mode 100644 index 00000000..7861254f --- /dev/null +++ b/examples/static-file-server/assets/index.html @@ -0,0 +1 @@ +Hi from index.html diff --git a/examples/static-file-server/assets/script.js b/examples/static-file-server/assets/script.js new file mode 100644 index 00000000..184dfcc9 --- /dev/null +++ b/examples/static-file-server/assets/script.js @@ -0,0 +1 @@ +console.log("Hello, World!"); diff --git a/examples/static-file-server/src/main.rs b/examples/static-file-server/src/main.rs index f7ac2bb9..72fc0c46 100644 --- a/examples/static-file-server/src/main.rs +++ b/examples/static-file-server/src/main.rs @@ -5,13 +5,19 @@ //! ``` use axum::{ - http::StatusCode, + body::Body, + http::{Request, StatusCode}, response::IntoResponse, routing::{get, get_service}, Router, }; +use axum_extra::routing::SpaRouter; use std::{io, net::SocketAddr}; -use tower_http::{services::ServeDir, trace::TraceLayer}; +use tower::ServiceExt; +use tower_http::{ + services::{ServeDir, ServeFile}, + trace::TraceLayer, +}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] @@ -24,27 +30,95 @@ async fn main() { .with(tracing_subscriber::fmt::layer()) .init(); + tokio::join!( + serve(using_spa_router(), 3000), + serve(using_serve_dir(), 3001), + serve(using_serve_dir_with_assets_fallback(), 3002), + serve(using_serve_dir_only_from_root_via_fallback(), 3003), + serve(two_serve_dirs(), 3004), + serve(calling_serve_dir_from_a_handler(), 3005), + ); +} + +fn using_spa_router() -> Router { // `SpaRouter` is the easiest way to serve assets at a nested route like `/assets` - // let app = Router::new() - // .route("/foo", get(|| async { "Hi from /foo" })) - // .merge(axum_extra::routing::SpaRouter::new("/assets", ".")) - // .layer(TraceLayer::new_for_http()); - - // for serving assets directly at the root you can use `tower_http::services::ServeDir` - // as the fallback to a `Router` - let app: _ = Router::new() + // + // Requests starting with `/assets` will be served from files in the current directory. + // Requests to unknown routes will get `index.html`. + Router::new() .route("/foo", get(|| async { "Hi from /foo" })) - .fallback_service(get_service(ServeDir::new(".")).handle_error(handle_error)) - .layer(TraceLayer::new_for_http()); + .merge(SpaRouter::new("/assets", "assets").index_file("index.html")) +} - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - tracing::debug!("listening on {}", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await - .unwrap(); +fn using_serve_dir() -> Router { + // `SpaRouter` is just a convenient wrapper around `ServeDir` + // + // You can use `ServeDir` directly to further customize your setup + let serve_dir = get_service(ServeDir::new("assets")).handle_error(handle_error); + + Router::new() + .route("/foo", get(|| async { "Hi from /foo" })) + .nest_service("/assets", serve_dir.clone()) + .fallback_service(serve_dir) +} + +fn using_serve_dir_with_assets_fallback() -> Router { + // for example `ServeDir` allows setting a fallback if an asset is not found + // so with this `GET /assets/doesnt-exist.jpg` will return `index.html` + // rather than a 404 + let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html")); + let serve_dir = get_service(serve_dir).handle_error(handle_error); + + Router::new() + .route("/foo", get(|| async { "Hi from /foo" })) + .nest_service("/assets", serve_dir.clone()) + .fallback_service(serve_dir) +} + +fn using_serve_dir_only_from_root_via_fallback() -> Router { + // you can also serve the assets directly from the root (not nested under `/assets`) + // by only setting a `ServeDir` as the fallback + let serve_dir = ServeDir::new("assets").not_found_service(ServeFile::new("assets/index.html")); + let serve_dir = get_service(serve_dir).handle_error(handle_error); + + Router::new() + .route("/foo", get(|| async { "Hi from /foo" })) + .fallback_service(serve_dir) +} + +fn two_serve_dirs() -> Router { + // you can also have two `ServeDir`s nested at different paths + let serve_dir_from_assets = get_service(ServeDir::new("assets")).handle_error(handle_error); + let serve_dir_from_dist = get_service(ServeDir::new("dist")).handle_error(handle_error); + + Router::new() + .nest_service("/assets", serve_dir_from_assets) + .nest_service("/dist", serve_dir_from_dist) +} + +#[allow(clippy::let_and_return)] +fn calling_serve_dir_from_a_handler() -> Router { + // via `tower::Service::call`, or more conveniently `tower::ServiceExt::oneshot` you can + // call `ServeDir` yourself from a handler + Router::new().nest_service( + "/foo", + get(|request: Request| async { + let service = get_service(ServeDir::new("assets")).handle_error(handle_error); + let result = service.oneshot(request).await; + result + }), + ) } async fn handle_error(_err: io::Error) -> impl IntoResponse { (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...") } + +async fn serve(app: Router, port: u16) { + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + tracing::debug!("listening on {}", addr); + axum::Server::bind(&addr) + .serve(app.layer(TraceLayer::new_for_http()).into_make_service()) + .await + .unwrap(); +}