Migrate serve command to hyper (#904)
hyper is already included in Zola due to the reqwest dependency (used in the link_checker and templates components). Replacing Actix with hyper in the serve command reduces the number of dependencies and slightly improves build times and binary size.
This commit is contained in:
parent
622b0f2965
commit
2966adbe4e
887
Cargo.lock
generated
887
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -26,8 +26,9 @@ termcolor = "1.0.4"
|
||||||
# Used in init to ensure the url given as base_url is a valid one
|
# Used in init to ensure the url given as base_url is a valid one
|
||||||
url = "2"
|
url = "2"
|
||||||
# Below is for the serve cmd
|
# Below is for the serve cmd
|
||||||
actix-files = "0.1"
|
hyper = { version = "0.13", default-features = false, features = ["runtime"] }
|
||||||
actix-web = { version = "1.0", default-features = false, features = [] }
|
hyper-staticfile = "0.5"
|
||||||
|
tokio = { version = "0.2", default-features = false, features = [] }
|
||||||
notify = "4"
|
notify = "4"
|
||||||
ws = "0.9"
|
ws = "0.9"
|
||||||
ctrlc = "3"
|
ctrlc = "3"
|
||||||
|
|
133
src/cmd/serve.rs
133
src/cmd/serve.rs
|
@ -22,16 +22,18 @@
|
||||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::{read_dir, remove_dir_all, File};
|
use std::fs::{read_dir, remove_dir_all};
|
||||||
use std::io::Read;
|
|
||||||
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
|
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use actix_files as fs;
|
use hyper::header;
|
||||||
use actix_web::middleware::errhandlers::{ErrorHandlerResponse, ErrorHandlers};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use actix_web::{dev, http, web, App, HttpResponse, HttpServer};
|
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
||||||
|
use hyper_staticfile::ResolveResult;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use ctrlc;
|
use ctrlc;
|
||||||
use notify::{watcher, RecursiveMode, Watcher};
|
use notify::{watcher, RecursiveMode, Watcher};
|
||||||
|
@ -56,34 +58,79 @@ enum ChangeKind {
|
||||||
Config,
|
Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static INTERNAL_SERVER_ERROR_TEXT: &[u8] = b"Internal Server Error";
|
||||||
|
static METHOD_NOT_ALLOWED_TEXT: &[u8] = b"Method Not Allowed";
|
||||||
|
static NOT_FOUND_TEXT: &[u8] = b"Not Found";
|
||||||
|
|
||||||
// This is dist/livereload.min.js from the LiveReload.js v3.1.0 release
|
// This is dist/livereload.min.js from the LiveReload.js v3.1.0 release
|
||||||
const LIVE_RELOAD: &str = include_str!("livereload.js");
|
const LIVE_RELOAD: &str = include_str!("livereload.js");
|
||||||
|
|
||||||
struct ErrorFilePaths {
|
async fn handle_request(req: Request<Body>, root: PathBuf) -> Result<Response<Body>> {
|
||||||
not_found: PathBuf,
|
// livereload.js is served using the LIVE_RELOAD str, not a file
|
||||||
|
if req.uri().path() == "/livereload.js" {
|
||||||
|
if req.method() == Method::GET {
|
||||||
|
return Ok(livereload_js());
|
||||||
|
} else {
|
||||||
|
return Ok(method_not_allowed());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn not_found<B>(
|
let result = hyper_staticfile::resolve(&root, &req).await.unwrap();
|
||||||
res: dev::ServiceResponse<B>,
|
match result {
|
||||||
) -> std::result::Result<ErrorHandlerResponse<B>, actix_web::Error> {
|
ResolveResult::MethodNotMatched => return Ok(method_not_allowed()),
|
||||||
let buf: Vec<u8> = {
|
ResolveResult::NotFound | ResolveResult::UriNotMatched => {
|
||||||
let error_files: &ErrorFilePaths = res.request().app_data().unwrap();
|
return Ok(not_found(Path::new(&root.join("404.html"))).await)
|
||||||
|
}
|
||||||
let mut fh = File::open(&error_files.not_found)?;
|
_ => (),
|
||||||
let mut buf: Vec<u8> = vec![];
|
|
||||||
let _ = fh.read_to_end(&mut buf)?;
|
|
||||||
buf
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_resp = HttpResponse::build(http::StatusCode::NOT_FOUND)
|
Ok(hyper_staticfile::ResponseBuilder::new().request(&req).build(result).unwrap())
|
||||||
.header(http::header::CONTENT_TYPE, http::header::HeaderValue::from_static("text/html"))
|
|
||||||
.body(buf);
|
|
||||||
|
|
||||||
Ok(ErrorHandlerResponse::Response(res.into_response(new_resp.into_body())))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn livereload_handler() -> HttpResponse {
|
fn livereload_js() -> Response<Body> {
|
||||||
HttpResponse::Ok().content_type("text/javascript").body(LIVE_RELOAD)
|
Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "text/javascript")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(LIVE_RELOAD.into())
|
||||||
|
.expect("Could not build livereload.js response")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn internal_server_error() -> Response<Body> {
|
||||||
|
Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(INTERNAL_SERVER_ERROR_TEXT.into())
|
||||||
|
.expect("Could not build Internal Server Error response")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn method_not_allowed() -> Response<Body> {
|
||||||
|
Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
|
.status(StatusCode::METHOD_NOT_ALLOWED)
|
||||||
|
.body(METHOD_NOT_ALLOWED_TEXT.into())
|
||||||
|
.expect("Could not build Method Not Allowed response")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn not_found(page_path: &Path) -> Response<Body> {
|
||||||
|
if let Ok(mut file) = tokio::fs::File::open(page_path).await {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
if file.read_to_end(&mut buf).await.is_ok() {
|
||||||
|
return Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "text/html")
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(buf.into())
|
||||||
|
.expect("Could not build Not Found response");
|
||||||
|
}
|
||||||
|
|
||||||
|
return internal_server_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a plain text response when page_path isn't available
|
||||||
|
Response::builder()
|
||||||
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(NOT_FOUND_TEXT.into())
|
||||||
|
.expect("Could not build Not Found response")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuild_done_handling(broadcaster: &Option<Sender>, res: Result<()>, reload_path: &str) {
|
fn rebuild_done_handling(broadcaster: &Option<Sender>, res: Result<()>, reload_path: &str) {
|
||||||
|
@ -202,28 +249,38 @@ pub fn serve(
|
||||||
let static_root = output_path.clone();
|
let static_root = output_path.clone();
|
||||||
let broadcaster = if !watch_only {
|
let broadcaster = if !watch_only {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let s = HttpServer::new(move || {
|
let addr = address.parse().unwrap();
|
||||||
let error_handlers =
|
|
||||||
ErrorHandlers::new().handler(http::StatusCode::NOT_FOUND, not_found);
|
let mut rt = tokio::runtime::Builder::new()
|
||||||
|
.enable_all()
|
||||||
|
.basic_scheduler()
|
||||||
|
.build()
|
||||||
|
.expect("Could not build tokio runtime");
|
||||||
|
|
||||||
|
rt.block_on(async {
|
||||||
|
let make_service = make_service_fn(move |_| {
|
||||||
|
let static_root = static_root.clone();
|
||||||
|
|
||||||
|
async {
|
||||||
|
Ok::<_, hyper::Error>(service_fn(move |req| {
|
||||||
|
handle_request(req, static_root.clone())
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let server = Server::bind(&addr).serve(make_service);
|
||||||
|
|
||||||
App::new()
|
|
||||||
.data(ErrorFilePaths { not_found: static_root.join("404.html") })
|
|
||||||
.wrap(error_handlers)
|
|
||||||
.route("/livereload.js", web::get().to(livereload_handler))
|
|
||||||
// Start a webserver that serves the `output_dir` directory
|
|
||||||
.service(fs::Files::new("/", &static_root).index_file("index.html"))
|
|
||||||
})
|
|
||||||
.bind(&address)
|
|
||||||
.expect("Can't start the webserver")
|
|
||||||
.shutdown_timeout(20);
|
|
||||||
println!("Web server is available at http://{}\n", &address);
|
println!("Web server is available at http://{}\n", &address);
|
||||||
if open {
|
if open {
|
||||||
if let Err(err) = open::that(format!("http://{}", &address)) {
|
if let Err(err) = open::that(format!("http://{}", &address)) {
|
||||||
eprintln!("Failed to open URL in your browser: {}", err);
|
eprintln!("Failed to open URL in your browser: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.run()
|
|
||||||
|
server.await.expect("Could not start web server");
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// The websocket for livereload
|
// The websocket for livereload
|
||||||
let ws_server = WebSocket::new(|output: Sender| {
|
let ws_server = WebSocket::new(|output: Sender| {
|
||||||
move |msg: Message| {
|
move |msg: Message| {
|
||||||
|
|
Loading…
Reference in a new issue