use std::cmp::{self, Ordering}; use std::collections::{BinaryHeap, VecDeque}; use std::fs; use std::os::unix::fs::MetadataExt; use std::path::PathBuf; use std::result::Result; use std::time::{Duration, Instant}; use crate::errors::TreeBuildError; use crate::flat_tree::{LineType, Tree, TreeLine}; use crate::git_ignore::GitIgnoreFilter; use crate::task_sync::TaskLifetime; use crate::tree_options::{OptionBool, TreeOptions}; // like a tree line, but with the info needed during the build // This structure isn't usable independantly from the tree builder struct BLine { parent_idx: usize, path: PathBuf, depth: u16, name: String, file_type: fs::FileType, children: Option>, // sorted and filtered (indexes of the children in tree.blines) next_child_idx: usize, // index for iteration, among the children has_error: bool, has_match: bool, score: i32, ignore_filter: Option, nb_kept_children: i32, // used during the trimming step } // the result of trying to build a bline enum BLineResult { Some(BLine), // the only positive result FilteredOutAsHidden, FilteredOutByPattern, FilteredOutAsNonFolder, GitIgnored, Invalid, } impl BLine { // a special constructor, checking nothing fn from_root(path: PathBuf, respect_ignore: OptionBool) -> Result { let name = match path.file_name() { Some(name) => name.to_string_lossy().to_string(), None => String::from("???"), // should not happen }; let ignore_filter = if respect_ignore == OptionBool::No { None } else { let gif = GitIgnoreFilter::applicable_to(&path); // if auto, we don't look for other gif if we're not in a git dir if respect_ignore == OptionBool::Auto && gif.files.is_empty() { None } else { Some(gif) } }; if let Ok(md) = fs::metadata(&path) { let file_type = md.file_type(); Ok(BLine { parent_idx: 0, path, depth: 0, name, children: None, next_child_idx: 0, file_type, has_error: false, has_match: true, score: 0, ignore_filter, nb_kept_children: 0, }) } else { Err(TreeBuildError::FileNotFound { path: format!("{:?}", path), }) } } // return a bline if the direntry directly matches the options and there's no error fn from( parent_idx: usize, e: fs::DirEntry, depth: u16, options: &TreeOptions, parent_ignore_filter: &Option, ) -> BLineResult { let name = e.file_name(); let name = match name.to_str() { Some(name) => name, None => { return BLineResult::Invalid; } }; if !options.show_hidden && name.starts_with('.') { return BLineResult::FilteredOutAsHidden; } let mut has_match = true; let mut score = 0; if options.pattern.is_some() { if let Some(m) = options.pattern.find(&name) { // we dope less deep entries score = m.score + 10000 - i32::from(depth); } else { has_match = false; } } let file_type = match e.file_type() { Ok(ft) => ft, Err(_) => { return BLineResult::Invalid; } }; if file_type.is_file() || file_type.is_symlink() { if !has_match { return BLineResult::FilteredOutByPattern; } if options.only_folders { return BLineResult::FilteredOutAsNonFolder; } } let path = e.path(); let mut ignore_filter = None; if let Some(gif) = parent_ignore_filter { if !gif.accepts(&path, &name, file_type.is_dir()) { return BLineResult::GitIgnored; } if file_type.is_dir() { ignore_filter = Some(gif.extended_to(&path)); } } BLineResult::Some(BLine { parent_idx, path, depth, name: name.to_string(), file_type, children: None, next_child_idx: 0, has_error: false, has_match, score, ignore_filter, nb_kept_children: 0, }) } fn to_tree_line(&self) -> TreeLine { let mut mode = 0; let mut uid = 0; let mut gid = 0; let mut has_error = self.has_error; if let Ok(metadata) = fs::symlink_metadata(&self.path) { mode = metadata.mode(); uid = metadata.uid(); gid = metadata.gid(); } let line_type = if self.file_type.is_dir() { LineType::Dir } else if self.file_type.is_symlink() { if let Ok(target) = fs::read_link(&self.path) { let target = target.to_string_lossy().into_owned(); let mut target_path = PathBuf::from(&target); if target_path.is_relative() { target_path = self.path.parent().unwrap().join(target_path) } if let Ok(target_metadata) = fs::symlink_metadata(&target_path) { if target_metadata.file_type().is_dir() { LineType::SymLinkToDir(target) } else { LineType::SymLinkToFile(target) } } else { has_error = true; LineType::SymLinkToFile(target) } } else { has_error = true; LineType::SymLinkToFile(String::from("????")) } } else { LineType::File }; let unlisted = if let Some(children) = &self.children { children.len() - self.next_child_idx } else { 0 }; TreeLine { left_branchs: vec![false; self.depth as usize].into_boxed_slice(), depth: self.depth, name: self.name.to_string(), path: self.path.clone(), line_type, has_error, unlisted, score: self.score, mode, uid, gid, size: None, } } } // a structure making it possible to keep bline references // sorted in a binary heap with the line with the smallest // score at the top struct SortableBLineIdx { idx: usize, score: i32, } impl Eq for SortableBLineIdx {} impl PartialEq for SortableBLineIdx { fn eq(&self, other: &SortableBLineIdx) -> bool { self.score == other.score // unused but required by spec of Ord } } impl Ord for SortableBLineIdx { fn cmp(&self, other: &SortableBLineIdx) -> Ordering { if self.score == other.score { Ordering::Equal } else if self.score < other.score { Ordering::Greater } else { Ordering::Less } } } impl PartialOrd for SortableBLineIdx { fn partial_cmp(&self, other: &SortableBLineIdx) -> Option { Some(self.cmp(other)) } } // The TreeBuilder builds a Tree according to options (including an optional search pattern) // Instead of the final TreeLine, the builder uses an internal structure: BLine. // All BLines used during build are stored in the blines vector and kept until the end. // Most operations and temporary data structures just deal with the indexes of lines in // the blines vector. pub struct TreeBuilder { blines: Vec, // all blines, even the ones not yet "seen" by BFS options: TreeOptions, targeted_size: usize, // the number of lines we should fill (height of the screen) nb_gitignored: u32, // number of times a gitignore pattern excluded a file } impl TreeBuilder { pub fn from( path: PathBuf, options: TreeOptions, targeted_size: usize, ) -> Result { let mut blines = Vec::new(); blines.push(BLine::from_root(path, options.respect_git_ignore)?); Ok(TreeBuilder { blines, options, targeted_size, nb_gitignored: 0, }) } // stores (move) the bline in the global vec. Returns its index fn store(&mut self, bline: BLine) -> usize { let idx = self.blines.len(); self.blines.push(bline); idx } // returns true when there are direct matches among children fn load_children(&mut self, bline_idx: usize) -> bool { let mut has_child_match = false; match fs::read_dir(&self.blines[bline_idx].path) { Ok(entries) => { let mut children: Vec = Vec::new(); for e in entries { if let Ok(e) = e { let bl = BLine::from( bline_idx, e, self.blines[bline_idx].depth + 1, &self.options, &self.blines[bline_idx].ignore_filter, ); match bl { BLineResult::Some(bl) => { if bl.has_match { // direct match self.blines[bline_idx].has_match = true; has_child_match = true; } children.push(self.store(bl)); } BLineResult::GitIgnored => { self.nb_gitignored += 1; } _ => { // other reason, we don't care } } } } children.sort_by(|&a, &b| { self.blines[a] .name .to_lowercase() .cmp(&self.blines[b].name.to_lowercase()) }); self.blines[bline_idx].children = Some(children); } Err(_err) => { //debug!( // "Error while listing {:?} : {:?}", // self.blines[bline_idx].path, err //); self.blines[bline_idx].has_error = true; self.blines[bline_idx].children = Some(Vec::new()); } } has_child_match } // load_children must have been called before on bline_idx fn next_child( &mut self, bline_idx: usize, // the parent ) -> Option { let bline = &mut self.blines[bline_idx]; if let Some(children) = &bline.children { if bline.next_child_idx < children.len() { let next_child = children[bline.next_child_idx]; bline.next_child_idx += 1; Some(next_child) } else { Option::None } } else { unreachable!(); } } // first step of the build: we explore the directories and gather lines. // If there's no search pattern we stop when we have enough lines to fill the screen. // If there's a pattern, we try to gather more lines that will be sorted afterwards. fn gather_lines(&mut self, task_lifetime: &TaskLifetime) -> Option> { let start = Instant::now(); let mut out_blines: Vec = Vec::new(); // the blines we want to display (indexes into blines) let not_long = Duration::from_millis(600); let optimal_size = self.options.pattern.optimal_result_number(self.targeted_size); out_blines.push(0); let mut nb_lines_ok = 1; // in out_blines let mut open_dirs: VecDeque = VecDeque::new(); let mut next_level_dirs: Vec = Vec::new(); self.load_children(0); open_dirs.push_back(0); loop { if (nb_lines_ok > optimal_size) || (nb_lines_ok >= self.targeted_size && start.elapsed() > not_long) { break; } if let Some(open_dir_idx) = open_dirs.pop_front() { if let Some(child_idx) = self.next_child(open_dir_idx) { open_dirs.push_back(open_dir_idx); let child = &self.blines[child_idx]; if child.has_match { nb_lines_ok += 1; } if child.file_type.is_dir() { next_level_dirs.push(child_idx); } out_blines.push(child_idx); } } else { // this depth is finished, we must go deeper if next_level_dirs.is_empty() { // except there's nothing deeper break; } for next_level_dir_idx in &next_level_dirs { if task_lifetime.is_expired() { info!("task expired (core build - inner loop)"); return None; } let has_child_match = self.load_children(*next_level_dir_idx); if has_child_match { // we must ensure the ancestors are made Ok let mut idx = *next_level_dir_idx; loop { let mut bline = &mut self.blines[idx]; if !bline.has_match { bline.has_match = true; nb_lines_ok += 1; } idx = bline.parent_idx; if idx == 0 { break; } } } open_dirs.push_back(*next_level_dir_idx); } next_level_dirs.clear(); } } if self.options.show_sizes || !self.options.trim_root { // if the root directory isn't totally read, we finished it even // it it goes past the bottom of the screen while let Some(child_idx) = self.next_child(0) { out_blines.push(child_idx); } } Some(out_blines) } // Post search trimming // When there's a pattern, gathering normally brings many more lines than // strictly necessary to fill the screen. // This function keeps only the best ones while taking care of not // removing a parent before its children. fn trim_excess(&mut self, out_blines: &[usize]) { let mut count = 1; let trim_root = self.options.trim_root && !self.options.show_sizes; for idx in out_blines[1..].iter() { if self.blines[*idx].has_match { count += 1; let parent_idx = self.blines[*idx].parent_idx; self.blines[parent_idx].nb_kept_children += 1; } } let mut remove_queue: BinaryHeap = BinaryHeap::new(); for idx in out_blines[1..].iter() { let bline = &self.blines[*idx]; if bline.has_match && bline.nb_kept_children == 0 && (bline.depth > 1 || trim_root) // keep the complete first level when showing sizes { remove_queue.push(SortableBLineIdx { idx: *idx, score: bline.score, }); } } debug!( "Trimming: we have {} lines for a goal of {}", count, self.targeted_size ); while count > self.targeted_size { if let Some(sli) = remove_queue.pop() { self.blines[sli.idx].has_match = false; let parent_idx = self.blines[sli.idx].parent_idx; let mut parent = &mut self.blines[parent_idx]; parent.nb_kept_children -= 1; if parent.nb_kept_children == 0 { remove_queue.push(SortableBLineIdx { idx: parent_idx, score: parent.score, }); } count -= 1; } else { debug!("trimming prematurely interrupted"); break; } } } // makes a tree from the builder's specific structure fn take(&mut self, out_blines: &[usize]) -> Tree { let mut lines: Vec = Vec::new(); for idx in out_blines.iter() { if self.blines[*idx].has_match { // we need to count the children, so we load them if self.blines[*idx].file_type.is_dir() && self.blines[*idx].children.is_none() { self.load_children(*idx); } lines.push(self.blines[*idx].to_tree_line()); } } let mut tree = Tree { lines: lines.into_boxed_slice(), selection: 0, options: self.options.clone(), scroll: 0, nb_gitignored: self.nb_gitignored, }; tree.after_lines_changed(); if self.options.show_sizes { tree.fetch_file_sizes(); // not the dirs, only simple files } tree } // build a tree. Can be called only once per builder pub fn build(mut self, task_lifetime: &TaskLifetime) -> Option { debug!("start building with pattern {}", self.options.pattern); match self.gather_lines(task_lifetime) { Some(out_blines) => { self.trim_excess(&out_blines); Some(self.take(&out_blines)) } None => None, // interrupted } } }