stree 0.1.0

A directory hierarchy mapping format, similar to mtree
Documentation
use std::path::Path;
use std::fmt::{self, Display};
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::{Entry, EntryConfig, Result, Error, err, FileType, Comparisons};
use pathstr::PathStr;
use walkdir::WalkDir;
use rayon::prelude::*;

#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};


/// A directory hierarchy
/// 
/// # Usage
/// ```no_run
/// use stree::Tree;
/// let tree = Tree::new(std::env::current_dir()?)?;
/// 
/// print!("{tree}");
/// ```
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "kebab-case"))]
pub struct Tree
{
    pub time: u64,
    pub top: PathStr,
    pub entries: Vec<Entry>
}

impl Tree
{
    /// Scan a path to construct a new tree
    pub fn new<P>(top: P, conf: Option<&EntryConfig>) -> Result<Self>
    where P: AsRef<Path>
    {
        let mut ptop: PathStr;
        if let Some(stop) = top.as_ref().to_str()
        && let Some(stripped) = stop.strip_suffix("/")
        {
            ptop = PathStr::from(stripped);
        }
        else
        {
            ptop = PathStr::from(top.as_ref());
        }
        if ptop.is_relative()
        {
            ptop = PathStr::from(format!("./{ptop}"));
        }
        let mut r = Self::default();
        r.time = SystemTime::now().duration_since(UNIX_EPOCH).map_err(|e| err!(e))?.as_secs();
        r.top = ptop.clone();
        if !r.top.is_dir()
        {
            r.entries.push(Entry::new(&r.top, conf)?);
        }
        else
        {
            let walk = WalkDir::new(&r.top).contents_first(false).follow_links(false);
            for entry in walk.into_iter()
            {
                r.entries.push(Entry::new(entry?.path(), conf)?);
            }
        }
        r.entries.shrink_to_fit();
        Ok(r)
    }

    /// Attempt to parse an STree manifest given its' path
    pub fn parse_from<P>(path: P) -> Result<Self>
    where P: AsRef<Path>
    {
        Self::from_str(std::fs::read_to_string(path.as_ref())?.as_str())
    }

    /// Attempt to parse an STree manifest from a string
    pub fn parse(input: &str) -> Result<Self>
    {
        Self::from_str(input)
    }

    /// Compare this tree against an on-disk tree
    pub fn compare<P>(&self, path: P) -> Result<Comparisons>
    where P: AsRef<Path>
    {
        let mut r = Comparisons::default();
        let path = PathStr::from(path.as_ref());
        let pathstring = path.string();
        let config = EntryConfig::from(self);
        let other = Tree::new(&path, Some(&config))?;

        let selfpaths: Vec<PathStr> = self.entries.iter().map(|e| e.name.clone()).collect();
        let otherpaths: Vec<PathStr> = other.entries.iter().map(|e| e.name.clone()).collect();
        let mut missing = Vec::new();
        let mut aliens = Vec::new();
        for p in selfpaths.iter().by_ref()
        {
            let comparator = PathStr::from(p.string().strip_suffix("./").unwrap_or(p.string().as_str()));
            let x = PathStr::from(format!("./{}", path.join(comparator.strip_prefix("./").unwrap_or(comparator.clone()))));
            if !otherpaths.contains(&x)
            {
                missing.push(PathStr::from(x.string().replace(&format!("./{pathstring}"), ".")))
            }
        }

        for p in otherpaths.iter().by_ref()
        {
            let ps = p.string();
            let x = PathStr::from(ps.replace(&format!("./{pathstring}"), "."));
            if !selfpaths.contains(&x)
            {
                aliens.push(x)
            }
        }

        r.inner = self.entries.clone().into_par_iter()
            .map(|entry| entry.compare(path.join(&entry.name), Some(&config)))
            .filter_map(|res| res.ok())
            .filter(|cmp| !cmp.is_empty())
            .collect();
        r.missing = Some(missing);
        r.aliens = Some(aliens);
        Ok(r)
    }
}

impl Display for Tree
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
    {
        let _ = writeln!(f, "#stree {}", self.time);
        for (n, entry) in self.entries.iter().enumerate()
        {
            let path = entry.name.string();
            let fixed = PathStr::from(path.replace(&self.top.string(), "."));
            let _ = write!(f,
                "{} type={} mode={} user={}:{} group={}:{} time.modify={}",
                fixed.percent_encode(),
                entry.kind,
                entry.mode.octal(),
                if let Some(uname) = &entry.user.name { &uname } else { "<unknown>" },
                entry.user.id,
                if let Some(gname) = &entry.group.name { &gname } else { "<unknown>" },
                entry.group.id,
                entry.mtime
            );
            if matches!(entry.kind, FileType::Regular)
            {
                let _ = write!(f, " size={}", entry.size);
            }
            #[cfg(feature = "magic")]
            if let Some(ref mime) = entry.mime { let _ = write!(f, " mime.type={mime}"); }
            #[cfg(feature = "magic")]
            if let Some(ref enc) = entry.encoding
            && entry.kind != FileType::Directory
            {
                let _ = write!(f, " mime.encoding={enc}");
            }
            
            if let Some(ino) = entry.inode { let _ = write!(f, " inode={ino}"); }
            if let Some(nl) = entry.nlink { let _ = write!(f, " nlink={nl}"); }
            if let Some(ctime) = entry.ctime { let _ = write!(f, " time.change={ctime}"); }
            if let Some(atime) = entry.atime { let _ = write!(f, " time.access={atime}"); }
            if let Some(btime) = entry.btime { let _ = write!(f, " time.birth={btime}"); }
            if let Some(dev) = entry.device { let _ = write!(f, " device={dev}"); }
            if let Some(rdev) = entry.special_device
            && matches!(entry.kind, FileType::Character|FileType::Block)
            {
                let _ = write!(f, " device.special={rdev}");
            }
            if let Some(ref cksums) = entry.checksums
            {
                cksums.iter().for_each(|sum| { let _ = write!(f, " hash.{sum}"); });
            }
            if let Some(ref target) = entry.link_target { let _ = write!(f, " link.target={target}"); }
            if let Some(ref real) = entry.link_real { let _ = write!(f, " link.real={real}"); }
            if n != self.entries.len() - 1
            {
                let _ = writeln!(f, "");
            }
        }
        Ok(())
    }
}

impl FromStr for Tree
{
    type Err = Error;
    fn from_str(s: &str) -> Result<Self>
    {
        crate::parse::parse_tree(s)
    }
}