trillium_static/
handler.rs

1use crate::{
2    fs_shims::{fs, File},
3    options::StaticOptions,
4    StaticConnExt,
5};
6use std::path::{Path, PathBuf};
7use trillium::{async_trait, conn_unwrap, Conn, Handler};
8
9/**
10trillium handler to serve static files from the filesystem
11*/
12#[derive(Debug)]
13pub struct StaticFileHandler {
14    fs_root: PathBuf,
15    index_file: Option<String>,
16    root_is_file: bool,
17    options: StaticOptions,
18}
19
20#[derive(Debug)]
21enum Record {
22    File(PathBuf, File),
23    Dir(PathBuf),
24}
25
26impl StaticFileHandler {
27    async fn resolve_fs_path(&self, url_path: &str) -> Option<PathBuf> {
28        let mut file_path = self.fs_root.clone();
29        log::trace!(
30            "attempting to resolve {} relative to {}",
31            url_path,
32            file_path.to_str().unwrap()
33        );
34        for segment in Path::new(url_path) {
35            match segment.to_str() {
36                Some("/") => {}
37                Some(".") => {}
38                Some("..") => {
39                    file_path.pop();
40                }
41                _ => {
42                    file_path.push(segment);
43                }
44            };
45        }
46
47        if file_path.starts_with(&self.fs_root) {
48            fs::canonicalize(file_path).await.ok().map(Into::into)
49        } else {
50            None
51        }
52    }
53
54    async fn resolve(&self, url_path: &str) -> Option<Record> {
55        let fs_path = self.resolve_fs_path(url_path).await?;
56        let metadata = fs::metadata(&fs_path).await.ok()?;
57        if metadata.is_dir() {
58            log::trace!("resolved {} as dir {}", url_path, fs_path.to_str().unwrap());
59            Some(Record::Dir(fs_path))
60        } else if metadata.is_file() {
61            File::open(&fs_path)
62                .await
63                .ok()
64                .map(|file| Record::File(fs_path, file))
65        } else {
66            None
67        }
68    }
69
70    /**
71    builds a new StaticFileHandler
72
73    If the fs_root is a file instead of a directory, that file will be served at all paths.
74
75    ```
76    # #[cfg(not(unix))] fn main() {}
77    # #[cfg(unix)] fn main() {
78    # use trillium::Handler;
79    # trillium_testing::block_on(async {
80    use trillium_static::{StaticFileHandler, crate_relative_path};
81    use trillium_testing::prelude::*;
82
83    let mut handler = StaticFileHandler::new(crate_relative_path!("examples/files"));
84    # handler.init(&mut "testing".into()).await;
85
86    assert_not_handled!(get("/").run_async(&handler).await); // no index file configured
87
88    assert_ok!(
89        get("/index.html").run_async(&handler).await,
90        "<h1>hello world</h1>",
91        "content-type" => "text/html; charset=utf-8"
92    );
93    # }); }
94    ```
95    */
96    pub fn new(fs_root: impl AsRef<Path>) -> Self {
97        let fs_root = fs_root.as_ref().canonicalize().unwrap();
98        Self {
99            fs_root,
100            index_file: None,
101            root_is_file: false,
102            options: StaticOptions::default(),
103        }
104    }
105
106    /// do not set an etag header
107    pub fn without_etag_header(mut self) -> Self {
108        self.options.etag = false;
109        self
110    }
111
112    /// do not set last-modified header
113    pub fn without_modified_header(mut self) -> Self {
114        self.options.modified = false;
115        self
116    }
117
118    /**
119    sets the index file on this StaticFileHandler
120    ```
121    # #[cfg(not(unix))] fn main() {}
122    # #[cfg(unix)] fn main() {
123    # use trillium::Handler;
124    # trillium_testing::block_on(async {
125
126    use trillium_static::{StaticFileHandler, crate_relative_path};
127
128    let mut handler = StaticFileHandler::new(crate_relative_path!("examples/files"))
129        .with_index_file("index.html");
130    # handler.init(&mut "testing".into()).await;
131
132    use trillium_testing::prelude::*;
133    assert_ok!(
134        get("/").run_async(&handler).await,
135        "<h1>hello world</h1>", "content-type" => "text/html; charset=utf-8"
136    );
137    # }); }
138    ```
139    */
140    pub fn with_index_file(mut self, file: &str) -> Self {
141        self.index_file = Some(file.to_string());
142        self
143    }
144}
145
146#[async_trait]
147impl Handler for StaticFileHandler {
148    async fn init(&mut self, _info: &mut trillium::Info) {
149        self.root_is_file = match self.resolve("/").await {
150            Some(Record::File(path, _)) => {
151                log::info!("serving {:?} for all paths", path);
152                true
153            }
154
155            Some(Record::Dir(dir)) => {
156                log::info!("serving files within {:?}", dir);
157                false
158            }
159
160            None => {
161                log::error!(
162                    "could not find {:?} on init, continuing anyway",
163                    self.fs_root
164                );
165                false
166            }
167        };
168    }
169
170    async fn run(&self, conn: Conn) -> Conn {
171        match self.resolve(conn.path()).await {
172            Some(Record::File(path, file)) => conn.send_file(file).await.with_mime_from_path(path),
173
174            Some(Record::Dir(path)) => {
175                let index = conn_unwrap!(self.index_file.as_ref(), conn);
176                let path = path.join(index);
177                let file = conn_unwrap!(File::open(path.to_str().unwrap()).await.ok(), conn);
178                conn.send_file_with_options(file, &self.options)
179                    .await
180                    .with_mime_from_path(path)
181            }
182
183            _ => conn,
184        }
185    }
186}