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 151 152 153 154 155 156 157 158 159 160 161 162 163
/// Private unwrap macro for unrecoverable error handling
#[macro_use]
mod unwrap;
/// Use static files
mod files;
/// Watch folders and send websocket updates
#[cfg(feature = "watch")]
mod watch;
use std::{convert::Infallible, fs, path::Path};
use http::{Method, Request, Response, StatusCode};
use hyper::{
service::{make_service_fn, service_fn},
Body, Server,
};
use crate::{Port, DEV_BUILD_DIR};
pub use files::{dev_script, fallback_404};
#[cfg(feature = "watch")]
pub use watch::watch;
/// Create server and listen on localhost port
///
/// Similar to GitHub Pages router
///
/// Reads file on every request: this should not be a problem for a dev server
pub fn listen(port: Port, public: &str, port_ws: Port) {
// Create runtime
let runtime = unwrap!(
tokio::runtime::Builder::new_current_thread()
.enable_io()
.build(),
"Failed to build tokio runtime"
);
let public = public.to_string().clone();
// Block on server running
unwrap!(
runtime.block_on(async {
// Create service for router
// Moves `public`
let make_svc =
make_service_fn(move |_| {
let public = public.clone();
async move {
Ok::<_, Infallible>(service_fn(move |req| {
server_router(req, public.clone(), port_ws)
}))
}
});
// Parse IP address
let addr = unwrap!(
format!("127.0.0.1:{}", port).parse(),
"Failed to parse constant IP address"
);
// Create and start server
Server::bind(&addr).serve(make_svc).await
}),
// Generic runtime error
err: "Error in server runtime: `{err:?}`"
);
}
/// Route path to read and return file.
///
/// Accepts '/foo', '/foo.html', and '/foo/index.html' patterns
///
/// If no possible file was found, use 404 route (same as <URL>/404 request).
/// If no custom 404 page was found, use fallback 404 page
async fn server_router(
req: Request<Body>,
public: String,
port_ws: Port,
) -> Result<Response<Body>, Infallible> {
// Check if is GET request
if req.method() == Method::GET {
let path = req.uri().path();
// Map public files to source public folder
if path.starts_with("/public/") {
let path = path.replacen("/public", &public, 1);
return Ok(Response::new(Body::from(read_and_unwrap(&path))));
}
// Return corresponding file as body if exists
// Routes everything but `/public/` files
if let Some(file) = get_best_possible_file(path) {
return Ok(Response::new(file));
}
}
// 404 route
Ok(unwrap!(
Response::builder().status(StatusCode::NOT_FOUND).body(
if let Some(file) = get_best_possible_file("/404.html") {
// If custom 404 route is defined (requesting route `/404.html`)
file
} else {
// Fallback 404 response
Body::from(fallback_404(port_ws))
},
),
// Should not error
err: "Failed to build 404 route response `{err:?}`",
))
}
/// Loops through files in `possible_path_suffixes` to find best file match, and reads file
///
/// Returns as `Option<Body>`, to allow non-UTF-8 file formats (such as images).
/// Returns `None` if no files were found
///
/// Panics if file exists, but was unable to be read
fn get_best_possible_file(path: &str) -> Option<Body> {
let possible_suffixes = possible_path_suffixes(path);
for suffix in possible_suffixes {
let path = &format!("{DEV_BUILD_DIR}{path}{suffix}");
// If file exists, and not directory
if Path::new(path).is_file() {
// Returns file content as `Body`
// Automatically parses to string, if is valid UTF-8, otherwise uses buffer
return Some(read_and_unwrap(path));
}
}
None
}
/// Read file and convert to body
///
/// Panics if IO error occurs
fn read_and_unwrap(path: &str) -> Body {
Body::from(unwrap!(
fs::read(path),
// Should only happen due to insufficient permissions or similar, not 'file not exist' error
"Could not read file '{}'",
path
))
}
/// Gets the possible path 'suffixes' from the path string
///
/// If path ends with '.html', or starts with '/styles', then return a slice of an empty string.
/// This path should refer to a file literally
///
/// Otherwise, return a slice of: an empty string (for a literal file), '.html', and '/index.html' (for file path shorthand).
/// Suffixes are returned in that order, to match a file based on specificity
///
/// Paths starting with `/public/` should never be routed through here
//TODO Make error for /public/ routing here
fn possible_path_suffixes(path: &str) -> &'static [&'static str] {
if path.ends_with(".html") || path.starts_with("/styles/") {
&[""]
} else {
&["", ".html", "/index.html"]
}
}