1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! The simplest file server for web development purposes.
//!
//! ```no_run
//! use env_logger::{Builder, Env};
//! use tiny_file_server::FileServer;
//!
//! fn main() {
//!     Builder::from_env(Env::default().default_filter_or("debug")).init();
//!
//!     FileServer::http("127.0.0.1:9080")
//!         .expect("Server should be created")
//!         .run("path/to/static/files")
//!         .expect("Server should start");
//! }
//! ```

use std::{
    borrow::Cow, collections::HashMap, error, ffi::OsStr, fmt::Display, fs::File, io, net::ToSocketAddrs,
    path::PathBuf, str::FromStr,
};

use log::{debug, info};
use std::borrow::Borrow;
use tiny_http::{Header, Response, Server, StatusCode};

pub type Error = Box<dyn error::Error + Send + Sync + 'static>;

pub struct FileServer {
    server: Server,
    default_file: Cow<'static, str>,
    default_content_type: Cow<'static, str>,
    content_type_by_extension: HashMap<&'static str, &'static str>,
}

impl FileServer {
    pub fn http(addr: impl ToSocketAddrs + Display) -> Result<Self, Error> {
        info!("Starting file server on http://{}", addr);
        let server = Server::http(addr)?;

        let content_type_by_extension = [
            ("js", "application/javascript"),
            ("wasm", "application/wasm"),
            ("html", "text/html"),
            ("css", "text/css"),
        ]
        .iter()
        .cloned()
        .collect();

        Ok(Self {
            server,
            default_file: "index.html".into(),
            default_content_type: "text/plain".into(),
            content_type_by_extension,
        })
    }

    pub fn with_default_file(mut self, file_name: impl Into<Cow<'static, str>>) -> Self {
        self.set_default_file(file_name);
        self
    }

    pub fn with_default_content_type(mut self, content_type: impl Into<Cow<'static, str>>) -> Self {
        self.set_default_content_type(content_type);
        self
    }

    pub fn with_content_type_by_extension(
        mut self,
        content_types: impl IntoIterator<Item = (&'static str, &'static str)>,
    ) -> Self {
        self.content_type_by_extension.extend(content_types);
        self
    }

    pub fn set_default_file(&mut self, file_name: impl Into<Cow<'static, str>>) {
        self.default_file = file_name.into();
    }

    pub fn set_default_content_type(&mut self, content_type: impl Into<Cow<'static, str>>) {
        self.default_content_type = content_type.into();
    }

    pub fn content_type_by_extension(&self) -> &HashMap<&'static str, &'static str> {
        &self.content_type_by_extension
    }

    pub fn content_type_by_extension_mut(&mut self) -> &mut HashMap<&'static str, &'static str> {
        &mut self.content_type_by_extension
    }

    pub fn unblock(&self) {
        self.server.unblock();
    }

    pub fn run(&self, statics_path: impl Into<PathBuf>) -> Result<(), io::Error> {
        let statics_path = statics_path.into();
        info!("Listen incoming requests to {}", statics_path.display());
        for request in self.server.incoming_requests() {
            debug!(
                "Received request. Method: {:?}, url: {:?}, headers: {:?}",
                request.method(),
                request.url(),
                request.headers()
            );

            let mut file_path = statics_path.clone();
            if request.url().len() > 1 {
                for chunk in request.url().trim_start_matches('/').split('/') {
                    file_path.push(chunk);
                }
            } else {
                let default_file: &str = self.default_file.borrow();
                file_path.push(default_file);
            };

            debug!("Requested file: {}", file_path.display());

            if !file_path.exists() {
                let status = StatusCode(404);
                debug!("Status: {} ({})", status.default_reason_phrase(), status.0);
                request.respond(Response::empty(status))?;
            } else {
                match File::open(&file_path) {
                    Ok(file) => {
                        let mut response = Response::from_file(file);
                        let content_type = file_path
                            .extension()
                            .and_then(OsStr::to_str)
                            .and_then(|ext| self.content_type_by_extension.get(ext).copied())
                            .unwrap_or(&self.default_content_type);
                        response.add_header(
                            Header::from_str(&format!("Content-Type: {}", content_type))
                                .map_err(|_| io::Error::from(io::ErrorKind::Other))?,
                        );
                        request.respond(response)?;
                    },
                    Err(err) => {
                        let status = StatusCode(500);
                        debug!("Status: {} ({})", status.default_reason_phrase(), status.0);
                        debug!("Error: {:?}", err);
                        request.respond(Response::empty(status))?;
                    },
                }
            };
        }
        info!("File server socket is shutdown");
        Ok(())
    }
}