axum_embed_files/
service.rs

1use tracing::debug;
2
3/// A tower Service serving an embedded file.
4///
5/// Most easily obtained from the [`embed_file!()`](../macro.embed_file.html) macro.
6/// Most usefully, implements the flavor of `tower` Service required to use as an
7/// `axum` handler.
8///
9/// By default, serves the embedded version of the file.  For development use,
10/// enable the feature `serve-from-fs` to load the contents dynamically from the
11/// local file system.
12///
13/// # Example
14///
15/// ```
16/// use axum::{routing, Router};
17/// use axum_embed_files::service::EmbeddedFileService;
18/// # #[derive(Clone)]
19/// # struct State {}
20///
21/// fn router<S: Clone + Send + Sync + 'static>() -> Router<S> {
22///     Router::new().route("/colophon.txt", routing::get_service(EmbeddedFileService {
23///         directory: "/path/to/local/files",
24///         filename: "include/colophon.txt",
25///         bytes: b"This project was brought to you by Rust.",
26///         etag: "\"V1\"",
27///         last_modified: "Mon, 01 Jan 1970 00:00:00 GMT",
28///         content_type: Some("text/plain"),
29///     }))
30/// }
31/// ```
32#[derive(Debug, Clone)]
33pub struct EmbeddedFileService {
34    /// The base directory to find the file.
35    pub directory: &'static str, // TODO: non-unicode?
36    /// The path to the source file.
37    pub filename: &'static str, // TODO: non-unicode?
38    /// The content of the file as bytes.
39    pub bytes: &'static [u8],
40    /// An ETag validator for the file, inclusive of quotes.
41    ///
42    /// Consider using `axum_embed_files_core::etag::generate`.
43    pub etag: &'static str,
44    /// The formatted last modified date of the file.
45    ///
46    /// Consider using `axum_embed_files_core::last_modified::of_path`.
47    pub last_modified: &'static str,
48    /// The file's content type, if we know it.
49    ///
50    /// Consider using `axum_embed_files_core::content_type::guess_from_path`.
51    pub content_type: Option<&'static str>,
52}
53
54#[cfg(not(feature = "serve-from-fs"))]
55impl<B> tower_service::Service<http::Request<B>> for EmbeddedFileService {
56    type Response = http::Response<axum::body::Body>;
57    type Error = std::convert::Infallible;
58    type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
59
60    fn poll_ready(&mut self, _: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
61        std::task::Poll::Ready(Ok(()))
62    }
63
64    fn call(&mut self, req: http::Request<B>) -> Self::Future {
65        if let Some(value) = req.headers().get(http::header::IF_NONE_MATCH) {
66            if let Ok(etag) = value.to_str() {
67                if etag == self.etag {
68                    debug!("cache hit");
69
70                    let mut res = http::Response::new([][..].into());
71                    *res.status_mut() = http::StatusCode::NOT_MODIFIED;
72                    res.headers_mut().insert(
73                        http::header::ETAG,
74                        http::header::HeaderValue::from_static(self.etag),
75                    );
76                    res.headers_mut().insert(
77                        http::header::LAST_MODIFIED,
78                        http::header::HeaderValue::from_static(self.last_modified),
79                    );
80                    return std::future::ready(Ok(res));
81                }
82            }
83        }
84
85        // TODO: if match, if modified since, if unmodified since
86
87        debug!("serving from embedded file {}", self.filename);
88
89        let mut res = http::Response::new(self.bytes.into());
90        res.headers_mut().insert(
91            http::header::CACHE_CONTROL,
92            http::header::HeaderValue::from_static("max-age=31536000"),
93        );
94        res.headers_mut().insert(
95            http::header::ETAG,
96            http::header::HeaderValue::from_static(self.etag),
97        );
98        res.headers_mut().insert(
99            http::header::LAST_MODIFIED,
100            http::header::HeaderValue::from_static(self.last_modified),
101        );
102        if let Some(content_type) = &self.content_type {
103            res.headers_mut().insert(
104                http::header::CONTENT_TYPE,
105                http::header::HeaderValue::from_static(content_type),
106            );
107        }
108        std::future::ready(Ok(res))
109    }
110}
111
112#[cfg(feature = "serve-from-fs")]
113impl<B> tower_service::Service<http::Request<B>> for EmbeddedFileService {
114    type Response = http::Response<axum::body::Body>;
115    type Error = std::convert::Infallible;
116    type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
117
118    fn poll_ready(&mut self, _: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
119        std::task::Poll::Ready(Ok(()))
120    }
121
122    fn call(&mut self, _req: http::Request<B>) -> Self::Future {
123        use axum_embed_files_core::content_type;
124
125        debug!("serving from local file {}", self.filename);
126
127        let path = std::path::PathBuf::from(&self.directory)
128            .join(&self.filename);
129
130        let bytes = std::fs::read(path).unwrap();
131
132        let mut res = http::Response::new(bytes.into());
133        if let Some(content_type) = content_type::guess_from_path(&self.filename) {
134            res.headers_mut().insert(
135                http::header::CONTENT_TYPE,
136                http::header::HeaderValue::from_str(&content_type).unwrap(),
137            );
138        }
139        std::future::ready(Ok(res))
140    }
141}