#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]
use std::{
str::FromStr,
sync::{RwLock, TryLockError},
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServerBuilder {
source: std::path::PathBuf,
hostname: Option<String>,
port: Option<u16>,
}
impl ServerBuilder {
pub fn new(source: impl Into<std::path::PathBuf>) -> Self {
Self {
source: source.into(),
hostname: None,
port: None,
}
}
pub fn hostname(&mut self, hostname: impl Into<String>) -> &mut Self {
self.hostname = Some(hostname.into());
self
}
pub fn port(&mut self, port: u16) -> &mut Self {
self.port = Some(port);
self
}
pub fn build(&self) -> Server {
let source = self.source.clone();
let hostname = self.hostname.as_deref().unwrap_or("localhost");
let port = self
.port
.or_else(|| get_available_port(hostname))
.unwrap_or(3000);
Server {
source,
addr: format!("{hostname}:{port}"),
server: RwLock::new(None),
}
}
pub fn serve(&self) -> Result<(), Error> {
self.build().serve()
}
}
pub struct Server {
source: std::path::PathBuf,
addr: String,
server: RwLock<Option<tiny_http::Server>>,
}
impl Server {
pub fn new(source: impl Into<std::path::PathBuf>) -> Self {
ServerBuilder::new(source).build()
}
pub fn source(&self) -> &std::path::Path {
self.source.as_path()
}
pub fn addr(&self) -> &str {
self.addr.as_str()
}
pub fn is_running(&self) -> bool {
matches!(self.server.read().as_deref(), Ok(Some(_)))
}
pub fn serve(&self) -> Result<(), Error> {
match self.server.try_write().as_deref_mut() {
Ok(server @ None) => {
*server = Some(tiny_http::Server::http(self.addr()).map_err(Error::new)?);
}
Ok(Some(_)) | Err(TryLockError::WouldBlock) => {
return Err(Error::new("the server is running"));
}
Err(error @ TryLockError::Poisoned(_)) => return Err(Error::new(error)),
}
{
let server = self.server.read().map_err(Error::new)?;
for request in server.as_ref().unwrap().incoming_requests() {
if let Err(e) = static_file_handler(self.source(), request) {
log::error!("{e}");
}
}
}
*self.server.write().map_err(Error::new)? = None;
Ok(())
}
pub fn close(&self) {
if let Ok(Some(server)) = self.server.read().as_deref() {
server.unblock();
}
}
}
#[derive(Debug)]
pub struct Error {
message: String,
}
impl Error {
fn new(message: impl ToString) -> Self {
Self {
message: message.to_string(),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.message.fmt(fmt)
}
}
impl std::error::Error for Error {}
fn static_file_handler(dest: &std::path::Path, req: tiny_http::Request) -> Result<(), Error> {
let mut req_path = req.url().to_owned();
if let Some(position) = req_path.rfind('?') {
req_path.truncate(position);
}
let path = dest.to_path_buf().join(&req_path[1..]);
let serve_path = if path.is_file() {
path
} else {
path.join("index.html")
};
if serve_path.exists() {
let file = std::fs::File::open(&serve_path).map_err(Error::new)?;
let mut response = tiny_http::Response::from_file(file);
if let Some(mime) = mime_guess::MimeGuess::from_path(&serve_path).first_raw() {
let content_type = format!("Content-Type:{mime}");
let content_type =
tiny_http::Header::from_str(&content_type).expect("formatted correctly");
response.add_header(content_type);
}
req.respond(response).map_err(Error::new)?;
} else {
req.respond(
tiny_http::Response::from_string("<h1> <center> 404: Page not found </center> </h1>")
.with_status_code(404)
.with_header(
tiny_http::Header::from_str("Content-Type: text/html")
.expect("formatted correctly"),
),
)
.map_err(Error::new)?;
}
Ok(())
}
fn get_available_port(host: &str) -> Option<u16> {
(1024..9000).find(|port| port_is_available(host, *port))
}
fn port_is_available(host: &str, port: u16) -> bool {
std::net::TcpListener::bind((host, port)).is_ok()
}