Skip to content

Commit

Permalink
utils(fs): add build music folders
Browse files Browse the repository at this point in the history
  • Loading branch information
vnghia committed Jan 11, 2024
1 parent 4a2de95 commit 1412aec
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 0 deletions.
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
310 changes: 310 additions & 0 deletions src/utils/fs/folders.rs
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);
}
}
1 change: 1 addition & 0 deletions src/utils/fs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod folders;
2 changes: 2 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod fs;

#[cfg(test)]
pub mod test;

0 comments on commit 1412aec

Please sign in to comment.