use log::{error, info, warn}; use std::path::{Path, PathBuf}; use thiserror::Error; #[derive(Error, Debug)] pub enum RunDirError { #[error("Unable to initialize rundir.")] Initialize { path: PathBuf }, #[error("The path provided is not a directory.")] PathIsNotDir { path: PathBuf }, #[error("The directory is not empty.")] DirIsNotEmpty { path: PathBuf, child_path: PathBuf }, #[error("Unable to create directory: {}", path.display())] CreateDir { path: PathBuf, #[source] source: std::io::Error, }, #[error("Unable to scan directory: {}", path.display())] ScanDir { path: PathBuf, #[source] source: std::io::Error, }, #[error("Unable to recreate directory: {}", path.display())] RecreateDir { path: PathBuf, #[source] source: std::io::Error, }, #[error("Unable to remove subdirectory: {}", path.display())] RemoveSubDir { path: PathBuf, #[source] source: std::io::Error, }, #[error("Invalid subdirectory name: {}", name)] InvalidSubDirName { name: String, inner_error: Option<std::io::Error>, }, } type Result<T, E = RunDirError> = std::result::Result<T, E>; pub struct RunDir { path: PathBuf, allow_cleaning: bool, } impl RunDir { pub fn new<T: Into<PathBuf>>(path: T) -> RunDir { RunDir { path: path.into(), allow_cleaning: false, } } /// Set whether the directory can perform cleanup operations. /// /// Use with caution. This will clear existing directories on initialization /// and cleanup. pub fn allow_cleaning(mut self, allow_cleaning: bool) -> RunDir { self.allow_cleaning = allow_cleaning; self } /// Creates the initial RunDir. /// /// # Examples /// /// ``` /// use collective::rundir::RunDir; /// /// let rundir = RunDir::new("tests/rundir").allow_cleaning(true); /// /// rundir.initialize().unwrap(); /// /// rundir.cleanup().unwrap(); /// ``` pub fn initialize(&self) -> Result<()> { if Path::exists(&self.path) { info!("RunDir already exists: {}", self.path.display()); if !Path::is_dir(&self.path) { error!("RunDir is not a directory: {}", self.path.display()); return Err(RunDirError::PathIsNotDir { path: self.path.clone(), }); } else { // Scan dir let mut dir_iterator = std::fs::read_dir(&self.path).map_err(|source| RunDirError::ScanDir { path: self.path.clone(), source, })?; let existing_child_path = dir_iterator .next() .map(|entry_result| entry_result.map(|entry| entry.path())); if let Some(child_path) = existing_child_path { let child_path = child_path.map_err(|source| RunDirError::ScanDir { path: self.path.clone(), source, })?; if self.allow_cleaning { info!("Recreating RunDir."); std::fs::remove_dir_all(&self.path).map_err(|source| { RunDirError::RecreateDir { path: self.path.clone(), source, } })?; std::fs::create_dir_all(&self.path).map_err(|source| { RunDirError::RecreateDir { path: self.path.clone(), source, } })?; } else { return Err(RunDirError::DirIsNotEmpty { path: self.path.clone(), child_path, }); } } } } else { info!("Creating new RunDir: {}", self.path.display()); std::fs::create_dir_all(&self.path).map_err(|source| RunDirError::CreateDir { path: self.path.clone(), source, })? } Ok(()) } pub fn cleanup(&self) -> Result<()> { if self.allow_cleaning { std::fs::remove_dir_all(&self.path).map_err(|source| RunDirError::RecreateDir { path: self.path.clone(), source, })?; info!("Cleaned up RunDir: {}", self.path.display()); } else { warn!("Leaving RunDir unmodified. Manual cleanup may be needed."); } Ok(()) } /// Creates a subdir within the RunDir. pub fn create_subdir(&self, name: &str) -> Result<PathBuf> { let pathbuf = self.validate_subdir_name(name)?; std::fs::create_dir(&pathbuf).map_err(|source| RunDirError::CreateDir { path: pathbuf.clone(), source, })?; Ok(pathbuf) } /// Removes a subdir and all its contents from the RunDir. pub fn remove_subdir_all(&self, name: &str) -> Result<()> { let pathbuf = self.validate_subdir_name(name)?; std::fs::remove_dir_all(&pathbuf).map_err(|source| RunDirError::RemoveSubDir { path: pathbuf, source, })?; Ok(()) } /// Checks if a subdir exists within the RunDir. pub fn subdir_exists(&self, name: &str) -> Result<bool> { let pathbuf = self.validate_subdir_name(name)?; Ok(pathbuf.exists()) } fn validate_subdir_name(&self, name: &str) -> Result<PathBuf> { // Check that the name results in a dir exactly one level below the current one. let mut pathbuf = self.path.clone(); pathbuf.push(name); if let Some(parent) = pathbuf.parent() { if parent != self.path.as_path() { return Err(RunDirError::InvalidSubDirName { name: String::from(name), inner_error: None, }); } } else { return Err(RunDirError::InvalidSubDirName { name: String::from(name), inner_error: None, }); } Ok(pathbuf) } } impl AsRef<Path> for RunDir { fn as_ref(&self) -> &Path { &self.path } } #[cfg(test)] mod tests { use super::{RunDir, RunDirError}; use std::path::PathBuf; #[test] fn test_initialize() { // Create dir for the first time. let result = RunDir::new("tests/rundir").initialize(); assert!(result.is_ok()); // Use existing empty dir. let result = RunDir::new("tests/rundir").initialize(); assert!(result.is_ok()); // Fail when dir is not empty and allow_cleaning is not set. std::fs::write("tests/rundir/hello.world", "test").unwrap(); let result = RunDir::new("tests/rundir").initialize(); match result { Err(RunDirError::DirIsNotEmpty { path, child_path }) => { assert_eq!(path, PathBuf::from("tests/rundir")); assert_eq!(child_path, PathBuf::from("tests/rundir/hello.world")); } _ => panic!("Expected an error."), } // Clean existing dir. let result = RunDir::new("tests/rundir") .allow_cleaning(true) .initialize(); assert!(result.is_ok()); std::fs::remove_dir("tests/rundir").unwrap(); // Fail when dir is not a directory. std::fs::write("tests/rundir", "hello").unwrap(); let result = RunDir::new("tests/rundir").initialize(); match result { Err(RunDirError::PathIsNotDir { path }) => { assert_eq!(path, PathBuf::from("tests/rundir")); } _ => panic!("Expected an error."), } std::fs::remove_file("tests/rundir").unwrap(); } #[test] fn test_subdirs() { let rundir = RunDir::new("tests/rundir2").allow_cleaning(true); rundir.initialize().unwrap(); assert!(!PathBuf::from("tests/rundir2/subtest").exists()); assert!(!rundir.subdir_exists("subtest").unwrap()); rundir.create_subdir("subtest").unwrap(); assert!(PathBuf::from("tests/rundir2/subtest").exists()); assert!(rundir.subdir_exists("subtest").unwrap()); rundir.remove_subdir_all("subtest").unwrap(); assert!(!PathBuf::from("tests/rundir2/subtest").exists()); assert!(!rundir.subdir_exists("subtest").unwrap()); rundir.cleanup().unwrap(); } }