use std::collections::vec_deque::VecDeque;
use std::fs;
use std::fs::File;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::{error, warn};
use crate::entry::KindMeta;
use crate::monitor::Monitor;
use crate::stats::LiveTreeIterStats;
use crate::*;
#[derive(Clone)]
pub struct LiveTree {
path: PathBuf,
}
impl LiveTree {
pub fn open<P: AsRef<Path>>(path: P) -> Result<LiveTree> {
Ok(LiveTree {
path: path.as_ref().to_path_buf(),
})
}
fn relative_path(&self, apath: &Apath) -> PathBuf {
apath.below(&self.path)
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn open_file(&self, entry: &EntryValue) -> Result<File> {
assert_eq!(entry.kind(), Kind::File);
let path = self.relative_path(&entry.apath);
fs::File::open(&path).map_err(|source| Error::ReadSourceFile { path, source })
}
}
impl tree::ReadTree for LiveTree {
type Entry = EntryValue;
type IT = Iter;
fn iter_entries(
&self,
subtree: Apath,
exclude: Exclude,
_monitor: Arc<dyn Monitor>,
) -> Result<Self::IT> {
Iter::new(&self.path, subtree, exclude)
}
}
fn entry_from_fs_metadata(
apath: Apath,
source_path: &Path,
metadata: &fs::Metadata,
) -> Result<EntryValue> {
let mtime = metadata
.modified()
.expect("Failed to get file mtime")
.into();
let kind_meta = if metadata.is_file() {
KindMeta::File {
size: metadata.len(),
}
} else if metadata.is_dir() {
KindMeta::Dir
} else if metadata.is_symlink() {
let t = match source_path.read_link() {
Ok(t) => t,
Err(e) => {
error!("Failed to read target of symlink {source_path:?}: {e}");
return Err(e.into());
}
};
let target = match t.into_os_string().into_string() {
Ok(t) => t,
Err(e) => {
error!("Failed to decode target of symlink {source_path:?}: {e:?}");
return Err(Error::UnsupportedTargetEncoding {
path: source_path.to_owned(),
});
}
};
KindMeta::Symlink { target }
} else {
return Err(Error::UnsupportedSourceKind {
path: source_path.to_owned(),
});
};
let owner = Owner::from(metadata);
let unix_mode = UnixMode::from(metadata.permissions());
Ok(EntryValue {
apath,
mtime,
kind_meta,
unix_mode,
owner,
})
}
#[derive(Debug)]
pub struct Iter {
root_path: PathBuf,
dir_deque: VecDeque<Apath>,
entry_deque: VecDeque<EntryValue>,
check_order: apath::DebugCheckOrder,
exclude: Exclude,
stats: LiveTreeIterStats,
}
impl Iter {
fn new(root_path: &Path, subtree: Apath, exclude: Exclude) -> Result<Iter> {
let start_path = subtree.below(root_path);
let start_metadata = fs::symlink_metadata(&start_path)?;
let entry_deque: VecDeque<EntryValue> = [entry_from_fs_metadata(
subtree.clone(),
&start_path,
&start_metadata,
)?]
.into();
let dir_deque: VecDeque<Apath> = [subtree].into();
Ok(Iter {
root_path: root_path.to_path_buf(),
entry_deque,
dir_deque,
check_order: apath::DebugCheckOrder::new(),
exclude,
stats: LiveTreeIterStats::default(),
})
}
fn visit_next_directory(&mut self, parent_apath: &Apath) {
self.stats.directories_visited += 1;
let mut children = Vec::<(String, EntryValue)>::new();
let dir_path = parent_apath.below(&self.root_path);
let dir_iter = match fs::read_dir(&dir_path) {
Ok(i) => i,
Err(err) => {
error!("Error reading directory {dir_path:?}: {err}");
return;
}
};
let mut subdir_apaths: Vec<Apath> = Vec::new();
for dir_entry in dir_iter {
let dir_entry = match dir_entry {
Ok(dir_entry) => dir_entry,
Err(err) => {
error!("Error reading next entry from directory {dir_path:?}: {err}");
continue;
}
};
let child_osstr = dir_entry.file_name();
let child_name = match child_osstr.to_str() {
Some(c) => c,
None => {
error!("Couldn't decode filename {child_osstr:?} in {dir_path:?}",);
continue;
}
};
let child_apath = parent_apath.append(child_name);
if self.exclude.matches(&child_apath) {
self.stats.exclusions += 1;
continue;
}
let ft = match dir_entry.file_type() {
Ok(ft) => ft,
Err(e) => {
error!("Error getting type of {child_apath:?} during iteration: {e}");
continue;
}
};
if ft.is_dir() {
match cachedir::is_tagged(dir_entry.path()) {
Ok(true) => continue,
Ok(false) => (),
Err(e) => {
error!("Error checking CACHEDIR.TAG in {dir_entry:?}: {e}");
}
}
}
let metadata = match dir_entry.metadata() {
Ok(metadata) => metadata,
Err(e) => {
match e.kind() {
ErrorKind::NotFound => {
warn!("File disappeared during iteration: {child_apath:?}: {e}");
}
_ => {
error!("Failed to read source metadata from {child_apath:?}: {e}");
self.stats.metadata_error += 1;
}
};
continue;
}
};
if ft.is_dir() {
subdir_apaths.push(child_apath.clone());
}
let child_path = dir_path.join(dir_entry.file_name());
let entry = match entry_from_fs_metadata(child_apath, &child_path, &metadata) {
Ok(entry) => entry,
Err(Error::UnsupportedSourceKind { .. }) => {
continue;
}
Err(err) => {
error!("Failed to build entry for {child_path:?}: {err:?}");
continue;
}
};
children.push((child_name.to_string(), entry));
}
if !subdir_apaths.is_empty() {
subdir_apaths.sort_unstable();
self.dir_deque.reserve(subdir_apaths.len());
for a in subdir_apaths.into_iter().rev() {
self.dir_deque.push_front(a);
}
}
children.sort_unstable_by(|a, b| a.0.cmp(&b.0));
self.entry_deque.extend(children.into_iter().map(|x| x.1));
}
}
impl Iterator for Iter {
type Item = EntryValue;
fn next(&mut self) -> Option<EntryValue> {
loop {
if let Some(entry) = self.entry_deque.pop_front() {
self.stats.entries_returned += 1;
self.check_order.check(&entry.apath);
return Some(entry);
} else if let Some(entry) = self.dir_deque.pop_front() {
self.visit_next_directory(&entry)
} else {
return None;
}
}
}
}
#[cfg(test)]
mod test {
use crate::monitor::test::TestMonitor;
use crate::test_fixtures::{entry_iter_to_apath_strings, TreeFixture};
use super::*;
#[test]
fn open_tree() {
let tf = TreeFixture::new();
let lt = LiveTree::open(tf.path()).unwrap();
assert_eq!(lt.path(), tf.path());
}
#[test]
fn list_simple_directory() {
let tf = TreeFixture::new();
tf.create_file("bba");
tf.create_file("aaa");
tf.create_dir("jam");
tf.create_file("jam/apricot");
tf.create_dir("jelly");
tf.create_dir("jam/.etc");
let lt = LiveTree::open(tf.path()).unwrap();
let result: Vec<EntryValue> = lt
.iter_entries(Apath::root(), Exclude::nothing(), TestMonitor::arc())
.unwrap()
.collect();
let names = entry_iter_to_apath_strings(&result);
assert_eq!(
names,
[
"/",
"/aaa",
"/bba",
"/jam",
"/jelly",
"/jam/.etc",
"/jam/apricot"
]
);
let repr = format!("{:?}", &result[6]);
println!("{repr}");
assert!(repr.starts_with("EntryValue {"));
assert!(repr.contains("Apath(\"/jam/apricot\")"));
}
#[test]
fn exclude_entries_directory() {
let tf = TreeFixture::new();
tf.create_file("foooo");
tf.create_file("bar");
tf.create_dir("fooooBar");
tf.create_dir("baz");
tf.create_file("baz/bar");
tf.create_file("baz/bas");
tf.create_file("baz/test");
let exclude = Exclude::from_strings(["/**/fooo*", "/**/??[rs]", "/**/*bas"]).unwrap();
let lt = LiveTree::open(tf.path()).unwrap();
let names = entry_iter_to_apath_strings(
lt.iter_entries(Apath::root(), exclude, TestMonitor::arc())
.unwrap(),
);
assert_eq!(names, ["/", "/baz", "/baz/test"]);
}
#[cfg(unix)]
#[test]
fn symlinks() {
let tf = TreeFixture::new();
tf.create_symlink("from", "to");
let lt = LiveTree::open(tf.path()).unwrap();
let names = entry_iter_to_apath_strings(
lt.iter_entries(Apath::root(), Exclude::nothing(), TestMonitor::arc())
.unwrap(),
);
assert_eq!(names, ["/", "/from"]);
}
#[test]
fn iter_subtree_entries() {
let tf = TreeFixture::new();
tf.create_file("in base");
tf.create_dir("subdir");
tf.create_file("subdir/a");
tf.create_file("subdir/b");
tf.create_file("zzz");
let lt = LiveTree::open(tf.path()).unwrap();
let names = entry_iter_to_apath_strings(
lt.iter_entries("/subdir".into(), Exclude::nothing(), TestMonitor::arc())
.unwrap(),
);
assert_eq!(names, ["/subdir", "/subdir/a", "/subdir/b"]);
}
#[test]
fn exclude_cachedir() {
let tf = TreeFixture::new();
tf.create_file("a");
let cache_dir = tf.create_dir("cache");
tf.create_dir("cache/1");
cachedir::add_tag(cache_dir).unwrap();
let lt = LiveTree::open(tf.path()).unwrap();
let names = entry_iter_to_apath_strings(
lt.iter_entries(Apath::root(), Exclude::nothing(), TestMonitor::arc())
.unwrap(),
);
assert_eq!(names, ["/", "/a"]);
}
}