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,
}
}
pub fn allow_cleaning(mut self, allow_cleaning: bool) -> RunDir {
self.allow_cleaning = allow_cleaning;
self
}
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 {
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(())
}
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)
}
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(())
}
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> {
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() {
let result = RunDir::new("tests/rundir").initialize();
assert!(result.is_ok());
let result = RunDir::new("tests/rundir").initialize();
assert!(result.is_ok());
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."),
}
let result = RunDir::new("tests/rundir")
.allow_cleaning(true)
.initialize();
assert!(result.is_ok());
std::fs::remove_dir("tests/rundir").unwrap();
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();
}
}