-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,310 @@ | ||
use concat_string::concat_string; | ||
use futures::stream::{self, StreamExt, TryStreamExt}; | ||
use std::path::{Path, PathBuf}; | ||
use tokio::fs::*; | ||
use walkdir::WalkDir; | ||
|
||
fn get_deepest_folders<P: AsRef<Path>>(root: P, max_depth: u8) -> Vec<PathBuf> { | ||
let mut folders = Vec::<PathBuf>::default(); | ||
|
||
let entries = WalkDir::new(&root) | ||
.max_depth(max_depth.into()) | ||
.into_iter() | ||
.filter_entry(|entry| { | ||
entry | ||
.metadata() | ||
.expect(&concat_string!( | ||
"can not read metadata of ", | ||
entry.path().to_string_lossy() | ||
)) | ||
.is_dir() | ||
}) | ||
.collect::<Result<Vec<_>, _>>() | ||
.expect(&concat_string!( | ||
"can not traverse ", | ||
root.as_ref().to_string_lossy() | ||
)); | ||
|
||
for i in 0..entries.len() { | ||
if i == entries.len() - 1 || !entries[i + 1].path().starts_with(entries[i].path()) { | ||
// if it is not a children of the `previous_entry`, | ||
// it means that the `previous_entry` is a deepest folder, | ||
// add it to the result and `previous_entry` back to its parent. | ||
// The last one is always a deepest folder. | ||
folders.push(entries[i].path().to_path_buf()); | ||
} | ||
} | ||
|
||
folders | ||
} | ||
|
||
pub async fn build_music_folders<P: AsRef<Path>>( | ||
top_paths: &[P], | ||
depth_levels: &[u8], | ||
) -> Vec<PathBuf> { | ||
let canonicalized_top_paths = stream::iter(top_paths) | ||
.then(|path| async move { | ||
if !metadata(path).await?.is_dir() { | ||
Err(std::io::Error::new( | ||
std::io::ErrorKind::InvalidInput, | ||
concat_string!(path.as_ref().to_string_lossy(), " is not a directory"), | ||
)) | ||
} else { | ||
canonicalize(&path).await | ||
} | ||
}) | ||
.try_collect::<Vec<_>>() | ||
.await | ||
.expect("top path is not a directory or it can not be canonicalized"); | ||
|
||
for i in 0..canonicalized_top_paths.len() - 1 { | ||
for j in i + 1..canonicalized_top_paths.len() { | ||
if canonicalized_top_paths[i].starts_with(&canonicalized_top_paths[j]) | ||
|| canonicalized_top_paths[j].starts_with(&canonicalized_top_paths[i]) | ||
{ | ||
std::panic::panic_any(concat_string!( | ||
&canonicalized_top_paths[i].to_string_lossy(), | ||
" and ", | ||
&canonicalized_top_paths[j].to_string_lossy(), | ||
" contain each other" | ||
)) | ||
} | ||
} | ||
} | ||
|
||
if depth_levels.is_empty() { | ||
return canonicalized_top_paths; | ||
} else if depth_levels.len() != top_paths.len() { | ||
std::panic::panic_any("depth levels and top paths must have the same length") | ||
} | ||
|
||
let depth_levels = depth_levels.to_owned(); | ||
tokio::task::spawn_blocking(move || { | ||
canonicalized_top_paths | ||
.iter() | ||
.zip(depth_levels.iter()) | ||
.flat_map(|(root, depth)| get_deepest_folders(root, *depth)) | ||
.collect::<Vec<_>>() | ||
}) | ||
.await | ||
.expect("can not get deepest folders from top paths") | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::utils::test::fs::TemporaryFs; | ||
|
||
use futures::FutureExt; | ||
use std::str::FromStr; | ||
|
||
#[tokio::test] | ||
async fn test_top_paths_non_existent() { | ||
let result = build_music_folders(&[PathBuf::from_str("/non-existent").unwrap()], &[0]) | ||
.catch_unwind() | ||
.await; | ||
|
||
assert!(result | ||
.as_ref() | ||
.err() | ||
.unwrap() | ||
.downcast_ref::<String>() | ||
.unwrap() | ||
.contains("top path is not a directory or it can not be canonicalized")); | ||
|
||
assert!(result | ||
.as_ref() | ||
.err() | ||
.unwrap() | ||
.downcast_ref::<String>() | ||
.unwrap() | ||
.contains("No such file or directory")); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_top_paths_is_file() { | ||
let temp_fs = TemporaryFs::new(); | ||
let file = temp_fs.create_file("test.txt").await; | ||
|
||
let result = build_music_folders(&[file.clone()], &[0]) | ||
.catch_unwind() | ||
.await; | ||
|
||
assert!(result | ||
.err() | ||
.unwrap() | ||
.downcast_ref::<String>() | ||
.unwrap() | ||
.contains(&concat_string!( | ||
&file.to_string_lossy(), | ||
" is not a directory" | ||
))); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_top_paths_nested() { | ||
let temp_fs = TemporaryFs::new(); | ||
let parent = temp_fs.create_dir("test1/").await; | ||
let child = temp_fs.create_dir("test1/test2/").await; | ||
|
||
let result = build_music_folders(&[parent.clone(), child.clone()], &[0, 0]) | ||
.catch_unwind() | ||
.await; | ||
|
||
assert_eq!( | ||
*result.err().unwrap().downcast_ref::<String>().unwrap(), | ||
concat_string!( | ||
&parent.canonicalize().unwrap().to_string_lossy(), | ||
" and ", | ||
&child.canonicalize().unwrap().to_string_lossy(), | ||
" contain each other" | ||
) | ||
); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_top_paths_depth_levels_empty() { | ||
let temp_fs = TemporaryFs::new(); | ||
let dir_1 = temp_fs.create_dir("test1/").await; | ||
let dir_2 = temp_fs.create_dir("test2/").await; | ||
|
||
let mut input = vec![dir_1, dir_2]; | ||
let mut result = build_music_folders(&input, &[]).await; | ||
|
||
input = stream::iter(input) | ||
.then(canonicalize) | ||
.try_collect::<Vec<_>>() | ||
.await | ||
.unwrap(); | ||
result.sort(); | ||
assert_eq!(input, result); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_top_paths_depth_levels_neq_len() { | ||
let temp_fs = TemporaryFs::new(); | ||
let dir_1 = temp_fs.create_dir("test1/").await; | ||
let dir_2 = temp_fs.create_dir("test2/").await; | ||
|
||
let result = build_music_folders(&[dir_1, dir_2], &[0, 0, 0]) | ||
.catch_unwind() | ||
.await; | ||
|
||
assert_eq!( | ||
*result.err().unwrap().downcast_ref::<&str>().unwrap(), | ||
"depth levels and top paths must have the same length" | ||
); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_get_deepest_folder() { | ||
let temp_fs = TemporaryFs::new(); | ||
temp_fs.create_dir("test1/test1.1/test1.1.1/").await; | ||
temp_fs | ||
.create_dir("test1/test1.1/test1.1.2/test1.1.2.1/test1.1.2.1.1/") | ||
.await; | ||
temp_fs.create_dir("test1/test1.2/").await; | ||
temp_fs | ||
.create_dir("test1/test1.3/test1.3.1/test1.3.1.1/") | ||
.await; | ||
temp_fs.create_dir("test2/").await; | ||
|
||
let mut inputs = temp_fs.join_paths(&[ | ||
"test1/test1.1/test1.1.1/", | ||
"test1/test1.1/test1.1.2/test1.1.2.1/test1.1.2.1.1/", | ||
"test1/test1.2/", | ||
"test1/test1.3/test1.3.1/test1.3.1.1/", | ||
"test2/", | ||
]); | ||
let mut results = get_deepest_folders(temp_fs.get_root_path(), u8::MAX); | ||
|
||
inputs.sort(); | ||
results.sort(); | ||
assert_eq!(inputs, results); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_get_deepest_folder_max_depth() { | ||
let temp_fs = TemporaryFs::new(); | ||
temp_fs.create_dir("test1/test1.1/test1.1.1/").await; | ||
temp_fs | ||
.create_dir("test1/test1.1/test1.1.2/test1.1.2.1/test1.1.2.1.1/") | ||
.await; | ||
temp_fs.create_dir("test1/test1.2/").await; | ||
temp_fs | ||
.create_dir("test1/test1.3/test1.3.1/test1.3.1.1/") | ||
.await; | ||
temp_fs.create_dir("test2/").await; | ||
|
||
let mut inputs = temp_fs.join_paths(&[ | ||
"test1/test1.1/test1.1.1/", | ||
"test1/test1.1/test1.1.2/", | ||
"test1/test1.2/", | ||
"test1/test1.3/test1.3.1/", | ||
"test2/", | ||
]); | ||
let mut results = get_deepest_folders(temp_fs.get_root_path(), 3); | ||
|
||
inputs.sort(); | ||
results.sort(); | ||
assert_eq!(inputs, results); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_get_deepest_folder_file() { | ||
let temp_fs = TemporaryFs::new(); | ||
temp_fs | ||
.create_file("test1/test1.1/test1.1.1/test1.1.1.1.txt") | ||
.await; | ||
temp_fs | ||
.create_dir("test1/test1.1/test1.1.2/test1.1.2.1/test1.1.2.1.1/") | ||
.await; | ||
temp_fs.create_dir("test1/test1.2/").await; | ||
temp_fs | ||
.create_dir("test1/test1.3/test1.3.1/test1.3.1.1/") | ||
.await; | ||
temp_fs.create_file("test2/test2.1.txt").await; | ||
|
||
let mut inputs = temp_fs.join_paths(&[ | ||
"test1/test1.1/test1.1.1/", | ||
"test1/test1.1/test1.1.2/", | ||
"test1/test1.2/", | ||
"test1/test1.3/test1.3.1/", | ||
"test2/", | ||
]); | ||
let mut results = get_deepest_folders(temp_fs.get_root_path(), 3); | ||
|
||
inputs.sort(); | ||
results.sort(); | ||
assert_eq!(inputs, results); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_build_music_folders() { | ||
let temp_fs = TemporaryFs::new(); | ||
temp_fs | ||
.create_file("test1/test1.1/test1.1.1/test1.1.1.1.txt") | ||
.await; | ||
temp_fs | ||
.create_dir("test1/test1.1/test1.1.2/test1.1.2.1/test1.1.2.1.1/") | ||
.await; | ||
temp_fs.create_dir("test1/test1.2/").await; | ||
temp_fs | ||
.create_dir("test1/test1.3/test1.3.1/test1.3.1.1/") | ||
.await; | ||
temp_fs.create_file("test2/test2.1.txt").await; | ||
|
||
let mut inputs = temp_fs.canonicalize_paths(&temp_fs.join_paths(&[ | ||
"test1/test1.1/", | ||
"test1/test1.2/", | ||
"test1/test1.3/", | ||
"test2/", | ||
])); | ||
let mut results = | ||
build_music_folders(&temp_fs.join_paths(&["test1/", "test2/"]), &[1, 2]).await; | ||
|
||
inputs.sort(); | ||
results.sort(); | ||
assert_eq!(inputs, results); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod folders; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
pub mod fs; | ||
|
||
#[cfg(test)] | ||
pub mod test; |