use std::cmp::max;
use std::collections::HashSet;
use std::env;
use std::ffi::OsStr;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
#[cfg(target_family = "windows")]
use std::path::MAIN_SEPARATOR_STR;
use chrono::{DateTime, Utc};
use glob::Pattern;
use multimap::MultiMap;
use path_clean::PathClean;
use walkdir::{DirEntry, WalkDir};
use crate::config::{Config, EntryKind};
use crate::error::{MyError, MyResult};
use crate::metadata::Metadata;
use crate::regex;
const EXT_WIDTH: usize = 4; pub struct File {
pub abs_dir: PathBuf,
pub rel_dir: PathBuf,
pub link_path: Option<PathBuf>,
pub file_depth: usize,
pub file_name: String,
pub file_ext: String,
pub file_type: EntryKind,
pub file_mode: u32,
pub file_size: u64,
pub file_time: DateTime<Utc>,
}
pub struct Total {
pub start_time: Option<DateTime<Utc>>,
pub max_size: u64,
pub total_size: u64,
pub ext_width: usize,
pub num_files: usize,
pub num_dirs: usize,
}
#[allow(dead_code)]
pub struct Finder<'a> {
config: &'a Config,
current: PathBuf,
start_time: Option<DateTime<Utc>>,
git_bash: bool,
}
impl File {
pub fn new(
abs_dir: PathBuf,
rel_dir: PathBuf,
link_path: Option<PathBuf>,
file_depth: usize,
file_name: String,
file_ext: String,
file_type: EntryKind,
file_mode: u32,
file_size: u64,
file_time: DateTime<Utc>,
) -> File {
File {
abs_dir,
rel_dir,
link_path,
file_depth,
file_name,
file_ext,
file_type,
file_mode,
file_size,
file_time,
}
}
pub fn select_dir(&self, abs_path: bool) -> &Path {
if abs_path {
&self.abs_dir
} else {
&self.rel_dir
}
}
pub fn select_parent_for_indent(&self) -> Option<&Path> {
if self.file_type == EntryKind::Dir {
self.rel_dir.parent()
} else {
Some(&self.rel_dir)
}
}
pub fn select_name_for_indent(&self) -> Option<&str> {
if self.file_type == EntryKind::Dir {
self.rel_dir.file_name().and_then(OsStr::to_str)
} else {
Some(&self.file_name)
}
}
pub fn group_dir_before_file(&self) -> u8 {
if self.file_type == EntryKind::Dir { 0 } else { 1 }
}
}
impl PartialEq for File {
fn eq(&self, other: &Self) -> bool {
(self.abs_dir == other.abs_dir) && (self.file_name == other.file_name)
}
}
impl Eq for File {
}
impl Hash for File {
fn hash<H: Hasher>(&self, state: &mut H) {
self.abs_dir.hash(state);
self.file_name.hash(state);
}
}
impl Total {
pub fn new(start_time: Option<DateTime<Utc>>) -> Total {
return Total {
start_time,
max_size: 0,
total_size: 0,
ext_width: 0,
num_files: 0,
num_dirs: 0,
}
}
fn update_total(&mut self, file: &File) {
self.max_size = max(self.max_size, file.file_size);
self.total_size += file.file_size;
self.ext_width = max(self.ext_width, file.file_ext.len());
if file.file_type == EntryKind::Dir {
self.num_dirs += 1;
} else {
self.num_files += 1;
}
}
}
impl<'a> Finder<'a> {
pub fn new(config: &Config, git_bash: bool) -> MyResult<Finder> {
let current = env::current_dir()?;
let start_time = config.start_time();
let finder = Finder { config, current, start_time, git_bash };
return Ok(finder);
}
pub fn find_files(&self) -> MyResult<Vec<File>> {
let mut files = HashSet::new();
let tasks = self.group_tasks()?;
for ((abs_root, rel_root), patterns) in tasks.iter_all() {
let inner = self.find_inner(abs_root, rel_root, patterns)?;
files.extend(inner);
}
let files = files.into_iter().collect();
return Ok(files);
}
pub fn calc_total(&self, files: &Vec<File>) -> Total {
let mut total = Total::new(self.start_time);
for file in files.iter() {
total.update_total(&file);
}
if total.ext_width > 0 {
total.ext_width = max(total.ext_width, EXT_WIDTH);
}
return total;
}
fn group_tasks(&self) -> MyResult<MultiMap<(PathBuf, PathBuf), Pattern>> {
let mut tasks = MultiMap::new();
for pattern in &self.config.patterns {
if let Some((abs_root, rel_root, filename)) = self.parse_pattern(pattern) {
let pattern = Pattern::new(&filename)?;
tasks.insert((abs_root, rel_root), pattern);
}
}
return Ok(tasks);
}
#[cfg(target_family = "windows")]
fn parse_pattern(&self, pattern: &str) -> Option<(PathBuf, PathBuf, String)> {
return if self.git_bash {
let drive_regex = regex!(r#"^/([A-Za-z])/(.+)$"#);
if let Some(captures) = drive_regex.captures(pattern) {
let drive = captures.get(1).unwrap().as_str().to_uppercase();
let path = captures.get(2).unwrap().as_str().replace("/", MAIN_SEPARATOR_STR);
let pattern = format!("{}:{}{}", drive, MAIN_SEPARATOR_STR, path);
self.split_pattern(&pattern)
} else {
let pattern = pattern.replace("/", MAIN_SEPARATOR_STR);
self.split_pattern(&pattern)
}
} else {
self.split_pattern(pattern)
}
}
#[cfg(not(target_family = "windows"))]
fn parse_pattern(&self, pattern: &str) -> Option<(PathBuf, PathBuf, String)> {
self.split_pattern(pattern)
}
fn split_pattern(&self, pattern: &str) -> Option<(PathBuf, PathBuf, String)> {
let wildcard = Self::requires_wildcard(pattern);
let rel_root = PathBuf::from(pattern);
let abs_root = self.current.join(&rel_root).clean();
if wildcard {
return Some((abs_root, rel_root, String::from("*")));
}
if let Some(mut name) = Self::find_name(&abs_root) {
if let Some(abs_root) = Self::find_parent(&abs_root) {
if let Some(rel_root) = Self::find_parent(&rel_root) {
if name.starts_with(".") {
name = format!("*{name}");
}
return Some((abs_root, rel_root, name));
}
}
}
return None;
}
fn requires_wildcard(pattern: &str) -> bool {
let wildcard_regex = regex!(r"(^\.+$|[\\/]\.*$)");
return wildcard_regex.is_match(pattern);
}
fn find_inner(
&self,
abs_root: &PathBuf,
rel_root: &PathBuf,
patterns: &Vec<Pattern>,
) -> MyResult<HashSet<File>> {
let mut files = HashSet::new();
let filter = self.choose_filter();
let walker = self.create_walker(abs_root).into_iter().filter_entry(filter);
for entry in walker {
match entry {
Ok(entry) => {
if let Some(file) = self.create_file(&entry, abs_root, rel_root, patterns)? {
if !self.config.order_name || self.config.show_indent || (file.file_type != EntryKind::Dir) {
files.insert(file);
}
}
}
Err(error) => {
let error = MyError::from(error);
error.eprint();
}
}
}
if self.config.show_indent {
let mut parents = HashSet::new();
for file in files.iter() {
Self::find_ancestors(&mut parents, &file.abs_dir, file.file_depth);
}
for (parent, depth) in parents.into_iter() {
if let Some(file) = Self::create_parent(abs_root, rel_root, parent, depth)? {
files.insert(file);
}
}
}
return Ok(files);
}
fn create_walker(&self, abs_root: &Path) -> WalkDir {
let mut walker = WalkDir::new(abs_root).min_depth(1);
if let Some(depth) = self.config.max_depth {
walker = walker.max_depth(depth);
}
return walker;
}
fn choose_filter(&self) -> fn(&DirEntry) -> bool {
if self.config.all_files {
|_| true
} else {
Self::filter_hidden
}
}
fn filter_hidden(entry: &DirEntry) -> bool {
if let Some(name) = entry.file_name().to_str() {
if name.starts_with(".") {
return false;
}
if name.starts_with("__") && name.ends_with("__") {
return false;
}
}
return true;
}
fn create_file(
&self,
entry: &DirEntry,
abs_root: &Path,
rel_root: &Path,
patterns: &Vec<Pattern>,
) -> MyResult<Option<File>> {
if let Some(name) = entry.file_name().to_str() {
if patterns.iter().any(|p| p.matches(name)) {
if let Some(depth) = self.config.min_depth {
if entry.depth() < depth {
return Ok(None);
}
}
let file_type = EntryKind::from(entry.file_type());
if let Some(filter_type) = self.config.filter_type {
if file_type != filter_type {
return Ok(None);
}
}
let link_path = Self::read_link(entry)?;
return if let Some(link_path) = link_path {
let file_data = Metadata::from_path(&link_path)?;
self.create_inner(entry, abs_root, rel_root, file_type, Some(link_path), file_data)
} else {
let file_data = Metadata::from_entry(entry)?;
self.create_inner(entry, abs_root, rel_root, file_type, None, file_data)
}
}
}
return Ok(None);
}
fn create_inner(
&self,
entry: &DirEntry,
abs_root: &Path,
rel_root: &Path,
file_type: EntryKind,
link_path: Option<PathBuf>,
file_data: Metadata,
) -> MyResult<Option<File>> {
let file_time = DateTime::<Utc>::from(file_data.file_time);
if let Some(start_time) = self.start_time {
if file_time < start_time {
return Ok(None);
}
}
let abs_path = entry.path();
if let Some(abs_dir) = Self::select_parent(abs_path, file_type) {
let rel_path = abs_path.strip_prefix(abs_root)?;
let rel_path = rel_root.join(rel_path).clean();
if let Some(rel_dir) = Self::select_parent_from_owned(rel_path, file_type) {
let file_depth = entry.depth();
let file_name = Self::select_name(&abs_path, file_type).unwrap_or(String::from(""));
let file_ext = Self::find_extension(&abs_path, file_type);
let file_size = match file_type {
EntryKind::File => file_data.file_size,
EntryKind::Link => file_data.file_size,
_ => 0,
};
let file = File::new(
abs_dir,
rel_dir,
link_path,
file_depth,
file_name,
file_ext,
file_type,
file_data.file_mode,
file_size,
file_time,
);
return Ok(Some(file));
}
}
return Ok(None);
}
fn read_link(entry: &DirEntry) -> MyResult<Option<PathBuf>> {
return if entry.path_is_symlink() {
let link = entry.path().read_link()?;
Ok(Some(link))
} else {
Ok(None)
}
}
fn find_ancestors(
parents: &mut HashSet<(PathBuf, usize)>,
abs_path: &Path,
file_depth: usize,
) {
if file_depth > 1 {
if parents.insert((PathBuf::from(abs_path), file_depth - 1)) {
if let Some(abs_path) = abs_path.parent() {
Self::find_ancestors(parents, abs_path, file_depth - 1);
}
}
}
}
fn create_parent(
abs_root: &Path,
rel_root: &Path,
abs_path: PathBuf,
file_depth: usize,
) -> MyResult<Option<File>> {
let file_data = Metadata::from_path(&abs_path)?;
let rel_path = abs_path.strip_prefix(abs_root)?;
let rel_path = rel_root.join(rel_path).clean();
if let Some(abs_dir) = Self::select_parent_from_owned(abs_path, EntryKind::Dir) {
if let Some(rel_dir) = Self::select_parent_from_owned(rel_path, EntryKind::Dir) {
let file_time = DateTime::<Utc>::from(file_data.file_time);
let file = File::new(
abs_dir,
rel_dir,
None,
file_depth,
String::from(""),
String::from(""),
EntryKind::Dir,
file_data.file_mode,
file_data.file_size,
file_time,
);
return Ok(Some(file));
}
}
return Ok(None);
}
fn select_parent_from_owned(path: PathBuf, file_type: EntryKind) -> Option<PathBuf> {
if file_type == EntryKind::Dir {
Some(path)
} else {
Self::find_parent(&path)
}
}
fn select_parent(path: &Path, file_type: EntryKind) -> Option<PathBuf> {
if file_type == EntryKind::Dir {
Some(PathBuf::from(path))
} else {
Self::find_parent(path)
}
}
fn select_name(path: &Path, file_type: EntryKind) -> Option<String> {
if file_type == EntryKind::Dir {
Some(String::from(""))
} else {
Self::find_name(path)
}
}
fn find_parent(path: &Path) -> Option<PathBuf> {
path.parent().map(PathBuf::from)
}
fn find_name(path: &Path) -> Option<String> {
path.file_name().and_then(OsStr::to_str).map(String::from)
}
fn find_extension(path: &Path, file_type: EntryKind) -> String {
if file_type == EntryKind::File || file_type == EntryKind::Link {
if let Some(ext) = path.extension() {
if let Some(ext) = ext.to_str() {
return format!(".{ext}");
}
}
}
return String::from("");
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_requires_wildcard() {
assert_eq!(true, Finder::requires_wildcard("."));
assert_eq!(true, Finder::requires_wildcard(".."));
assert_eq!(false, Finder::requires_wildcard("file"));
assert_eq!(true, Finder::requires_wildcard("/path/to/dir/"));
assert_eq!(true, Finder::requires_wildcard("/path/to/dir/."));
assert_eq!(true, Finder::requires_wildcard("/path/to/dir/.."));
assert_eq!(false, Finder::requires_wildcard("/path/to/dir/file"));
assert_eq!(true, Finder::requires_wildcard(r"\path\to\dir\"));
assert_eq!(true, Finder::requires_wildcard(r"\path\to\dir\."));
assert_eq!(true, Finder::requires_wildcard(r"\path\to\dir\.."));
assert_eq!(false, Finder::requires_wildcard(r"\path\to\dir\file"));
}
}