use chrono::{Local, Utc}; use std::collections::HashSet; use std::ffi::OsString; use std::fs::DirEntry; use std::path::{Path, PathBuf}; use std::{fs, io}; pub struct ReadDirRecursive { ignored: HashSet<OsString>, read_dir: fs::ReadDir, dir_stack: Vec<PathBuf>, files_only: bool, } impl ReadDirRecursive { /// Start the iterator for a new directory pub fn start<P: AsRef<Path>>(path: P) -> io::Result<Self> { let path = path.as_ref(); let read_dir = path.read_dir()?; Ok(ReadDirRecursive { ignored: HashSet::new(), read_dir, dir_stack: Vec::new(), files_only: false, }) } pub fn ignored<S: Into<OsString>>(mut self, s: S) -> Self { self.ignored.insert(s.into()); self } pub fn files(mut self) -> Self { self.files_only = true; self } /// Tries to populate the `read_dir` field with a new `ReadDir` instance to consume. fn next_read_dir(&mut self) -> io::Result<bool> { if let Some(path) = self.dir_stack.pop() { self.read_dir = path.read_dir()?; Ok(true) } else { Ok(false) } } /// Convenience method to add a new directory to the stack. fn push_entry(&mut self, entry: &io::Result<DirEntry>) { if let Ok(entry) = entry { if entry.path().is_dir() { self.dir_stack.push(entry.path()); } } } /// Determine whether an entry should be returned by the iterator. fn should_return(&self, entry: &io::Result<DirEntry>) -> bool { if let Ok(entry) = entry { let mut res = !self.ignored.contains(&entry.file_name()); // Please just let me combine these already if self.files_only { if let Ok(file_type) = entry.file_type() { res = res && file_type.is_file(); } // We couldn't determine if it's a file, so we don't return it else { res = false; } } res } else { true } } } impl Iterator for ReadDirRecursive { type Item = io::Result<DirEntry>; fn next(&mut self) -> Option<Self::Item> { loop { // First, we try to consume the current directory's items while let Some(entry) = self.read_dir.next() { self.push_entry(&entry); if self.should_return(&entry) { return Some(entry); } } // If we get an error while setting up a new directory, we return this, otherwise we // keep trying to consume the directories match self.next_read_dir() { Ok(true) => (), // There's no more directories to traverse, so the iterator is done Ok(false) => return None, Err(e) => return Some(Err(e)), } } } } pub trait PathExt { /// Confirm whether the file has not been modified since the given timestamp. /// /// This function will only return true if it can determine with certainty that the file hasn't /// been modified. /// /// # Args /// /// * `timestamp` - Timestamp to compare modified time with /// /// # Returns /// /// True if the file has not been modified for sure, false otherwise. fn not_modified_since(&self, timestamp: chrono::DateTime<Utc>) -> bool; /// An extension of the `read_dir` command that runs through the entire underlying directory /// structure using breadth-first search fn read_dir_recursive(&self) -> io::Result<ReadDirRecursive>; } impl PathExt for Path { fn not_modified_since(&self, timestamp: chrono::DateTime<Utc>) -> bool { self.metadata() .and_then(|m| m.modified()) .map(|last_modified| { let t: chrono::DateTime<Utc> = last_modified.into(); let t = t.with_timezone(&Local); t < timestamp }) .unwrap_or(false) } fn read_dir_recursive(&self) -> io::Result<ReadDirRecursive> { ReadDirRecursive::start(self) } }