use std::path::{PathBuf,Path}; use std::fs; use std::os::unix::fs::MetadataExt; use std::ffi::OsString; #[derive(Debug)] pub enum Error { EntryNotFound(String) } #[derive(Debug, Clone)] pub struct Entry { pub name: OsString, pub path: PathBuf, pub base_dir: PathBuf, } impl Entry { pub fn new(path: PathBuf, name: OsString, base_dir: PathBuf) -> Entry { Entry { path, name, base_dir, } } pub fn read_setting(&self, setting: &str) -> Option { let mut path = self.path.clone(); path.set_extension(setting); read_or_none(&path) } } /// Load single entry from folder pub fn entry(basedir: &str, filter: impl Fn(&Path) -> bool, name: &str) -> Option { let basepath = PathBuf::from(&basedir); let path = basepath.clone().join(name); if !path.exists() || !filter(&path) { return None; } Some(Entry::new( path.clone(), path.file_name().expect("Failed to read file name").to_os_string(), basepath )) } /// Loads entire database from folder pub fn from(path_name: &str, filter: impl Fn(&Path) -> bool) -> Result, std::io::Error> { let path = PathBuf::from(&path_name); let mut entries = Vec::new(); for file in path.read_dir()? { if file.is_err() { // Dismiss individual errors (in case there's a permission problem) // TODO: maybe print a warning? Actually a configurable error level // (using enum variants) should be passed to the function to configure // whether to continue silently or error out for individual files continue; } let entry = file?.path(); if filter(&entry) { entries.push( Entry::new( entry.clone(), entry.file_name().expect("Failed to read file name").to_os_string(), path.clone() ) ); } } return Ok(entries); } /// Reads the file and strips whitespace (including newlines) /// Useful for file-based key-value store pub fn read_or_none(path: &Path) -> Option { if !path.is_file() { return None; } match fs::read_to_string(path) { Ok(content) => { // Remove trailing space/newlines Some(content.trim().to_string()) }, Err(e) => { eprintln!("IO ERROR: {}", e); None } } } fn list_files(path: &Path) -> Vec { let mut res = Vec::new(); match fs::read_dir(path) { Ok(files) => { for r in files { match r { Ok(entry) => { let file = entry.path(); if file.is_file() { res.push(file); } }, Err(e) => { eprintln!("IOERROR: {}", e) } } } }, Err(e) => { eprintln!("IOERROR: {}", e); } } return res; } // If the file doesn't exist or fails, return false pub fn is_executable(path: &Path) -> bool { // Do not match directories if !path.is_file() { return false; } match fs::metadata(path) { Ok(stat) => { let mode = stat.mode(); // Check user exec permission) if (mode & 0o100) == 0o100 { true } else { false } }, Err(e) => { eprintln!("IO Error: {}", e); false } } } #[cfg(test)] mod tests { use super::*; #[test] fn can_load_db() { let base_dir = "tests/success"; let entries = from(base_dir, is_executable).expect("Could not load db"); let entries_names: Vec = entries.iter().map(|x| x.name.clone().into_string().unwrap()).collect(); let expected: Vec = vec!("task", "symlink", "no_source").iter().map(|x| x.to_string()).collect(); assert_eq!(expected.len(), entries_names.len()); for entry in expected { if !entries_names.contains(&entry.to_string()) { panic!("Could not find {}", &entry); } } } }