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};
#[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
{
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)
}
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())
}
pub fn parse(input: &str) -> Result<Self>
{
Self::from_str(input)
}
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)
}
}