pub mod naming;
pub mod sync;
pub(crate) mod util;
use anyhow::{Context, Result};
use regex::Regex;
use std::path::{Component, Path, PathBuf};
use tokio::fs;
use util::{iteritems, FtIterItemState};
#[derive(Debug)]
pub enum FtFilter {
Raw(String),
Path(PathBuf),
Regex(Regex),
}
pub fn is_subdir(path: impl AsRef<Path>, dir: impl AsRef<Path>) -> bool {
for component in path.as_ref().components() {
if let Component::Normal(p) = component {
if p == dir.as_ref().as_os_str() {
return true;
}
}
}
false
}
pub fn path_contains(path: impl AsRef<Path>, pattern: impl AsRef<Path> ) -> bool {
if let Some(p) = path.as_ref().to_str() {
if let Some(pat) = pattern.as_ref().to_str() {
return p.contains(pat);
}
}
false
}
pub async fn ensure_directory(dir: impl AsRef<Path>) -> Result<()> {
if !dir.as_ref().exists() {
fs::create_dir_all(dir)
.await
.context("unable to create directory")?;
}
Ok(())
}
pub async fn create_multiple_directories(
path: impl AsRef<Path>,
directories: &[impl AsRef<Path>],
) -> Result<()> {
for dir in directories {
let target = path.as_ref().join(dir);
ensure_directory(target).await?;
}
Ok(())
}
pub async fn create_numeric_directories(
path: impl AsRef<Path>,
start: usize,
end: usize,
fill: usize,
) -> Result<()> {
for i in start..end {
let name = path
.as_ref()
.join(naming::generate_n_digit_name(i, fill, ""));
ensure_directory(name)
.await
.context("creating numeric directories")?;
}
Ok(())
}
pub async fn list_files<P: AsRef<Path> + Send>(path: P) -> Result<Vec<PathBuf>> {
anyhow::ensure!(path.as_ref().exists(), "path does not exist");
anyhow::ensure!(
path.as_ref().is_dir(),
"path should be a directory, not a file"
);
iteritems(path, FtIterItemState::File, None).await
}
pub async fn list_nested_files<P: AsRef<Path> + Send>(path: P) -> Result<Vec<PathBuf>> {
anyhow::ensure!(path.as_ref().exists(), "path does not exist");
anyhow::ensure!(
path.as_ref().is_dir(),
"path should be a directory, not a file"
);
iteritems(path, FtIterItemState::RFile, None).await
}
pub async fn list_files_with_filter<P: AsRef<Path> + Send>(
path: P,
pattern: FtFilter,
) -> Result<Vec<PathBuf>> {
anyhow::ensure!(path.as_ref().exists(), "path does not exist");
anyhow::ensure!(
path.as_ref().is_dir(),
"path should be a directory, not a file"
);
iteritems(path, FtIterItemState::File, Some(&pattern)).await
}
pub async fn list_nested_files_with_filter<P: AsRef<Path> + Send>(
path: P,
pattern: FtFilter,
) -> Result<Vec<PathBuf>> {
anyhow::ensure!(path.as_ref().exists(), "path does not exist");
anyhow::ensure!(
path.as_ref().is_dir(),
"path should be a directory, not a file"
);
iteritems(path, FtIterItemState::RFile, Some(&pattern)).await
}
pub async fn list_directories<P: AsRef<Path> + Send>(path: P) -> Result<Vec<PathBuf>> {
anyhow::ensure!(path.as_ref().exists(), "path does not exist");
anyhow::ensure!(
path.as_ref().is_dir(),
"path should be a directory, not a file"
);
iteritems(path, FtIterItemState::Dir, None).await
}
pub async fn list_nested_directories<P: AsRef<Path> + Send>(path: P) -> Result<Vec<PathBuf>> {
anyhow::ensure!(path.as_ref().exists(), "path does not exist");
iteritems(path, FtIterItemState::RDir, None).await
}
pub async fn list_directories_with_filter<P: AsRef<Path> + Send>(
path: P,
filter: FtFilter,
) -> Result<Vec<PathBuf>> {
anyhow::ensure!(path.as_ref().exists(), "path does not exist");
anyhow::ensure!(
path.as_ref().is_dir(),
"path should be a directory, not a file"
);
iteritems(path, FtIterItemState::Dir, Some(&filter)).await
}
pub async fn list_nested_directories_with_filter<P: AsRef<Path> + Send>(
path: P,
filter: FtFilter,
) -> Result<Vec<PathBuf>> {
anyhow::ensure!(path.as_ref().exists(), "path does not exist");
anyhow::ensure!(
path.as_ref().is_dir(),
"path should be a directory, not a file"
);
iteritems(path, FtIterItemState::RDir, Some(&filter)).await
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::{Context, Result};
use std::path::PathBuf;
use util::TempPath;
#[tokio::test]
async fn creates_a_directory() -> Result<()> {
let tmp = std::env::temp_dir();
let single_path = tmp.join("create_dir");
ensure_directory(&single_path)
.await
.context("create directory single")?;
assert!(single_path.exists());
let nested_path = tmp.join("create_dir/test/this/is/nested");
ensure_directory(&nested_path)
.await
.context("create directory nested")?;
assert!(nested_path.exists());
std::fs::remove_dir_all(single_path)?;
Ok(())
}
#[tokio::test]
async fn checks_if_a_directory_is_a_subdirectory() -> Result<()> {
let root = TempPath::new("is_subdir").await?;
let nested = root
.nest_folders(vec!["this", "is", "a", "nested", "tmp", "dir"])
.await?;
let mut result = is_subdir(&nested.path, "nested");
assert!(result);
result = is_subdir(&nested.path, "not_valid");
assert!(!result);
Ok(())
}
#[test]
fn check_path_contains_subpath() {
let main = "I/am/a/path/hello/there";
assert!(path_contains(main, "a/path"));
assert!(!path_contains(main, "not"));
let main = Path::new(main);
assert!(path_contains(main, Path::new("a/path")));
assert!(!path_contains(main, Path::new("not")));
let main = PathBuf::from("I/am/a/path/hello/there");
assert!(path_contains(&main, PathBuf::from("a/path")));
assert!(!path_contains(main, PathBuf::from("not")));
assert!(path_contains(
String::from("I/am/a/path/hello/there"),
String::from("a/path")
));
assert!(!path_contains(
String::from("I/am/a/path/hello/there"),
String::from("not")
));
}
#[tokio::test]
async fn check_list_files_works() -> Result<()> {
let root = TempPath::new("lf_test").await?;
root.multi_file(vec!["first.rs", "second.c", "third.js", "fourth.rb"])
.await?;
let res = list_files(root.path.clone()).await?;
assert_eq!(res.len(), 4);
assert!(list_files("IDoNotExistAsADirectoryOrShouldntAtLeAst")
.await
.is_err());
Ok(())
}
#[tokio::test]
async fn check_list_nested_files_works() -> Result<()> {
let root = TempPath::new("lfr_test").await?;
let ffolder = root.new_folder("ffolder").await?;
let sfolder = root.new_folder("sfolder").await?;
let tfolder = root.new_folder("tfolder").await?;
root.new_file("initial.pdf").await?;
ffolder.new_file("first.rs").await?;
sfolder.multi_file(vec!["second.txt", "third.php"]).await?;
tfolder.new_file("fourth.cpp").await?;
let res = list_nested_files(&root.path).await?;
assert_eq!(res.len(), 5);
assert!(list_files("IDoNotExistAsADirectoryOrShouldntAtLeAst")
.await
.is_err());
Ok(())
}
#[tokio::test]
async fn check_list_directories_works() -> Result<()> {
let root = TempPath::new("lfolder_test").await?;
root.multi_folder(vec!["folder1", "folder2", "folder3", "folder4"])
.await?;
let res = list_directories(root.path.clone()).await?;
assert_eq!(res.len(), 4);
assert!(list_directories("non-existant_path").await.is_err());
Ok(())
}
#[tokio::test]
async fn check_list_nested_directories_works() -> Result<()> {
let root = TempPath::new("lfolderrec_test").await?;
root.multi_folder(vec!["folder1", "folder2"]).await?;
let f1 = TempPath::new(root.join("folder1")).await?;
f1.multi_folder(vec!["sub1", "sub2", "sub3"]).await?;
let s2 = TempPath::new(f1.join("sub2")).await?;
s2.multi_folder(vec!["deep1", "deep2"]).await?;
let res = list_nested_directories(root.path.clone()).await?;
assert_eq!(res.len(), 7);
assert!(list_nested_directories("not-a-valId_pathd").await.is_err());
Ok(())
}
#[tokio::test]
async fn numeric_directories() -> Result<()> {
let tmp = TempPath::new("numeric_directories").await?;
create_numeric_directories(&tmp.path, 0, 100, 4).await?;
let mut folders = list_directories(&tmp.path).await?;
folders.sort();
assert_eq!(folders.len(), 100);
for (i, folder) in folders.into_iter().enumerate() {
let test = &tmp.path.join(format!("{:0fill$}", i, fill = 4));
assert_eq!(&folder, test);
}
Ok(())
}
#[tokio::test]
async fn multiple_directory_creation() -> Result<()> {
let tmp = TempPath::new("create_multiple_dirs").await?;
let dirs = ["config", "src", "tests"];
create_multiple_directories(&tmp.path, &dirs).await?;
let folders = list_directories(&tmp.path).await?;
assert_eq!(folders.len(), 3);
for check in dirs {
let target = tmp.path.join(check);
assert!(folders.contains(&target));
}
Ok(())
}
#[tokio::test]
async fn files_filter() -> Result<()> {
let root = TempPath::new("filter_files").await?;
root.multi_file(vec!["first.rs", "second.rs", "third.js", "fourth.rb"])
.await?;
let mut filter = FtFilter::Raw("fourth".to_string());
let mut result = list_files_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 1);
assert_eq!(result[0], root.path.join("fourth.rb"));
filter = FtFilter::Path(PathBuf::from("third.js"));
result = list_files_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 1);
assert_eq!(result[0], root.path.join("third.js"));
filter = FtFilter::Regex(Regex::new(r"(.*)\.rs").unwrap());
result = list_files_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 2);
assert!(result.contains(&root.path.join("first.rs")));
assert!(result.contains(&root.path.join("second.rs")));
Ok(())
}
#[tokio::test]
async fn files_filter_is_empty() -> Result<()> {
let root = TempPath::new("filter_files_empty").await?;
let mut filter = FtFilter::Raw("non-existant".to_string());
let mut result = list_files_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Raw("non-existant".to_string());
result = list_nested_files_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Path(PathBuf::from("another-missing"));
result = list_files_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Path(PathBuf::from("another-missing"));
result = list_nested_files_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Regex(Regex::new(r"(.*)\.rs").unwrap());
result = list_files_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Regex(Regex::new(r"(.*)\.rs").unwrap());
result = list_nested_files_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
Ok(())
}
#[tokio::test]
async fn list_items_error() -> Result<()> {
let root = TempPath::new("filter_files_error").await?;
let test = root.new_file("test.js").await?;
assert!(list_files(&test.path).await.is_err());
assert!(list_nested_files(&test.path).await.is_err());
assert!(
list_files_with_filter(&test.path, FtFilter::Raw("filter".to_string()))
.await
.is_err()
);
assert!(
list_nested_files_with_filter(&test.path, FtFilter::Raw("filter".to_string()))
.await
.is_err()
);
assert!(list_directories(&test.path).await.is_err());
assert!(list_nested_directories(&test.path).await.is_err());
assert!(
list_directories_with_filter(&test.path, FtFilter::Raw("filter".to_string()))
.await
.is_err()
);
assert!(list_nested_directories_with_filter(
&test.path,
FtFilter::Raw("filter".to_string())
)
.await
.is_err());
Ok(())
}
#[tokio::test]
async fn nested_files_filter() -> Result<()> {
let root = TempPath::new("nested_filter_files").await?;
let ffolder = root.new_folder("ffolder").await?;
let sfolder = root.new_folder("sfolder").await?;
let tfolder = root.new_folder("tfolder").await?;
root.new_file("initial.pdf").await?;
ffolder.new_file("first.rs").await?;
sfolder.multi_file(vec!["second.txt", "third.rs"]).await?;
tfolder.new_file("initial.cpp").await?;
let mut filter = FtFilter::Raw("initial".to_string());
let mut result = list_nested_files_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 2);
assert!(result.contains(&root.path.join("tfolder/initial.cpp")));
assert!(result.contains(&root.path.join("initial.pdf")));
filter = FtFilter::Path(PathBuf::from("second.txt"));
result = list_nested_files_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 1);
assert_eq!(result[0], root.path.join("sfolder/second.txt"));
Ok(())
}
#[tokio::test]
async fn directories_filter() -> Result<()> {
let root = TempPath::new("dir_filter").await?;
root.multi_folder(vec!["log_var", "store_var", "config", "etc"])
.await?;
let mut filter = FtFilter::Raw("config".to_string());
let mut result = list_directories_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 1);
assert_eq!(result[0], root.path.join("config"));
filter = FtFilter::Path(PathBuf::from("etc"));
result = list_directories_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 1);
assert_eq!(result[0], root.path.join("etc"));
filter = FtFilter::Regex(Regex::new(r"(.*)_var").unwrap());
result = list_directories_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 2);
assert!(result.contains(&root.path.join("log_var")));
assert!(result.contains(&root.path.join("store_var")));
Ok(())
}
#[tokio::test]
async fn nested_directories_filter() -> Result<()> {
let root = TempPath::new("nested_dir_filter_test").await?;
root.multi_folder(vec!["folder1", "folder2"]).await?;
let f1 = TempPath::new(root.join("folder1")).await?;
f1.multi_folder(vec!["sub1", "sub_2", "sub3"]).await?;
let s2 = TempPath::new(f1.join("sub_2")).await?;
s2.multi_folder(vec!["deep_1", "deep2"]).await?;
let mut filter = FtFilter::Raw("deep".to_string());
let mut result = list_nested_directories_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 2);
assert!(result.contains(&root.path.join("folder1/sub_2/deep_1")));
assert!(result.contains(&root.path.join("folder1/sub_2/deep2")));
filter = FtFilter::Path(PathBuf::from("folder1"));
result = list_nested_directories_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 6);
filter = FtFilter::Regex(Regex::new(r"(.*)_[0-9]{1}").unwrap());
result = list_nested_directories_with_filter(&root.path, filter).await?;
assert_eq!(result.len(), 3);
Ok(())
}
#[tokio::test]
async fn list_dirs_empty() -> Result<()> {
let root = TempPath::new("list_dirs_empty").await?;
let mut filter = FtFilter::Raw("non-existant".to_string());
let mut result = list_directories_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Raw("non-existant".to_string());
result = list_nested_directories_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Path(PathBuf::from("another-missing"));
result = list_directories_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Path(PathBuf::from("another-missing"));
result = list_nested_directories_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Regex(Regex::new(r"(.*)\.rs").unwrap());
result = list_directories_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
filter = FtFilter::Regex(Regex::new(r"(.*)\.rs").unwrap());
result = list_nested_directories_with_filter(&root.path, filter).await?;
assert!(result.is_empty());
Ok(())
}
}