use std::{ fs::{File, Metadata}, io::{BufRead, BufReader}, time::{Duration, SystemTime}, }; use camino::{Utf8Path, Utf8PathBuf}; use getopts::Options; struct Context { root: Utf8PathBuf, ignores: Vec, } impl Context { pub fn new(root: Utf8PathBuf) -> Self { Self { root, ignores: vec![], } } pub fn ignore_file(&mut self, ignore_path: Utf8PathBuf) { let file = File::open(ignore_path).unwrap(); let mut bufread = BufReader::new(file); let mut line = String::new(); loop { if bufread.read_line(&mut line).unwrap() == 0 { break; } if line.starts_with("\\#") || line.starts_with("/") { self.ignores.push(String::from((&line[1..]).trim())); } else if line.starts_with("#") || line.is_empty() || line.trim().is_empty() { () } else { self.ignores.push(line.trim().to_owned()); } line.clear(); } } pub fn is_file_ignored>(&self, path: P) -> bool { match path.as_ref().strip_prefix(&self.root) { Ok(rel) => { for ignore in &self.ignores { if rel.starts_with(ignore) { return true; } } false } Err(_) => false, } } } fn main() { let mut opts = Options::new(); #[rustfmt::skip] opts.optopt("", "ignore", "file of paths to ignore, one per line", "PATH"); opts.optflag("h", "help", "print this help thing"); let matches = match opts.parse(std::env::args()) { Ok(m) => m, Err(e) => { eprintln!("{e}"); return; } }; if matches.opt_present("h") { println!("{}", opts.usage("Usage: whenwasit [options] PATH")); return; } let root = match matches.free.get(1) { None => { println!("expected path"); println!("{}", opts.usage("Usage: whenwasit [options] PATH")); return; } Some(p) => Utf8PathBuf::from(p), }; let mut context = Context::new(root); if let Some(ignore) = matches.opt_str("ignore") { let ignore = Utf8PathBuf::from(ignore); context.ignore_file(ignore); } process(&context, &context.root) } /// Loop through the provided directory printing CSV rows. The `root_path` is /// used to make printed paths relative to it. /// /// Does two passes: First pass prints files. Second pass recurs, printing directories. fn process(ctx: &Context, path: &Utf8Path) { //TODO: do not panic, please let this_meta = std::fs::metadata(&path).unwrap(); let this_times = Times::metadata(&this_meta); row(&ctx.root, &path, &this_times); for entry in path.read_dir_utf8().unwrap() { let entry = entry.unwrap(); match entry.file_type() { Err(_) => panic!(), Ok(ft) if ft.is_file() => { let path = entry.path(); if ctx.is_file_ignored(path) { continue; } let meta = entry.metadata().unwrap(); let times = Times::metadata(&meta); row(&ctx.root, path, ×) } Ok(_) => {} } } for entry in path.read_dir_utf8().unwrap() { let entry = entry.unwrap(); match entry.file_type() { Err(_) => panic!(), Ok(ft) if ft.is_dir() => { let path = entry.path(); if ctx.is_file_ignored(path) { continue; } process(ctx, entry.path()) } Ok(_) => {} } } } fn row>(root: &Utf8Path, path: P, times: &Times) { let relative = path .as_ref() .strip_prefix(root) .expect("row wasn't relative to the root; what happened?"); /// Returns an owned string with the duration formatted as seconds, or a /// borrowed empty string fn time_string(time: Option) -> String { time.map(|d| d.as_secs().to_string()).unwrap_or_default() } println!( "{},{},{},{}", escape(relative), time_string(times.created()), time_string(times.modified()), time_string(times.accessed()) ); } /// Blackslash-escape comma and backslash fn escape>(raw: S) -> String { let raw = raw.as_ref(); let mut escaped = String::with_capacity(raw.len()); for c in raw.chars() { match c { ',' | '\\' => { escaped.push('\\'); escaped.push(c); } _ => escaped.push(c), } } escaped } struct Times { created: Option, modified: Option, accessed: Option, } impl Times { /// Get the btime, mtime, atime from the Metadata. If a time cannot /// be attained, leave a None in it's place pub fn metadata(meta: &Metadata) -> Self { Self { created: meta.created().ok(), modified: meta.modified().ok(), accessed: meta.accessed().ok(), } } /// The time since the Unix Epoch the file was created pub fn created(&self) -> Option { Self::from_epoch(self.created.as_ref()) } /// The time since the Unix Epoch the file was last modified pub fn modified(&self) -> Option { Self::from_epoch(self.modified.as_ref()) } /// The time since the Unix Epoch the file was last accessed pub fn accessed(&self) -> Option { Self::from_epoch(self.accessed.as_ref()) } fn from_epoch(maybe_time: Option<&SystemTime>) -> Option { maybe_time.map(|st| st.duration_since(SystemTime::UNIX_EPOCH).unwrap()) } }