use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::Error;
use crate::config::Config;
use crate::favicon::FaviconSet;
use crate::render::{BuiltSite, Theme};
pub(crate) struct Rebuilder {
config_path: PathBuf,
favicon_cache: Option<FaviconCacheEntry>,
}
struct FaviconCacheEntry {
set: FaviconSet,
source_path: PathBuf,
mtime: SystemTime,
}
impl Rebuilder {
#[cfg(test)]
pub(crate) fn new(config_path: PathBuf) -> Self {
Self {
config_path,
favicon_cache: None,
}
}
pub(crate) fn with_initial_favicon(
config_path: PathBuf,
favicon: Option<(FaviconSet, PathBuf, SystemTime)>,
) -> Self {
let favicon_cache = favicon.map(|(set, source_path, mtime)| FaviconCacheEntry {
set,
source_path,
mtime,
});
Self {
config_path,
favicon_cache,
}
}
pub(crate) fn config_path(&self) -> &Path {
&self.config_path
}
pub(crate) fn next_built(&mut self) -> Result<BuiltSite, Error> {
let config = Config::from_path(&self.config_path)?;
let theme = Theme::load(&config)?;
let favicon = self.refresh_favicon(&config)?;
BuiltSite::build_with_favicon(&config, &theme, favicon)
}
fn refresh_favicon(&mut self, config: &Config) -> Result<Option<FaviconSet>, Error> {
let Some(path) = config.favicon.as_ref() else {
self.favicon_cache = None;
return Ok(None);
};
let mtime = favicon_mtime(path)?;
if let Some(cache) = &self.favicon_cache
&& cache.source_path == *path
&& cache.mtime == mtime
{
return Ok(Some(cache.set.clone()));
}
let set = FaviconSet::generate(path, &config.title)?;
self.favicon_cache = Some(FaviconCacheEntry {
set: set.clone(),
source_path: path.clone(),
mtime,
});
Ok(Some(set))
}
#[cfg(test)]
pub(crate) fn cached_favicon_mtime(&self) -> Option<SystemTime> {
self.favicon_cache.as_ref().map(|c| c.mtime)
}
}
pub(crate) fn favicon_mtime(path: &Path) -> Result<SystemTime, Error> {
Ok(std::fs::metadata(path)?.modified()?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::write_file;
use std::fs;
use tempfile::TempDir;
fn write_minimal_site(dir: &Path, favicon_rel: &str) {
write_file(
&dir.join("aphid.toml"),
&format!(
"title = \"T\"\nbase_url = \"http://localhost\"\nfavicon = \"{favicon_rel}\"\n"
),
);
let img = image::RgbImage::from_pixel(1, 1, image::Rgb([255, 255, 255]));
img.save(dir.join(favicon_rel)).unwrap();
fs::create_dir_all(dir.join("content/blog")).unwrap();
fs::create_dir_all(dir.join("content/wiki")).unwrap();
fs::create_dir_all(dir.join("content/pages")).unwrap();
}
#[test]
fn favicon_reused_when_source_unchanged() {
let dir = TempDir::new().unwrap();
write_minimal_site(dir.path(), "favicon.png");
let mut rebuilder = Rebuilder::new(dir.path().join("aphid.toml"));
rebuilder.next_built().unwrap();
let mtime_after_first = rebuilder.cached_favicon_mtime().unwrap();
rebuilder.next_built().unwrap();
assert_eq!(rebuilder.cached_favicon_mtime().unwrap(), mtime_after_first);
}
#[test]
fn favicon_regenerates_when_source_mtime_changes() {
let dir = TempDir::new().unwrap();
write_minimal_site(dir.path(), "favicon.png");
let mut rebuilder = Rebuilder::new(dir.path().join("aphid.toml"));
rebuilder.next_built().unwrap();
let first_mtime = rebuilder.cached_favicon_mtime().unwrap();
let later = first_mtime + std::time::Duration::from_secs(2);
let path = dir.path().join("favicon.png");
let f = fs::File::options().write(true).open(&path).unwrap();
f.set_modified(later).unwrap();
drop(f);
rebuilder.next_built().unwrap();
assert_eq!(rebuilder.cached_favicon_mtime().unwrap(), later);
}
#[test]
fn favicon_cache_dropped_when_config_removes_favicon() {
let dir = TempDir::new().unwrap();
write_minimal_site(dir.path(), "favicon.png");
let mut rebuilder = Rebuilder::new(dir.path().join("aphid.toml"));
rebuilder.next_built().unwrap();
assert!(rebuilder.cached_favicon_mtime().is_some());
write_file(
&dir.path().join("aphid.toml"),
"title = \"T\"\nbase_url = \"http://localhost\"\n",
);
rebuilder.next_built().unwrap();
assert!(rebuilder.cached_favicon_mtime().is_none());
}
}