diff --git a/Cargo.lock b/Cargo.lock index 352f1cf0..1c3dbb03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1455,6 +1455,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", + "walkdir", ] [[package]] @@ -2184,6 +2185,15 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.23" @@ -3253,6 +3263,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3335,6 +3355,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index e674ce9c..87820175 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ tower = { version = "0.4", features = ["util"] } tower-http = { version = "0.5.0", features = ["trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +walkdir = "2.4.0" [build-dependencies] built = "0.7" diff --git a/src/utils/fs/folders.rs b/src/utils/fs/folders.rs new file mode 100644 index 00000000..728f1a20 --- /dev/null +++ b/src/utils/fs/folders.rs @@ -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>(root: P, max_depth: u8) -> Vec { + let mut folders = Vec::::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::, _>>() + .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>( + top_paths: &[P], + depth_levels: &[u8], +) -> Vec { + 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::>() + .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::>() + }) + .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::() + .unwrap() + .contains("top path is not a directory or it can not be canonicalized")); + + assert!(result + .as_ref() + .err() + .unwrap() + .downcast_ref::() + .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::() + .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::().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::>() + .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); + } +} diff --git a/src/utils/fs/mod.rs b/src/utils/fs/mod.rs new file mode 100644 index 00000000..9e57c0bc --- /dev/null +++ b/src/utils/fs/mod.rs @@ -0,0 +1 @@ +pub mod folders; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 82e03f9a..b7c50376 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,4 @@ +pub mod fs; + #[cfg(test)] pub mod test;