trillium_static_compiled/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(
3    clippy::dbg_macro,
4    missing_copy_implementations,
5    rustdoc::missing_crate_level_docs,
6    missing_debug_implementations,
7    missing_docs,
8    nonstandard_style,
9    unused_qualifications
10)]
11
12/*!
13Serves static file assets from memory, as included in the binary at
14compile time. Because this includes file system content at compile
15time, it requires a macro interface, [`static_compiled`](crate::static_compiled).
16
17If the root is a directory, it will recursively serve any files
18relative to the path that this handler is mounted at, or an index file
19if one is configured with
20[`with_index_file`](crate::StaticCompiledHandler::with_index_file).
21
22If the root is a file, it will serve that file at all request paths.
23
24This crate contains code from [`include_dir`][include_dir], but with
25several tweaks to make it more suitable for this specific use case.
26
27[include_dir]:https://docs.rs/include_dir/latest/include_dir/
28
29```
30# #[cfg(not(unix))] fn main() {}
31# #[cfg(unix)] fn main() {
32use trillium_static_compiled::static_compiled;
33
34let handler = static_compiled!("./examples/files")
35    .with_index_file("index.html");
36
37// given the following directory layout
38//
39// examples/files
40// ├── index.html
41// ├── subdir
42// │  └── index.html
43// └── subdir_with_no_index
44//    └── plaintext.txt
45//
46
47use trillium_testing::prelude::*;
48
49assert_ok!(
50    get("/").on(&handler),
51    "<html>\n  <head>\n    <script src=\"/js.js\"></script>\n  </head>\n  <body>\n    <h1>hello world</h1>\n  </body>\n</html>",
52    "content-type" => "text/html"
53);
54assert_not_handled!(get("/file_that_does_not_exist.txt").on(&handler));
55assert_ok!(get("/index.html").on(&handler));
56assert_ok!(
57    get("/subdir/index.html").on(&handler),
58    "subdir index.html 🎈",
59    "content-type" => "text/html; charset=utf-8"
60);
61assert_ok!(get("/subdir").on(&handler), "subdir index.html 🎈");
62assert_not_handled!(get("/subdir_with_no_index").on(&handler));
63assert_ok!(
64    get("/subdir_with_no_index/plaintext.txt").on(&handler),
65    "plaintext file",
66    "content-type" => "text/plain"
67);
68
69
70// with a different index file
71let plaintext_index = static_compiled!("./examples/files")
72    .with_index_file("plaintext.txt");
73
74assert_not_handled!(get("/").on(&plaintext_index));
75assert_not_handled!(get("/subdir").on(&plaintext_index));
76assert_ok!(
77    get("/subdir_with_no_index").on(&plaintext_index),
78    "plaintext file",
79    "content-type" => "text/plain"
80);
81
82// with no index file
83let no_index = static_compiled!("./examples/files");
84
85assert_not_handled!(get("/").on(&no_index));
86assert_not_handled!(get("/subdir").on(&no_index));
87assert_not_handled!(get("/subdir_with_no_index").on(&no_index));
88# }
89```
90*/
91
92use trillium::{
93    async_trait, Conn, Handler,
94    KnownHeaderName::{ContentType, LastModified},
95};
96
97mod dir;
98mod dir_entry;
99mod file;
100mod metadata;
101
102pub(crate) use crate::dir::Dir;
103pub(crate) use crate::dir_entry::DirEntry;
104pub(crate) use crate::file::File;
105pub(crate) use crate::metadata::Metadata;
106
107#[doc(hidden)]
108pub mod __macro_internals {
109    pub use crate::{dir::Dir, dir_entry::DirEntry, file::File, metadata::Metadata};
110    pub use trillium_static_compiled_macros::{include_dir, include_entry};
111}
112
113/**
114The static compiled handler which contains the compile-time loaded
115assets
116
117*/
118#[derive(Debug, Clone, Copy)]
119pub struct StaticCompiledHandler {
120    root: DirEntry,
121    index_file: Option<&'static str>,
122}
123
124impl StaticCompiledHandler {
125    /// Constructs a new StaticCompiledHandler. This must be used in
126    /// conjunction with [`root!`](crate::root). See crate-level docs for
127    /// example usage.
128    pub fn new(root: DirEntry) -> Self {
129        Self {
130            root,
131            index_file: None,
132        }
133    }
134    /// Configures the optional index file for this
135    /// StaticCompiledHandler. See the crate-level docs for example
136    /// usage.
137    pub fn with_index_file(mut self, file: &'static str) -> Self {
138        if let Some(file) = self.root.as_file() {
139            panic!(
140                "root is a file ({:?}), with_index_file is not meaningful.",
141                file.path()
142            );
143        }
144        self.index_file = Some(file);
145        self
146    }
147
148    fn serve_file(&self, mut conn: Conn, file: File) -> Conn {
149        let mime = mime_guess::from_path(file.path()).first_or_text_plain();
150
151        let is_ascii = file.contents().is_ascii();
152        let is_text = matches!(
153            (mime.type_(), mime.subtype()),
154            (mime::APPLICATION, mime::JAVASCRIPT) | (mime::TEXT, _) | (_, mime::HTML)
155        );
156
157        conn.response_headers_mut().try_insert(
158            ContentType,
159            if is_text && !is_ascii {
160                format!("{mime}; charset=utf-8")
161            } else {
162                mime.to_string()
163            },
164        );
165
166        if let Some(metadata) = file.metadata() {
167            conn.response_headers_mut()
168                .try_insert(LastModified, httpdate::fmt_http_date(metadata.modified()));
169        }
170
171        conn.ok(file.contents())
172    }
173
174    fn get_item(&self, path: &str) -> Option<DirEntry> {
175        if path.is_empty() || self.root.is_file() {
176            Some(self.root)
177        } else {
178            self.root.as_dir().and_then(|dir| {
179                dir.get_dir(path)
180                    .copied()
181                    .map(DirEntry::Dir)
182                    .or_else(|| dir.get_file(path).copied().map(DirEntry::File))
183            })
184        }
185    }
186}
187
188#[async_trait]
189impl Handler for StaticCompiledHandler {
190    async fn run(&self, conn: Conn) -> Conn {
191        match (
192            self.get_item(conn.path().trim_start_matches('/')),
193            self.index_file,
194        ) {
195            (None, _) => conn,
196            (Some(DirEntry::File(file)), _) => self.serve_file(conn, file),
197            (Some(DirEntry::Dir(_)), None) => conn,
198            (Some(DirEntry::Dir(dir)), Some(index_file)) => {
199                if let Some(file) = dir.get_file(dir.path().join(index_file)) {
200                    self.serve_file(conn, *file)
201                } else {
202                    conn
203                }
204            }
205        }
206    }
207}
208
209/**
210The preferred interface to build a StaticCompiledHandler
211
212Macro interface to build a
213[`StaticCompiledHandler`]. `static_compiled!("assets")` is
214identical to
215`StaticCompiledHandler::new(root!("assets"))`.
216
217This takes one argument, which must be a string literal.
218
219## Relative paths
220
221Relative paths are expanded and canonicalized relative to
222`$CARGO_MANIFEST_DIR`, which is usually the directory that contains
223your Cargo.toml. If compiled within a workspace, this will be the
224subcrate's Cargo.toml.
225
226## Environment variable expansion
227
228If the argument to `static_compiled` contains substrings that are
229formatted like an environment variable, beginning with a $, they will
230be interpreted in the compile time environment.
231
232For example "$OUT_DIR/some_directory" will expand to the directory
233`some_directory` within the env variable `$OUT_DIR` set by cargo. See
234[this link][env_vars] for further documentation on the environment
235variables set by cargo.
236
237[env_vars]:https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
238*/
239
240#[macro_export]
241macro_rules! static_compiled {
242    ($path:tt) => {
243        $crate::StaticCompiledHandler::new($crate::root!($path))
244    };
245}
246
247/**
248Include the path as root. To be passed into [`StaticCompiledHandler::new`].
249
250This takes one argument, which must be a string literal.
251
252## Relative paths
253
254Relative paths are expanded and canonicalized relative to
255`$CARGO_MANIFEST_DIR`, which is usually the directory that contains
256your Cargo.toml. If compiled within a workspace, this will be the
257subcrate's Cargo.toml.
258
259## Environment variable expansion
260
261If the argument to `static_compiled` contains substrings that are
262formatted like an environment variable, beginning with a $, they will
263be interpreted in the compile time environment.
264
265For example "$OUT_DIR/some_directory" will expand to the directory
266`some_directory` within the env variable `$OUT_DIR` set by cargo. See
267[this link][env_vars] for further documentation on the environment
268variables set by cargo.
269
270[env_vars]:https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
271
272*/
273#[macro_export]
274macro_rules! root {
275    ($path:tt) => {{
276        use $crate::__macro_internals::{include_entry, Dir, DirEntry, File, Metadata};
277        const ENTRY: DirEntry = include_entry!($path);
278        ENTRY
279    }};
280}