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}