diff --git a/manager/Cargo.lock b/manager/Cargo.lock index b9d5eb5..ba59776 100644 --- a/manager/Cargo.lock +++ b/manager/Cargo.lock @@ -120,6 +120,17 @@ dependencies = [ "vec_map", ] +[[package]] +name = "compound-error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106f619aa16d817037b5820326364166e67c52d1a78eb6f37ba354ec82b617c5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -152,6 +163,7 @@ name = "dotfiles" version = "0.1.0" dependencies = [ "async-recursion", + "compound-error", "futures", "log", "pretty_env_logger", diff --git a/manager/Cargo.toml b/manager/Cargo.toml index 3c6164e..6978341 100644 --- a/manager/Cargo.toml +++ b/manager/Cargo.toml @@ -12,6 +12,7 @@ futures = "0.3.13" async-recursion = "0.3.2" xdg = "2.2.0" templar = "0.5.0" +compound-error = "0.1.2" [dependencies.serde] version = "1.0.125" diff --git a/manager/src/builder.rs b/manager/src/builder.rs index 21bbcd9..507073d 100644 --- a/manager/src/builder.rs +++ b/manager/src/builder.rs @@ -1,13 +1,14 @@ +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::{self, ErrorKind}; +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::try_join; +use tokio::join; #[derive(Serialize)] struct TemplateContext<'a> { @@ -18,10 +19,14 @@ struct TemplateContext<'a> { const TEMPLATE_EXTENSION: &str = "tpl"; -pub async fn build_tree(cfg: &Config) -> io::Result<()> { +pub async fn build_tree(cfg: &Config) -> Result<(), Errors> { let tpl = Templar::global(); - let hostname = read_to_string("/etc/hostname").await?; + 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( @@ -43,7 +48,7 @@ async fn dir( ctx: &StandardContext, tpl: &Templar, relative: PathBuf, -) -> io::Result<()> { +) -> Result<(), Errors> { let template_path = cfg.template_dir.join(&relative); let build_path = cfg.build_dir.join(&relative); @@ -52,16 +57,18 @@ async fn dir( match create_dir(&build_path).await { Ok(_) => {} Err(e) if e.kind() == ErrorKind::AlreadyExists => {} - Err(e) => return Err(e), + Err(e) => return Err(e.with_location(&build_path).into()), } - let mut walker = read_dir(&template_path).await?; + 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? { - let meta = entry.metadata().await?; + 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() { @@ -71,23 +78,25 @@ async fn dir( } } - let dirs = async { - join_all(dir_tasks) - .await - .into_iter() - .collect::, _>>() - }; + 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 files = async { - join_all(file_tasks) - .await - .into_iter() - .collect::, _>>() - }; + let mut errors: Errors = files + .into_iter() + .filter_map(|r| r.err()) + .collect::>() + .into(); - try_join!(dirs, files)?; + for error in dirs.into_iter().filter_map(|r| r.err()) { + errors.join(error); + } - Ok(()) + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } } async fn file( @@ -95,31 +104,35 @@ async fn file( ctx: &StandardContext, tpl: &Templar, relative: PathBuf, -) -> io::Result<()> { +) -> Result<(), Error> { let template_path = cfg.template_dir.join(&relative); let mut new_path = cfg.build_dir.join(&relative); info!("rendering {:?}", template_path); - let file_data = read_to_string(&template_path).await?; if template_path.extension() == Some(OsStr::new(TEMPLATE_EXTENSION)) { // perform templating - // TODO: error handling + let file_str = read_to_string(&template_path) + .await + .with_location(&template_path)?; + let rendered = tpl - .parse_template(&file_data) - .expect("failed to parse template") + .parse_template(&file_str) + .with_location(&template_path)? .render(ctx) - .expect("failed to render template"); + .with_location(&template_path)?; // remove template file extension new_path.set_extension(""); // write the rendered file - write(&new_path, &rendered).await?; + 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?; + copy(&template_path, &new_path) + .await + .with_location(&template_path)?; } Ok(()) diff --git a/manager/src/error.rs b/manager/src/error.rs new file mode 100644 index 0000000..8364284 --- /dev/null +++ b/manager/src/error.rs @@ -0,0 +1,91 @@ +use compound_error::CompoundError; +use std::io; +use std::path::{Path, PathBuf}; + +#[derive(Default)] +pub struct Errors { + errors: Vec, +} + +pub struct Error { + location: PathBuf, + inner: InnerError, +} + +#[derive(CompoundError, Debug)] +pub enum InnerError { + IoErr(io::Error), + TemplateErr(templar::TemplarError), +} + +impl From> for Errors { + fn from(errors: Vec) -> Self { + Errors { errors } + } +} + +impl From for Errors +where + E: Into, +{ + fn from(error: E) -> Self { + Errors { + errors: vec![error.into()], + } + } +} + +impl Errors { + pub fn join(&mut self, mut other: Errors) { + self.errors.append(&mut other.errors); + } + + pub fn is_empty(&self) -> bool { + self.errors.is_empty() + } + + pub fn log(self) { + if self.errors.is_empty() { + return; + } + + error!("{} errors occured:", self.errors.len()); + for (i, error) in self.errors.iter().enumerate() { + error!("{:.2}. {:?}", i, error.location); + error!(" {:?}", error.inner); + } + } +} + +pub trait ErrorLocation { + type Err; + fn with_location(self, path: &Path) -> Self::Err; +} + +impl ErrorLocation for T +where + T: Into, +{ + type Err = Error; + + fn with_location(self, path: &Path) -> Error { + Error { + location: path.to_owned(), + inner: self.into(), + } + } +} + +impl ErrorLocation for Result +where + E: Into, +{ + type Err = Result; + + fn with_location(self, path: &Path) -> Result { + self.map_err(|e| Error { + location: path.to_owned(), + inner: e.into(), + }) + } +} diff --git a/manager/src/linker.rs b/manager/src/linker.rs index e9b3762..72c0e31 100644 --- a/manager/src/linker.rs +++ b/manager/src/linker.rs @@ -1,17 +1,18 @@ +use crate::error::{Error, ErrorLocation, Errors}; use crate::Config; use async_recursion::async_recursion; use futures::future::join_all; -use std::io::{self, ErrorKind}; +use std::io::ErrorKind; use std::path::PathBuf; use tokio::fs::{create_dir, read_dir, remove_file, symlink}; -use tokio::try_join; +use tokio::join; -pub async fn link_tree(cfg: &Config) -> io::Result<()> { +pub async fn link_tree(cfg: &Config) -> Result<(), Errors> { dir(cfg, PathBuf::new()).await } #[async_recursion] -async fn dir(cfg: &Config, relative: PathBuf) -> io::Result<()> { +async fn dir(cfg: &Config, relative: PathBuf) -> Result<(), Errors> { let build_path = cfg.build_dir.join(&relative); let link_path = cfg.link_dir.join(&relative); @@ -20,16 +21,16 @@ async fn dir(cfg: &Config, relative: PathBuf) -> io::Result<()> { match create_dir(&link_path).await { Ok(_) => {} Err(e) if e.kind() == ErrorKind::AlreadyExists => {} - Err(e) => return Err(e), + Err(e) => return Err(e.with_location(&link_path).into()), } - let mut walker = read_dir(&build_path).await?; + let mut walker = read_dir(&build_path).await.with_location(&build_path)?; let mut dir_tasks = vec![]; let mut file_tasks = vec![]; - while let Some(entry) = walker.next_entry().await? { - let meta = entry.metadata().await?; + while let Some(entry) = walker.next_entry().await.with_location(&build_path)? { + let meta = entry.metadata().await.with_location(&entry.path())?; let new_relative = relative.join(entry.file_name()); if meta.is_dir() { @@ -39,26 +40,28 @@ async fn dir(cfg: &Config, relative: PathBuf) -> io::Result<()> { } } - let dirs = async { - join_all(dir_tasks) - .await - .into_iter() - .collect::, _>>() - }; + 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 files = async { - join_all(file_tasks) - .await - .into_iter() - .collect::, _>>() - }; + let mut errors: Errors = files + .into_iter() + .filter_map(|r| r.err()) + .collect::>() + .into(); - try_join!(dirs, files)?; + for error in dirs.into_iter().filter_map(|r| r.err()) { + errors.join(error); + } - Ok(()) + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } } -async fn file(cfg: &Config, relative: PathBuf) -> io::Result<()> { +async fn file(cfg: &Config, relative: PathBuf) -> Result<(), Error> { let build_path = cfg.build_dir.join(&relative); let link_path = cfg.link_dir.join(&relative); @@ -67,7 +70,7 @@ async fn file(cfg: &Config, relative: PathBuf) -> io::Result<()> { info!("removed existing file {:?}", link_path); } Err(e) if e.kind() == ErrorKind::NotFound => {} - Err(e) => return Err(e), + Err(e) => return Err(e.with_location(&link_path)), }; info!("linking {:?} to {:?}", link_path, build_path); @@ -85,7 +88,9 @@ async fn file(cfg: &Config, relative: PathBuf) -> io::Result<()> { relative_symlink }; - symlink(symlink_content, link_path).await?; + symlink(symlink_content, &link_path) + .await + .with_location(&link_path)?; Ok(()) } diff --git a/manager/src/main.rs b/manager/src/main.rs index dd11ddb..4d52cea 100644 --- a/manager/src/main.rs +++ b/manager/src/main.rs @@ -2,15 +2,16 @@ extern crate log; mod builder; +mod error; mod linker; use builder::build_tree; +use error::Errors; use linker::link_tree; use log::LevelFilter; use std::env; use std::path::PathBuf; use structopt::StructOpt; -use tokio::io; #[derive(StructOpt)] struct Opt { @@ -45,7 +46,14 @@ pub enum ColorMode { } #[tokio::main] -async fn main() -> io::Result<()> { +async fn main() { + match run().await { + Ok(_) => {} + Err(errors) => errors.log(), + } +} + +async fn run() -> Result<(), Errors> { let opt = Opt::from_args(); let filter_level = match opt.verbosity {