lazin 1.0.4

Primarily a dotfiles manager
use std::{
    collections::BTreeSet,
    env, fs,
    path::{Path, PathBuf},
};

use lazin_error::{Context, LazinResult};

use crate::{config, encryption_management::GPG_EXTENSION, error::LazinError};

#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum Encryption {
    #[default]
    Disabled,
    Enabled {
        recipient: String,
    },
}

impl From<config::module::Encryption> for Encryption {
    fn from(value: config::module::Encryption) -> Self {
        match value {
            config::module::Encryption::Disabled => Self::Disabled,
            config::module::Encryption::Enabled { recipient } => Self::Enabled { recipient },
        }
    }
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Value {
    pub source: PathBuf,
    pub target: PathBuf,
    pub encryption: Encryption,
}

impl Value {
    fn new(source: PathBuf, target: PathBuf, encryption: Encryption) -> Self {
        Self {
            source,
            target,
            encryption,
        }
    }
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Module {
    pub name: String,
    pub values: BTreeSet<Value>,
}

impl Module {
    pub fn try_new(module: config::module::Module) -> LazinResult<Self> {
        let values = module.values.into_iter().try_fold(
            BTreeSet::new(),
            |mut acc, (source, value)| -> LazinResult<BTreeSet<Value>> {
                expand_module(&mut acc, source.into(), value)?;

                Ok(acc)
            },
        )?;

        Ok(Self {
            name: module.name.into(),
            values,
        })
    }
}

fn expand_module(
    buffer: &mut BTreeSet<Value>,
    source: PathBuf,
    value: config::module::Value,
) -> LazinResult {
    fn walk(
        buffer: &mut BTreeSet<Value>,
        source: PathBuf,
        target: PathBuf,
        config: &config::module::Config,
    ) -> LazinResult<()> {
        if !source.is_dir() {
            // When expanding encrypted directories files ending with .gpg
            // get used as source but they shouldn't be.
            // Source is modified and removes .gpg
            let mut source = source;
            let mut target = expand_tilde(&target)?;
            if let Some(extension) = source.extension()
                && extension == GPG_EXTENSION
            {
                source.set_extension("");
                target.set_extension("");
            }

            buffer.insert(Value::new(source, target, config.encryption.clone().into()));
        } else {
            for child in fs::read_dir(&source).context("Failed to read child directory")? {
                let child = child.context("Failed to get child directory")?.file_name();
                let child_source = source.join(&child);
                let child_target = target.join(&child);
                walk(buffer, child_source, child_target, config)?;
            }
        }
        Ok(())
    }

    walk(buffer, source, value.path, &value.config)
}

fn expand_tilde(path: &Path) -> LazinResult<PathBuf> {
    if !path.starts_with("~") {
        return Ok(path.into());
    }

    let home_dir = env::var_os("HOME").ok_or(LazinError::Custom(
        "unable to determine HOME directory; Lazin cannot run without detecting the home directory",
    ))?;
    let home_dir = PathBuf::from(home_dir);
    let stripped_path = path
        .strip_prefix("~")
        .context("Failed to strip tilde prefix")?;

    Ok(home_dir.join(stripped_path))
}