tower_serve_static/
lib.rs

1//! Tower file services embedding assets into the binary.
2//!
3//! # Serve Static File
4//!
5//! ```
6//! use tower_serve_static::{ServeFile, include_file};
7//!
8//! // File is located relative to `CARGO_MANIFEST_DIR` (the directory containing the manifest of your package).
9//! // This will embed and serve the `README.md` file.
10//! let service = ServeFile::new(include_file!("/README.md"));
11//!
12//! // Run our service using `axum`
13//! let app = axum::Router::new().nest_service("/", service);
14//!
15//! # async {
16//! // run our app with axum, listening locally on port 3000
17//! let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
18//! axum::serve(listener, app).await?;
19//! # Ok::<(), Box<dyn std::error::Error>>(())
20//! # };
21//! ```
22//!
23//! # Serve Static Directory
24//!
25//! ```
26//! use tower_serve_static::{ServeDir};
27//! use include_dir::{Dir, include_dir};
28//!
29//! // Use `$CARGO_MANIFEST_DIR` to make path relative to your package.
30//! // This will embed and serve files in the `src` directory and its subdirectories.
31//! static ASSETS_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/src");
32//! let service = ServeDir::new(&ASSETS_DIR);
33//!
34//! // Run our service using `axum`
35//! let app = axum::Router::new().nest_service("/", service);
36//!
37//! // run our app with axum, listening locally on port 3000
38//! # async {
39//! let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
40//! axum::serve(listener, app).await?;
41//! # Ok::<(), Box<dyn std::error::Error>>(())
42//! # };
43//! ```
44//!
45//! # Features
46//!
47//! This library exposes the following features that can be enabled:
48//!
49//! - `metadata` - enables [`ServeDir`] to include the `Last-Modified` header in the response headers.
50//!   Additionally, it enables responding with a suitable reply for `If-Modified-Since` conditional requests.
51
52#![deny(rust_2018_idioms, missing_docs)]
53
54#[macro_use]
55mod macros;
56
57mod serve_dir;
58mod serve_file;
59
60#[doc(hidden)]
61pub mod private {
62    pub use {http, mime, mime_guess};
63}
64
65use bytes::Bytes;
66use http_body::{Body, Frame};
67use pin_project::pin_project;
68use std::{
69    io,
70    pin::Pin,
71    task::{Context, Poll},
72};
73use tokio::io::AsyncRead;
74
75use futures_util::Stream;
76use tokio_util::io::ReaderStream;
77
78// default capacity 64KiB
79const DEFAULT_CAPACITY: usize = 65536;
80
81pub use self::{
82    serve_dir::{
83        ResponseBody as ServeDirResponseBody, ResponseFuture as ServeDirResponseFuture, ServeDir,
84    },
85    serve_file::{
86        File, ResponseBody as ServeFileResponseBody, ResponseFuture as ServeFileResponseFuture,
87        ServeFile,
88    },
89};
90
91// NOTE: This could potentially be upstreamed to `http-body`.
92/// Adapter that turns an `impl AsyncRead` to an `impl Body`.
93#[pin_project]
94#[derive(Debug)]
95pub struct AsyncReadBody<T> {
96    #[pin]
97    reader: ReaderStream<T>,
98}
99
100impl<T> AsyncReadBody<T>
101where
102    T: AsyncRead,
103{
104    /// Create a new [`AsyncReadBody`] wrapping the given reader,
105    /// with a specific read buffer capacity
106    fn with_capacity(read: T, capacity: usize) -> Self {
107        Self {
108            reader: ReaderStream::with_capacity(read, capacity),
109        }
110    }
111}
112
113impl<T> Body for AsyncReadBody<T>
114where
115    T: AsyncRead,
116{
117    type Data = Bytes;
118    type Error = io::Error;
119
120    fn poll_frame(
121        self: Pin<&mut Self>,
122        cx: &mut Context<'_>,
123    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
124        self.project().reader.poll_next(cx).map(|res| match res {
125            Some(Ok(buf)) => Some(Ok(Frame::data(buf))),
126            Some(Err(err)) => Some(Err(err)),
127            None => None,
128        })
129    }
130}
131
132#[cfg(feature = "metadata")]
133fn unmodified_since_request_condition<T>(
134    file: &include_dir::File<'_>,
135    req: &http::Request<T>,
136) -> bool {
137    use http::{header, Method};
138    use httpdate::HttpDate;
139
140    let Some(metadata) = file.metadata() else {
141        return false;
142    };
143
144    // If-Modified-Since header spec says:
145    //
146    // > When used in combination with If-None-Match, it is ignored, unless the server doesn't support If-None-Match.
147    //
148    // We can ignore the If-None-Match header is exists or not, because we currently do not support If-None-Match.
149
150    // If-Modified-Since can only be used with a GET or HEAD.
151    match req.method() {
152        &Method::GET | &Method::HEAD => (),
153        _ => return false,
154    }
155
156    let Some(since) = req
157        .headers()
158        .get(header::IF_MODIFIED_SINCE)
159        .and_then(|value| value.to_str().ok())
160        .and_then(|value| value.parse::<HttpDate>().ok())
161    else {
162        return false;
163    };
164
165    metadata.modified() <= since.into()
166}