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}