use std::cmp::max;
use std::collections::HashSet;
use std::env;
use std::ffi::OsStr;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::path::MAIN_SEPARATOR_STR;
use chrono::{DateTime, Utc};
use glob::{MatchOptions, Pattern};
use multimap::MultiMap;
use path_clean::PathClean;
use crate::config::{Config, FileKind};
use crate::error::{MyError, MyResult};
use crate::file::File;
use crate::metadata::Metadata;
use crate::regex;
use crate::system::{Entry, Flags, System};
const EXT_WIDTH: usize = 4; 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, F: Flags + Copy, E: Entry<F>, I: Iterator<Item = MyResult<E>>, S: System<F, E, I>> {
config: &'a Config,
system: S,
current: PathBuf,
options: MatchOptions,
start_time: Option<DateTime<Utc>>,
git_bash: bool,
phantom: PhantomData<(F, E, I)>,
}
impl Total {
pub fn new(start_time: Option<DateTime<Utc>>) -> Self {
return Self {
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 == FileKind::Dir {
self.num_dirs += 1;
} else {
self.num_files += 1;
}
}
}
impl<'a, F: Flags + Copy, E: Entry<F>, I: Iterator<Item = MyResult<E>>, S: System<F, E, I>> Finder<'a, F, E, I, S> {
pub fn new(config: &Config, system: S, git_bash: bool) -> MyResult<Finder<F, E, I, S>> {
let current = env::current_dir()?;
let options = Self::match_options();
let start_time = config.start_time();
let finder = Finder {
config,
system,
current,
options,
start_time,
git_bash,
phantom: PhantomData,
};
return Ok(finder);
}
#[cfg(windows)]
fn match_options() -> MatchOptions {
let mut options = MatchOptions::new();
options.case_sensitive = false;
return options;
}
#[cfg(not(windows))]
fn match_options() -> MatchOptions {
let options = MatchOptions::new();
return options;
}
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() {
self.find_entries(&mut files, abs_root, rel_root, patterns);
self.find_parents(&mut files, abs_root, rel_root);
}
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(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(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 = 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 find_entries(
&self,
files: &mut HashSet<File>,
abs_root: &Path,
rel_root: &Path,
patterns: &Vec<Pattern>,
) {
for entry in self.system.entries(abs_root, rel_root) {
match entry.and_then(|entry| self.create_file(&entry, abs_root, rel_root, patterns)) {
Ok(file) => {
if let Some(file) = file {
if !self.config.order_name || self.config.show_indent || (file.file_type != FileKind::Dir) {
files.insert(file);
}
}
}
Err(error) => {
let error = MyError::from(error);
error.eprint();
}
}
}
}
fn find_parents(
&self,
files: &mut HashSet<File>,
abs_root: &Path,
rel_root: &Path,
) {
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() {
match self.create_parent(abs_root, rel_root, parent, depth) {
Ok(file) => {
if let Some(file) = file {
files.insert(file);
}
}
Err(error) => {
let error = MyError::from(error);
error.eprint();
}
}
}
}
}
fn create_file(
&self,
entry: &E,
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_with(name, self.options)) {
if let Some(depth) = self.config.min_depth {
if entry.depth() < depth {
return Ok(None);
}
}
let mut file_data = entry.metadata()?;
let file_type = FileKind::from_type(entry.file_type(), file_data.file_mode);
if let Some(filter_types) = &self.config.filter_types {
if !filter_types.contains(&file_type) {
return Ok(None);
}
}
let link_path = entry.read_link()?;
return if let Some(link_path) = link_path {
if let Some((link_path, link_data)) = self.follow_link(entry.path(), &link_path) {
let link_type = FileKind::from_type(link_data.file_type, link_data.file_mode);
self.create_inner(entry, abs_root, rel_root, file_type, link_data, Some((link_path, link_type)))
} else {
file_data.file_size = 0;
file_data.file_time = None;
self.create_inner(entry, abs_root, rel_root, FileKind::Link(false), file_data, Some((link_path, FileKind::Link(false))))
}
} else {
self.create_inner(entry, abs_root, rel_root, file_type, file_data, None)
}
}
}
return Ok(None);
}
fn follow_link(&self, file_path: &Path, link_path: &Path) -> Option<(PathBuf, Metadata<F>)> {
if let Some(file_parent) = file_path.parent() {
let link_path = file_parent.join(link_path);
if let Some(link_data) = self.system.metadata(&link_path).ok() {
return Some((link_path, link_data));
}
}
return None;
}
fn create_inner(
&self,
entry: &E,
abs_root: &Path,
rel_root: &Path,
file_type: FileKind,
file_data: Metadata<F>,
link_data: Option<(PathBuf, FileKind)>,
) -> MyResult<Option<File>> {
let file_time = file_data.file_time.map(DateTime::<Utc>::from);
if let Some(start_time) = self.start_time {
if let Some(file_time) = file_time {
if file_time < start_time {
return Ok(None);
}
} else {
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_default();
let file_ext = Self::find_extension(&abs_path, file_type);
let file_size = Self::select_size(&file_data, &link_data, file_type);
let file = File::new(
abs_dir,
rel_dir,
file_depth,
file_name,
file_ext,
file_type,
file_data.file_mode,
file_size,
file_time,
link_data,
);
return Ok(Some(file));
}
}
return 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(
&self,
abs_root: &Path,
rel_root: &Path,
abs_path: PathBuf,
file_depth: usize,
) -> MyResult<Option<File>> {
let file_data = self.system.metadata(&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, FileKind::Dir) {
if let Some(rel_dir) = Self::select_parent_from_owned(rel_path, FileKind::Dir) {
let file_time = file_data.file_time.map(DateTime::<Utc>::from);
let file = File::new(
abs_dir,
rel_dir,
file_depth,
String::from(""),
String::from(""),
FileKind::Dir,
file_data.file_mode,
file_data.file_size,
file_time,
None,
);
return Ok(Some(file));
}
}
return Ok(None);
}
fn select_parent_from_owned(path: PathBuf, file_type: FileKind) -> Option<PathBuf> {
if file_type == FileKind::Dir {
Some(path)
} else {
Self::find_parent(&path)
}
}
fn select_parent(path: &Path, file_type: FileKind) -> Option<PathBuf> {
if file_type == FileKind::Dir {
Some(PathBuf::from(path))
} else {
Self::find_parent(path)
}
}
fn select_name(path: &Path, file_type: FileKind) -> Option<String> {
if file_type == FileKind::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: FileKind) -> String {
match file_type {
FileKind::File(_) | FileKind::Link(_) => path.extension()
.and_then(OsStr::to_str)
.map(|ext| format!(".{ext}"))
.unwrap_or_default(),
_ => String::default(),
}
}
fn select_size(
file_data: &Metadata<F>,
link_data: &Option<(PathBuf, FileKind)>,
file_type: FileKind,
) -> u64 {
if file_type == FileKind::Dir {
return 0;
}
if let Some((_, link_type)) = link_data {
if *link_type == FileKind::Dir {
return 0;
}
}
return file_data.file_size;
}
}
fn requires_wildcard(pattern: &str) -> bool {
let wildcard_regex = regex!(r"(^\.+$|[\\/]\.*$)");
return wildcard_regex.is_match(pattern);
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::time::SystemTime;
use std::vec::IntoIter;
use chrono::TimeZone;
use pretty_assertions::assert_eq;
use crate::config::RecentKind;
use crate::system::Flags;
use super::*;
#[test]
fn test_requires_wildcard() {
assert_eq!(true, requires_wildcard("."));
assert_eq!(true, requires_wildcard(".."));
assert_eq!(false, requires_wildcard("file"));
assert_eq!(true, requires_wildcard("/path/to/dir/"));
assert_eq!(true, requires_wildcard("/path/to/dir/."));
assert_eq!(true, requires_wildcard("/path/to/dir/.."));
assert_eq!(false, requires_wildcard("/path/to/dir/file"));
assert_eq!(true, requires_wildcard(r"\path\to\dir\"));
assert_eq!(true, requires_wildcard(r"\path\to\dir\."));
assert_eq!(true, requires_wildcard(r"\path\to\dir\.."));
assert_eq!(false, requires_wildcard(r"\path\to\dir\file"));
}
#[test]
fn test_parses_file_attributes() {
let config = Config::default()
.with_patterns(vec!["*"]);
let finder = create_finder(&config);
let files = find_files(&finder);
assert_eq!(10, files.len());
assert_data(files.get(0), 1, FileKind::File(true), 0o744, 100, Some((2023, 1, 1)));
assert_data(files.get(1), 1, FileKind::Dir, 0o755, 0, Some((2023, 2, 2)));
assert_data(files.get(2), 2, FileKind::Link(true), 0o755, 0, Some((2023, 3, 3)));
assert_data(files.get(3), 2, FileKind::Link(true), 0o644, 500, Some((2023, 5, 5)));
assert_data(files.get(4), 2, FileKind::Link(false), 0o644, 0, None);
assert_data(files.get(5), 2, FileKind::Dir, 0o755, 0, Some((2023, 3, 3)));
assert_data(files.get(6), 3, FileKind::File(false), 0o644, 400, Some((2023, 4, 4)));
assert_data(files.get(7), 3, FileKind::File(false), 0o644, 500, Some((2023, 5, 5)));
assert_data(files.get(8), 3, FileKind::File(false), 0o644, 600, Some((2023, 6, 6)));
assert_data(files.get(9), 3, FileKind::File(false), 0o644, 700, Some((2023, 7, 7)));
assert_path(files.get(0), "/root", "", "archive.sh", ".sh");
assert_path(files.get(1), "/root/dir", "dir", "", "");
assert_path(files.get(2), "/root/dir", "dir", "link1", "");
assert_path(files.get(3), "/root/dir", "dir", "link2", "");
assert_path(files.get(4), "/root/dir", "dir", "link3", "");
assert_path(files.get(5), "/root/dir/subdir", "dir/subdir", "", "");
assert_path(files.get(6), "/root/dir/subdir", "dir/subdir", "alpha.csv", ".csv");
assert_path(files.get(7), "/root/dir/subdir", "dir/subdir", "alpha.txt", ".txt");
assert_path(files.get(8), "/root/dir/subdir", "dir/subdir", "beta.csv", ".csv");
assert_path(files.get(9), "/root/dir/subdir", "dir/subdir", "beta.txt", ".txt");
assert_link(files.get(0), None);
assert_link(files.get(1), None);
assert_link(files.get(2), Some((FileKind::Dir, "/root/dir/subdir")));
assert_link(files.get(3), Some((FileKind::File(false), "/root/dir/subdir/alpha.txt")));
assert_link(files.get(4), Some((FileKind::Link(false), "/etc/missing.txt")));
assert_link(files.get(5), None);
assert_link(files.get(6), None);
assert_link(files.get(7), None);
assert_link(files.get(8), None);
assert_link(files.get(9), None);
}
#[test]
fn test_finds_multiple_patterns_in_same_directory() {
let expected = vec![
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
];
let config = Config::default()
.with_patterns(vec!["dir/subdir/alpha.*", "dir/subdir/*.txt"]);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_finds_multiple_patterns_in_diff_directories() {
let expected = vec![
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
];
let config = Config::default()
.with_patterns(vec!["dir/alpha.*", "dir/subdir/*.txt"]);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_finds_files_if_recurse_no_indent() {
let expected = vec![
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
];
let config = Config::default()
.with_patterns(vec!["*.txt"]);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_finds_parents_if_recurse_with_indent() {
let expected = vec![
"/root/dir/",
"/root/dir/subdir/",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.txt",
];
let config = Config::default()
.with_patterns(vec!["*.txt"])
.with_show_indent(true);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_hides_directories_if_order_by_name() {
let expected = vec![
"/root/archive.sh",
"/root/dir/link1",
"/root/dir/link2",
"/root/dir/link3",
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.csv",
"/root/dir/subdir/beta.txt",
];
let config = Config::default()
.with_patterns(vec!["*"])
.with_order_name(true);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_finds_files_with_bare_filename() {
let expected = vec![
"/root/dir/subdir/beta.csv",
];
let config = Config::default()
.with_patterns(vec!["beta.csv"]);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_finds_files_with_bare_extension() {
let expected = vec![
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/beta.csv",
];
let config = Config::default()
.with_patterns(vec![".csv"]);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_filters_files_by_minimum_depth() {
let expected = vec![
"/root/dir/link1",
"/root/dir/link2",
"/root/dir/link3",
"/root/dir/subdir/",
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.csv",
"/root/dir/subdir/beta.txt",
];
let config = Config::default()
.with_patterns(vec!["*"])
.with_min_depth(2);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_filters_files_by_maximum_depth() {
let expected = vec![
"/root/archive.sh",
"/root/dir/",
"/root/dir/link1",
"/root/dir/link2",
"/root/dir/link3",
"/root/dir/subdir/",
];
let config = Config::default()
.with_patterns(vec!["*"])
.with_max_depth(2);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_filters_files_by_file_type() {
let expected = vec![
"/root/archive.sh",
"/root/dir/subdir/alpha.csv",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.csv",
"/root/dir/subdir/beta.txt",
];
let config = Config::default()
.with_patterns(vec!["*"])
.with_filter_types(vec![FileKind::File(false), FileKind::File(true)]);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_filters_files_by_recent_time() {
let expected = vec![
"/root/dir/link2",
"/root/dir/subdir/alpha.txt",
"/root/dir/subdir/beta.csv",
"/root/dir/subdir/beta.txt",
];
let config = Config::default()
.with_patterns(vec!["*"])
.with_curr_time(2024, 1, 1, 0, 0, 0)
.with_filter_recent(RecentKind::Month, 8);
let finder = create_finder(&config);
let files = find_files(&finder);
let paths = convert_paths(files);
assert_eq!(expected, paths);
}
#[test]
fn test_calculates_total_from_files() {
let config = Config::default()
.with_patterns(vec!["*"]);
let finder = create_finder(&config);
let files = find_files(&finder);
let total = finder.calc_total(&files);
assert_eq!(700, total.max_size);
assert_eq!(2800, total.total_size);
assert_eq!(4, total.ext_width);
assert_eq!(8, total.num_files);
assert_eq!(2, total.num_dirs);
}
fn create_finder(config: &Config) -> Finder<MockFlags, MockEntry, MockIterator, MockSystem> {
let current = PathBuf::from("/root");
let system = create_system(config, ¤t);
let options = MatchOptions::new();
let start_time = config.start_time();
return Finder {
config,
system,
current,
options,
start_time,
git_bash: false,
phantom: PhantomData,
};
}
fn create_system<'a>(config: &'a Config, current: &Path) -> MockSystem<'a> {
let link1 = MockEntry::new(2, 'd', 0o755, 4096, 2023, 3, 3, "subdir", None);
let link2 = MockEntry::new(3, 'f', 0o644, 500, 2023, 5, 5, "subdir/alpha.txt", None);
let link3 = MockEntry::new(0, 'f', 0o644, 1000, 2023, 10, 10, "/etc/missing.txt", None);
let entries = vec![
MockEntry::new(1, 'f', 0o744, 100, 2023, 1, 1, "archive.sh", None),
MockEntry::new(1, 'd', 0o755, 4096, 2023, 2, 2, "dir", None),
MockEntry::new(2, 'l', 0o644, 99, 2023, 12, 31, "dir/link1", Some(link1)),
MockEntry::new(2, 'l', 0o644, 99, 2023, 12, 31, "dir/link2", Some(link2)),
MockEntry::new(2, 'l', 0o644, 99, 2023, 12, 31, "dir/link3", Some(link3)),
MockEntry::new(2, 'd', 0o755, 4096, 2023, 3, 3, "dir/subdir", None),
MockEntry::new(3, 'f', 0o644, 400, 2023, 4, 4, "dir/subdir/alpha.csv", None),
MockEntry::new(3, 'f', 0o644, 500, 2023, 5, 5, "dir/subdir/alpha.txt", None),
MockEntry::new(3, 'f', 0o644, 600, 2023, 6, 6, "dir/subdir/beta.csv", None),
MockEntry::new(3, 'f', 0o644, 700, 2023, 7, 7, "dir/subdir/beta.txt", None),
];
return MockSystem::new(config, current, entries);
}
fn find_files(finder: &Finder<MockFlags, MockEntry, MockIterator, MockSystem>) -> Vec<File> {
let mut files = finder.find_files().unwrap();
files.sort_by_key(File::get_path);
return files;
}
fn assert_data(
file: Option<&File>,
file_depth: usize,
file_type: FileKind,
file_mode: u32,
file_size: u64,
file_time: Option<(i32, u32, u32)>,
) {
let file = file.unwrap();
let file_time = file_time.map(|(y, m, d)| create_time(y, m, d));
assert_eq!(file_depth, file.file_depth);
assert_eq!(file_type, file.file_type);
assert_eq!(file_mode, file.file_mode);
assert_eq!(file_size, file.file_size);
assert_eq!(file_time, file.file_time);
}
fn assert_path(
file: Option<&File>,
abs_dir: &str,
rel_dir: &str,
file_name: &str,
file_ext: &str,
) {
let file = file.unwrap();
assert_eq!(PathBuf::from(abs_dir), file.abs_dir);
assert_eq!(PathBuf::from(rel_dir), file.rel_dir);
assert_eq!(file_name, file.file_name);
assert_eq!(file_ext, file.file_ext);
}
fn assert_link(
file: Option<&File>,
link_data: Option<(FileKind, &str)>,
) {
let file = file.unwrap();
let link_data = link_data.map(|(f, p)| (PathBuf::from(p), f));
assert_eq!(link_data, file.link_data);
}
fn create_time(year: i32, month: u32, day: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap()
}
fn convert_paths(files: Vec<File>) -> Vec<String> {
files.into_iter().flat_map(convert_path).collect()
}
#[cfg(windows)]
fn convert_path(file: File) -> Option<String> {
let path = file.abs_dir.join(file.file_name);
return path.to_str().map(|path| path.replace(MAIN_SEPARATOR_STR, "/"));
}
#[cfg(not(windows))]
fn convert_path(file: File) -> Option<String> {
let path = file.abs_dir.join(file.file_name);
return path.to_str().map(str::to_string);
}
#[derive(Copy, Clone)]
struct MockFlags {
file_type: char,
}
#[derive(Clone)]
struct MockEntry {
path: PathBuf,
file_type: MockFlags,
depth: usize,
metadata: Metadata<MockFlags>,
link: Option<Box<MockEntry>>,
}
struct MockIterator {
entries: IntoIter<MockEntry>,
}
struct MockSystem<'a> {
config: &'a Config,
entries: Vec<MockEntry>,
metadata: BTreeMap<PathBuf, Metadata<MockFlags>>,
}
impl MockFlags {
fn new(file_type: char) -> MockFlags {
MockFlags { file_type }
}
}
impl MockEntry {
fn new(
depth: usize,
file_type: char,
file_mode: u32,
file_size: u64,
year: i32,
month: u32,
day: u32,
path: &str,
link: Option<MockEntry>,
) -> MockEntry {
let path = PathBuf::from(path);
let file_type = MockFlags::new(file_type);
let file_time = create_time(year, month, day);
let file_time = SystemTime::from(file_time);
let file_time = Some(file_time);
let metadata = Metadata { file_type, file_mode, file_size, file_time };
let link = link.map(Box::new);
return MockEntry { path, file_type, depth, metadata, link };
}
fn replace_root(mut self, abs_root: &Path, rel_root: &Path) -> Option<Self> {
match self.path.strip_prefix(rel_root) {
Ok(path) => {
self.path = abs_root.join(path);
Some(self)
}
Err(_) => None
}
}
}
impl MockIterator {
fn new(entries: Vec<MockEntry>) -> MockIterator {
MockIterator { entries: entries.into_iter() }
}
}
impl<'a> MockSystem<'a> {
fn new(config: &'a Config, current: &Path, entries: Vec<MockEntry>) -> MockSystem<'a> {
let mut metadata = BTreeMap::new();
for entry in entries.iter() {
Self::add_metadata(&mut metadata, current, entry);
if let Some(link) = &entry.link {
if !link.path.starts_with("/") {
let parent = entry.path.parent().unwrap();
let parent = current.join(parent);
Self::add_metadata(&mut metadata, &parent, link);
}
}
}
return MockSystem { config, entries, metadata };
}
fn add_metadata(
metadata: &mut BTreeMap<PathBuf, Metadata<MockFlags>>,
parent: &Path,
entry: &MockEntry,
) {
let path = parent.join(&entry.path);
metadata.insert(path, entry.metadata.clone());
}
fn filter_depth(&self, entry: &MockEntry) -> bool {
match self.config.max_depth {
Some(depth) => entry.depth <= depth,
None => true,
}
}
}
impl Flags for MockFlags {
fn is_file(&self) -> bool {
self.file_type == 'f'
}
fn is_dir(&self) -> bool {
self.file_type == 'd'
}
fn is_symlink(&self) -> bool {
self.file_type == 'l'
}
}
impl Entry<MockFlags> for MockEntry {
fn path(&self) -> &Path {
&self.path
}
fn file_name(&self) -> &OsStr {
self.path.file_name().unwrap_or_default()
}
fn file_type(&self) -> MockFlags {
self.file_type
}
fn depth(&self) -> usize {
self.depth
}
fn metadata(&self) -> MyResult<Metadata<MockFlags>> {
Ok(self.metadata.clone())
}
fn read_link(&self) -> MyResult<Option<PathBuf>> {
let path = self.link.as_ref().map(|link| link.path.clone());
return Ok(path);
}
}
impl Iterator for MockIterator {
type Item = MyResult<MockEntry>;
fn next(&mut self) -> Option<Self::Item> {
self.entries.next().map(Ok)
}
}
impl<'a> System<MockFlags, MockEntry, MockIterator> for MockSystem<'a> {
fn entries(&self, abs_root: &Path, rel_root: &Path) -> MockIterator {
let entries = self.entries.clone()
.into_iter()
.filter(|entry| self.filter_depth(entry))
.flat_map(|entry| entry.replace_root(abs_root, rel_root))
.collect();
return MockIterator::new(entries);
}
fn metadata(&self, path: &Path) -> MyResult<Metadata<MockFlags>> {
match self.metadata.get(path) {
Some(metadata) => Ok(metadata.clone()),
None => Err(MyError::Text("file not found")),
}
}
}
}