breb 0.5.0

the blog/reblog library and command-line tool
Documentation
//! [`Serve`] implementation for serving files as they are

use std::{borrow::Cow, collections::HashMap, fs::File, io, path::Path};

use walkdir::WalkDir;

use crate::{Blog, Metadata, Mime};

use super::{Dir, Mount, Serve, Url};

/// [`Serve`]s files exactly as they are on-disk.
#[derive(Debug)]
pub struct AsIs {
	/// where this serve is serving from and under what path.
	mount: Mount,
	/// extension-to-MIME mappings to add to or override [the defaults](Serve::DEFAULT_MIME).
	mimes: HashMap<Cow<'static, str>, Mime>,
	/// whether to feed the blog information based on the contents
	metafree: bool,
}

impl AsIs {
	/// serve files from the given directory under the given url
	pub fn new(url: impl Into<Url>, dir: impl Into<Dir>) -> Self {
		Self {
			mount: Mount::new(url, dir),
			mimes: Default::default(),
			metafree: false,
		}
	}

	fn get_mime(&self, url: &Url) -> Mime {
		let Some(ext) = url.extension().and_then(|t| t.to_str()) else {
			return Mime::BYTES;
		};
		self.mimes.get(ext).cloned().or_else(|| Mime::from_ext(ext)).unwrap_or(Mime::BYTES)
	}

	/// add an extension-to-mime mapping
	pub fn mime(mut self, ext: impl Into<Cow<'static, str>>, mime: Mime) -> Self {
		self.mimes.insert(ext.into(), mime);
		self
	}

	/// add multiple extension-to-mime mappings
	pub fn mimes(mut self, addtl: impl Iterator<Item = (Cow<'static, str>, Mime)>) -> Self {
		self.mimes.extend(addtl);
		self
	}

	/// set this [`AsIs`] to not add any metadata except servable urls
	///
	/// by default, it will add any `.css` files as stylesheets.
	pub fn metafree(mut self) -> Self {
		self.metafree = true;
		self
	}
}

impl Serve for AsIs {
	fn base(&self) -> &Url {
		&self.mount.url
	}

	fn scan(&self, dir: &Path) -> Result<Metadata, std::io::Error> {
		let mut res = Metadata::default();
		let base = dir.join(&*self.mount.dir);
		for de in WalkDir::new(&base).follow_links(true) {
			let de = de?;
			if !de.file_type().is_file() {
				continue;
			}
			let Ok(rel) = de.path().strip_prefix(&base) else {
				continue;
			};
			let url = Url(self.mount.url.join(rel));
			let mime = self.get_mime(&url);
			if !self.metafree
				&& mime == Mime::CSS
				&& let Some(name) = url.file_name().and_then(|os| os.to_str())
			{
				res.stylesheets.insert(name.to_owned(), url.clone());
			}
			res.files.insert(url, mime);
		}
		Ok(res)
	}

	fn render(&self, blog: &Blog, url: &Url, into: &mut dyn std::io::Write) -> io::Result<()> {
		let in_dir = self.mount.dir(url).ok_or(io::Error::new(
			io::ErrorKind::InvalidInput,
			format!("url {:?} not under base {:?}", url, self.mount.url),
		))?;
		let mut f = File::open(blog.dir.join(&*in_dir))?;
		io::copy(&mut f, into)?;
		Ok(())
	}
}