breb 0.5.0

the blog/reblog library and command-line tool
Documentation
use std::{
	env,
	fs::{self, File},
	path::PathBuf,
	sync::{Arc, Barrier},
	time::Instant,
};

use rayon::iter::{IntoParallelIterator, ParallelIterator};

use crate::{LiveServer, raw};

macro_rules! usage {
	(@show $cmd:ident) => {
		print!(concat!(
			"usage: {cmd} help\n",
			"         show this usage menu\n",
			"       {cmd} run <input dir> <output dir>\n",
			"         generate the blog, writing it to the output directory\n",
			"       {cmd} serve <input dir> [<port>]\n",
			"         serve the blog on the given port (default: 8080)\n"
		), cmd=$cmd);
	};
	($cmd:ident) => {{
		$crate::cli::usage!(@show $cmd);
		return;
	}};
	($cmd:ident; $msg:literal$(, $arg:expr)* $(,)?) => {{
		$crate::cli::usage!(@show $cmd);
		println!(concat!("\nERROR: ", $msg), $($arg),*);
		return;
	}};
}
pub(super) use usage;

pub fn run(cmd: &str, mut args: env::Args, cfg: raw::Config, start: Instant) {
	let (in_dir, out_dir) = match (args.next(), args.next(), args.next()) {
		(Some(i), Some(o), None) => (PathBuf::from(i), PathBuf::from(o)),
		(None, _, _) => usage!(cmd; "bad `run`: missing <input dir>"),
		(_, None, _) => usage!(cmd; "bad `run`: missing <output dir>"),
		(_, _, Some(_)) => usage!(cmd; "bad `run`: extra argument"),
	};

	if !in_dir.is_dir() {
		usage!(cmd; "bad `run`: <input dir> is not a directory");
	}
	if !out_dir.is_dir() {
		fs::create_dir_all(&out_dir).expect("failed to create <output dir>");
	}

	let blog = cfg.build(&in_dir).expect("loading blog metadata failed");
	let servables = blog.servables();
	println!("blog loaded in {}us w/ {} urls", start.elapsed().as_micros(), servables.len());

	let start = Instant::now();
	servables.into_par_iter().for_each(|servable| {
		let out_path = out_dir.join(servable.filepath());
		if let Some(parent) = out_path.parent()
			&& let Err(e) = fs::create_dir_all(parent)
		{
			eprintln!("ERROR: {:?}: creating {parent:?}: {e}", servable.url());
			return;
		}
		if let Err(e) = File::create(&out_path).and_then(|mut out| servable.render(&mut out)) {
			eprintln!("ERROR: rendering {:?} ({out_path:?}: {e}", servable.url());
			std::process::exit(1); // to preserve the tmpdirs
		}
	});
	println!("blog rendered in {}us", start.elapsed().as_micros());
}

pub fn serve(cmd: &str, mut args: env::Args, cfg: raw::Config, start: Instant) {
	let (in_dir, port) = match (args.next(), args.next(), args.next()) {
		(Some(i), Some(p), None) => match p.parse() {
			Ok(p) => (i, p),
			Err(e) => usage!(cmd; "bad `serve`: bad <port>: {}", e),
		},
		(Some(i), None, _) => (i, 8080),
		(None, _, _) => usage!(cmd; "bad `serve`: missing <input dir>"),
		(_, _, Some(_)) => usage!(cmd; "bad `serve`: extra argument"),
	};

	let waiter = Arc::new(Barrier::new(2));
	ctrlc::set_handler({
		let mut waiter = Some(waiter.clone());
		move || {
			if let Some(w) = waiter.take() {
				println!(" shutting down...");
				w.wait();
			} else {
				println!(" please wait, we're stopping!");
			}
		}
	})
	.expect("failed to set ctrl+c handler");

	println!("serving {in_dir:?} at http://localhost:{port}/");

	let server = LiveServer::static_config(cfg, in_dir);

	let bg = server.listen_background(port).expect("failed to start server");
	println!("server started after {}us", start.elapsed().as_micros());

	// wait for ctrl+c to come...
	waiter.wait();
	let start = Instant::now();
	println!("stopping the server...");
	// ...then stop the server
	bg.stop();
	println!("server stopped in {}us. goodbye!", start.elapsed().as_micros());
}