use crate::error::{Error, ErrorLocation, Errors}; use crate::{ColorMode, Config}; use async_recursion::async_recursion; use futures::future::join_all; use serde::Serialize; use std::ffi::OsStr; use std::io::ErrorKind; use std::path::PathBuf; use templar::{Context, InnerData, StandardContext, Templar}; use tokio::fs::{copy, create_dir, read_dir, read_to_string, write}; use tokio::join; #[derive(Serialize)] struct TemplateContext<'a> { hostname: &'a str, lightmode: bool, darkmode: bool, } const TEMPLATE_EXTENSION: &str = "tpl"; pub async fn build_tree(cfg: &Config) -> Result<(), Errors> { let tpl = Templar::global(); let hostname_path: PathBuf = "/etc/hostname".into(); let hostname = read_to_string(&hostname_path) .await .with_location(&hostname_path)?; let darkmode = cfg.color == ColorMode::Dark; let ctx = StandardContext::new(); ctx.set( InnerData::new(TemplateContext { hostname: hostname.trim(), darkmode, lightmode: !darkmode, }) .expect("serialize template context"), ) .expect("set template context"); dir(cfg, &ctx, &tpl, PathBuf::new()).await } #[async_recursion] async fn dir( cfg: &Config, ctx: &StandardContext, tpl: &Templar, relative: PathBuf, ) -> Result<(), Errors> { let template_path = cfg.template_dir.join(&relative); let build_path = cfg.build_dir.join(&relative); info!("traversing {:?}", template_path); match create_dir(&build_path).await { Ok(_) => {} Err(e) if e.kind() == ErrorKind::AlreadyExists => {} Err(e) => return Err(e.with_location(&build_path).into()), } let mut walker = read_dir(&template_path) .await .with_location(&template_path)?; let mut dir_tasks = vec![]; let mut file_tasks = vec![]; while let Some(entry) = walker.next_entry().await.with_location(&&template_path)? { let meta = entry.metadata().await.with_location(&entry.path())?; let new_relative = relative.join(entry.file_name()); if meta.is_dir() { dir_tasks.push(dir(cfg, ctx, tpl, new_relative)); } else if meta.is_file() { file_tasks.push(file(cfg, ctx, tpl, new_relative)); } } let dirs = async { join_all(dir_tasks).await.into_iter().collect::>() }; let files = async { join_all(file_tasks).await.into_iter().collect::>() }; let (dirs, files) = join!(dirs, files); let mut errors: Errors = files .into_iter() .filter_map(|r| r.err()) .collect::>() .into(); for error in dirs.into_iter().filter_map(|r| r.err()) { errors.join(error); } if errors.is_empty() { Ok(()) } else { Err(errors) } } async fn file( cfg: &Config, ctx: &StandardContext, tpl: &Templar, relative: PathBuf, ) -> Result<(), Error> { let template_path = cfg.template_dir.join(&relative); let mut new_path = cfg.build_dir.join(&relative); info!("rendering {:?}", template_path); if template_path.extension() == Some(OsStr::new(TEMPLATE_EXTENSION)) { // perform templating let file_str = read_to_string(&template_path) .await .with_location(&template_path)?; let rendered = tpl .parse_template(&file_str) .with_location(&template_path)? .render(ctx) .with_location(&template_path)?; // remove template file extension new_path.set_extension(""); // write the rendered file write(&new_path, &rendered).await.with_location(&new_path)?; } else { // else just copy the file info!("copying {:?}", template_path); copy(&template_path, &new_path) .await .with_location(&template_path)?; } Ok(()) }