mod build_dir;
mod escape;
use std::{
io, mem, net::{Ipv4Addr, Ipv6Addr, SocketAddr}, path::PathBuf, sync::{Arc, RwLock, mpsc}, thread::{self, JoinHandle}, time::{Duration, Instant},
};
use rouille::{Request, Response, Server};
use crate::{Mime, live::build_dir::Current, raw, serve::Url};
trait RouilleHandler: Send + Sync + 'static + Fn(&Request) -> Response {}
impl<T: Send + Sync + 'static + Fn(&Request) -> Response> RouilleHandler for T {}
pub struct LiveServer {
dir: PathBuf,
cfg: ProvidedConfig,
autorefresh: bool,
}
#[derive(Clone)]
enum ProvidedConfig {
Live(PathBuf),
Static(raw::Config),
}
impl LiveServer {
pub fn live_config(cfg: impl Into<PathBuf>, dir: impl Into<PathBuf>) -> Self {
Self {
dir: dir.into(),
cfg: ProvidedConfig::Live(cfg.into()),
autorefresh: true,
}
}
pub fn static_config(cfg: raw::Config, dir: impl Into<PathBuf>) -> Self {
Self {
dir: dir.into(),
cfg: ProvidedConfig::Static(cfg),
autorefresh: true,
}
}
pub fn without_refresh(mut self) -> Self {
self.autorefresh = false;
self
}
fn server(
&self,
port: impl Into<Option<u16>>,
) -> io::Result<(Server<impl RouilleHandler + 'static>, Option<JoinHandle<()>>)> {
let autorefresh = self.autorefresh;
let (data, watcher) = if autorefresh {
let (current, watcher) = build_dir::watch(self.cfg.clone(), self.dir.clone());
(current, Some(watcher))
} else {
let current = build_dir::once(self.cfg.clone(), self.dir.clone());
(Arc::new(RwLock::new(current)), None)
};
let port = port.into().unwrap_or(0);
let data2 = data.clone();
let res = rouille::Server::new(
[(Ipv4Addr::LOCALHOST, port).into(), (Ipv6Addr::LOCALHOST, port).into()].as_ref(),
move |req| serve(&data, autorefresh, req),
);
match res {
Ok(server) => Ok((server.pool_size(1), watcher)),
Err(e) => {
mem::drop(data2);
if let Some(w) = watcher {
w.join().unwrap();
}
Err(io::Error::other(format!("failed to make server: {e}")))
}
}
}
pub fn listen(&self, port: impl Into<Option<u16>>) -> io::Result<()> {
let (server, worker) = self.server(port)?;
server.run();
if let Some(w) = worker {
w.join().unwrap();
}
Ok(())
}
pub fn listen_background(&self, port: impl Into<Option<u16>>) -> io::Result<BackgroundServer> {
let (server, watcher) = self.server(port)?;
let address = server.server_addr();
let (stop_send, stop_recv) = mpsc::sync_channel(0);
let handle = thread::spawn(move || {
while stop_recv.try_recv() == Err(mpsc::TryRecvError::Empty) {
server.poll_timeout(Duration::from_millis(100));
}
});
Ok(BackgroundServer {
address,
stop: stop_send,
handle,
watcher,
})
}
}
pub struct BackgroundServer {
pub address: SocketAddr,
stop: mpsc::SyncSender<()>,
handle: JoinHandle<()>,
watcher: Option<JoinHandle<()>>,
}
impl BackgroundServer {
pub fn stop(self) {
let _ = self.stop.send(());
self.handle.join().unwrap();
if let Some(w) = self.watcher {
w.join().unwrap();
}
}
}
const UNIQ: &str = "/.breb-sys/SzRPbelaEjcYBzDhwBxb4A/";
fn serve(watched: &RwLock<Current>, autorefresh: bool, req: &Request) -> Response {
const SERVER: &str = concat!("blog-reblog/", env!("CARGO_PKG_VERSION"));
let start = Instant::now();
let lock = watched.read().unwrap();
let data = &*lock;
let resp = if let Some(suffix) = req.raw_url().strip_prefix(UNIQ) {
let name = match suffix.split_once('?') {
Some((l, _)) => l,
None => suffix,
};
serve_internal(data, autorefresh, name)
} else {
let resp = serve_blog(data, autorefresh, req);
println!(
"{}: {} {:?} -({}us)-> {}",
req.remote_addr(),
req.method(),
req.url(),
start.elapsed().as_micros(),
resp.status_code,
);
resp
};
resp.with_unique_header("Server", SERVER).with_etag(req, format!("{:x}", data.version))
}
fn serve_blog(data: &Current, autorefresh: bool, req: &Request) -> Response {
if req.method() != "GET" && req.method() != "HEAD" {
return Response::text("only GET (and HEAD) allowed").with_status_code(405);
}
let raw_url = req.url();
let blog = match &data.blog {
Ok(b) => b,
Err(e) => return error(&raw_url, e),
};
let mut url = PathBuf::from(&raw_url);
if let Ok(rel) = url.strip_prefix("/") {
url = rel.to_owned();
}
let Some(servable) = blog.servable(Url(url.clone())) else {
return not_found(&raw_url);
};
if servable.content_type() == &Mime::HTML && !raw_url.ends_with("/") {
let mut new_url = raw_url;
new_url.push('/');
return Response::redirect_302(new_url);
}
let mut body = vec![];
if let Err(e) = servable.render(&mut body) {
return error(&raw_url, &e);
}
if autorefresh && servable.content_type() == &Mime::HTML {
use io::Write;
write!(
&mut body,
r##"<script data-uniq={:?} data-version="{:x}">{}</script>"##,
UNIQ,
data.version,
include_str!("refresher.js"),
)
.expect("write to vec is infallible");
}
Response::from_data(servable.content_type(), body)
}
fn serve_internal(data: &Current, autorefresh: bool, url: &str) -> Response {
match url {
"version" if autorefresh => Response::text(format!("{:x}", data.version)),
_ => Response::text("that page doesn't exist c:").with_status_code(404),
}
}
fn error(url: &str, e: &io::Error) -> Response {
Response::html(escape::render!("500.html", url = url, error = e)).with_status_code(500)
}
fn not_found(url: &str) -> Response {
Response::html(escape::render!("404.html", url = url)).with_status_code(404)
}