Skip to main content

trillium_static/
handler.rs

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