1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
20#![warn(clippy::print_stderr)]
21#![warn(clippy::print_stdout)]
22
23use std::{
24 str::FromStr,
25 sync::{RwLock, TryLockError},
26};
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct ServerBuilder {
31 source: std::path::PathBuf,
32 hostname: Option<String>,
33 port: Option<u16>,
34}
35
36impl ServerBuilder {
37 pub fn new(source: impl Into<std::path::PathBuf>) -> Self {
38 Self {
39 source: source.into(),
40 hostname: None,
41 port: None,
42 }
43 }
44
45 pub fn hostname(&mut self, hostname: impl Into<String>) -> &mut Self {
47 self.hostname = Some(hostname.into());
48 self
49 }
50
51 pub fn port(&mut self, port: u16) -> &mut Self {
55 self.port = Some(port);
56 self
57 }
58
59 pub fn build(&self) -> Server {
63 let source = self.source.clone();
64 let hostname = self.hostname.as_deref().unwrap_or("localhost");
65 let port = self
66 .port
67 .or_else(|| get_available_port(hostname))
68 .unwrap_or(3000);
70
71 Server {
72 source,
73 addr: format!("{hostname}:{port}"),
74 server: RwLock::new(None),
75 }
76 }
77
78 pub fn serve(&self) -> Result<(), Error> {
80 self.build().serve()
81 }
82}
83
84pub struct Server {
85 source: std::path::PathBuf,
86 addr: String,
87 server: RwLock<Option<tiny_http::Server>>,
88}
89
90impl Server {
91 pub fn new(source: impl Into<std::path::PathBuf>) -> Self {
93 ServerBuilder::new(source).build()
94 }
95
96 pub fn source(&self) -> &std::path::Path {
98 self.source.as_path()
99 }
100
101 pub fn addr(&self) -> &str {
106 self.addr.as_str()
107 }
108
109 pub fn is_running(&self) -> bool {
111 matches!(self.server.read().as_deref(), Ok(Some(_)))
112 }
113
114 pub fn serve(&self) -> Result<(), Error> {
116 match self.server.try_write().as_deref_mut() {
117 Ok(server @ None) => {
118 *server = Some(tiny_http::Server::http(self.addr()).map_err(Error::new)?);
120 }
121 Ok(Some(_)) | Err(TryLockError::WouldBlock) => {
122 return Err(Error::new("the server is running"));
123 }
124 Err(error @ TryLockError::Poisoned(_)) => return Err(Error::new(error)),
125 }
126
127 {
128 let server = self.server.read().map_err(Error::new)?;
129 for request in server.as_ref().unwrap().incoming_requests() {
131 if let Err(e) = static_file_handler(self.source(), request) {
133 log::error!("{e}");
134 }
135 }
136 }
137
138 *self.server.write().map_err(Error::new)? = None;
139
140 Ok(())
141 }
142
143 pub fn close(&self) {
145 if let Ok(Some(server)) = self.server.read().as_deref() {
146 server.unblock();
147 }
148 }
149}
150
151#[derive(Debug)]
153pub struct Error {
154 message: String,
155}
156
157impl Error {
158 fn new(message: impl ToString) -> Self {
159 Self {
160 message: message.to_string(),
161 }
162 }
163}
164
165impl std::fmt::Display for Error {
166 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 self.message.fmt(fmt)
168 }
169}
170
171impl std::error::Error for Error {}
172
173fn static_file_handler(dest: &std::path::Path, req: tiny_http::Request) -> Result<(), Error> {
174 let mut req_path = req.url().to_string();
176
177 if let Some(position) = req_path.rfind('?') {
180 req_path.truncate(position);
181 }
182
183 let path = dest.to_path_buf().join(&req_path[1..]);
186
187 let serve_path = if path.is_file() {
188 path
190 } else {
191 path.join("index.html")
194 };
195
196 if serve_path.exists() {
198 let file = std::fs::File::open(&serve_path).map_err(Error::new)?;
199 let mut response = tiny_http::Response::from_file(file);
200 if let Some(mime) = mime_guess::MimeGuess::from_path(&serve_path).first_raw() {
201 let content_type = format!("Content-Type:{mime}");
202 let content_type =
203 tiny_http::Header::from_str(&content_type).expect("formatted correctly");
204 response.add_header(content_type);
205 }
206 req.respond(response).map_err(Error::new)?;
207 } else {
208 req.respond(
210 tiny_http::Response::from_string("<h1> <center> 404: Page not found </center> </h1>")
211 .with_status_code(404)
212 .with_header(
213 tiny_http::Header::from_str("Content-Type: text/html")
214 .expect("formatted correctly"),
215 ),
216 )
217 .map_err(Error::new)?;
218 }
219
220 Ok(())
221}
222
223fn get_available_port(host: &str) -> Option<u16> {
224 (1024..9000).find(|port| port_is_available(host, *port))
227}
228
229fn port_is_available(host: &str, port: u16) -> bool {
230 std::net::TcpListener::bind((host, port)).is_ok()
231}