livereload_server/
serve.rs

1use std::path::Path;
2
3use crate::inject::inject_before_the_end_of_body;
4
5pub async fn serve_file(
6    target_dir: impl AsRef<Path>,
7    path_in_request: &str,
8    payload: &str,
9) -> anyhow::Result<Option<Vec<u8>>> {
10    let target_dir = target_dir.as_ref();
11
12    let mut path = path_in_request;
13
14    if path == "/" || path.is_empty() {
15        path = "/index.html";
16    }
17
18    let path = path.strip_prefix('/').unwrap_or(path);
19
20    let path_in_target_dir = target_dir.join(path);
21
22    if !path_in_target_dir.exists() {
23        return Ok(None);
24    }
25
26    let body = if path.ends_with(".html") {
27        log::info!(
28            "Serving HTML requested at {path} with {path_in_target_dir:?}"
29        );
30
31        let content = tokio::fs::read_to_string(path_in_target_dir).await?;
32
33        let data = inject_before_the_end_of_body(content.as_str(), payload)?;
34
35        data.bytes().collect()
36    } else {
37        log::info!("Serving a non HTML file requested as {path_in_request} with {path_in_target_dir:?}");
38
39        tokio::fs::read(path_in_target_dir).await?
40    };
41
42    Ok(Some(body))
43}
44
45#[cfg(test)]
46mod tests {
47    use indoc::indoc;
48    use tempfile::TempDir;
49
50    use super::*;
51
52    const INDEX: &str = indoc! {r#"
53        <!DOCTYPE html>
54        <html>
55        <body>
56            <h1>My First Heading</h1>
57            <p>My first paragraph.</p>
58        </body>
59        </html>
60    "#};
61
62    const INDEX_CSS: &str = indoc! {r#"
63        body {
64            color: red;
65        }
66    "#};
67
68    async fn prepare_directory() -> anyhow::Result<TempDir> {
69        let temp_dir = TempDir::new()?;
70
71        tokio::fs::write(temp_dir.path().join("index.html"), INDEX).await?;
72        tokio::fs::write(temp_dir.path().join("index.css"), INDEX_CSS).await?;
73
74        Ok(temp_dir)
75    }
76
77    #[tokio::test]
78    async fn serving_a_missing_file_returns_none() -> anyhow::Result<()> {
79        let temp_dir = prepare_directory().await?;
80
81        let served = serve_file(temp_dir.path(), "favicon.ico", "").await?;
82
83        assert!(served.is_none());
84
85        Ok(())
86    }
87
88    #[tokio::test]
89    async fn serving_at_root_returns_index_html() -> anyhow::Result<()> {
90        let temp_dir = prepare_directory().await?;
91
92        let expected: Vec<u8> = INDEX.bytes().collect();
93
94        let served = serve_file(temp_dir.path(), "index.html", "")
95            .await?
96            .unwrap();
97        assert_eq!(served, expected);
98
99        let served = serve_file(temp_dir.path(), "/", "").await?.unwrap();
100        assert_eq!(served, expected);
101
102        let served = serve_file(temp_dir.path(), "", "").await?.unwrap();
103        assert_eq!(served, expected);
104
105        Ok(())
106    }
107
108    #[tokio::test]
109    async fn serving_an_existing_file_returns_it() -> anyhow::Result<()> {
110        let temp_dir = prepare_directory().await?;
111
112        let served =
113            serve_file(temp_dir.path(), "index.css", "").await?.unwrap();
114
115        let expected: Vec<u8> = INDEX_CSS.bytes().collect();
116
117        assert_eq!(served, expected);
118
119        Ok(())
120    }
121}