diff --git a/Cargo.lock b/Cargo.lock index 5946e28..b3ecd35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + [[package]] name = "async-recursion" version = "0.3.2" @@ -13,18 +22,188 @@ dependencies = [ "syn", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "dfm" version = "0.1.0" dependencies = [ "async-recursion", + "clap", + "futures", + "git2", "tokio", + "xdg", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" + +[[package]] +name = "futures-executor" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" + +[[package]] +name = "futures-macro" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" +dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" + +[[package]] +name = "futures-task" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" + +[[package]] +name = "futures-util" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +dependencies = [ + "autocfg", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "git2" +version = "0.13.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659cd14835e75b64d9dba5b660463506763cf0aa6cb640aeeb0e98d841093490" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", ] [[package]] @@ -36,12 +215,93 @@ dependencies = [ "libc", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + [[package]] name = "libc" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5" +[[package]] +name = "libgit2-sys" +version = "0.12.22+1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c53ac117c44f7042ad8d8f5681378dfbc6010e49ec2c0d1f11dfedc7a4a1c3" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0186af0d8f171ae6b9c4c90ec51898bad5d08a2d5e470903a50d9ad8959cbee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + [[package]] name = "num_cpus" version = "1.13.0" @@ -52,12 +312,61 @@ dependencies = [ "libc", ] +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + [[package]] name = "pin-project-lite" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + [[package]] name = "proc-macro2" version = "1.0.28" @@ -76,6 +385,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "slab" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "syn" version = "1.0.75" @@ -87,6 +408,30 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "tokio" version = "1.10.1" @@ -110,8 +455,81 @@ dependencies = [ "syn", ] +[[package]] +name = "unicode-bidi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xdg" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" diff --git a/Cargo.toml b/Cargo.toml index 6e89e1a..75bb543 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,8 @@ codegen-units = 1 [dependencies] async-recursion = "0.3" +clap = "2.33" +futures = "0.3" +git2 = "0.13" tokio = { version = "1.10", features = ["fs", "macros", "rt-multi-thread"] } +xdg = "2.2" diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..ea77f35 --- /dev/null +++ b/src/build.rs @@ -0,0 +1,71 @@ +use std::path::PathBuf; + +use async_recursion::async_recursion; +use futures::future::join_all; +use tokio::fs::{copy, create_dir, read_dir}; + +use crate::Context; + +pub async fn build_tree(context: &Context) -> std::io::Result<()> { + dir(context, PathBuf::new()).await +} + +#[async_recursion] +async fn dir(context: &Context, relative_path: PathBuf) -> std::io::Result<()> { + let source_path = context.tree_source_dir.join(&relative_path); + let build_path = context.tree_build_dir.join(&relative_path); + + match create_dir(build_path).await { + Err(e) if e.kind() != std::io::ErrorKind::AlreadyExists => { + return Err(e); + } + _ => (), + } + + let mut dir_walker = read_dir(source_path).await?; + + let mut dir_tasks = vec![]; + let mut file_tasks = vec![]; + + while let Some(entry) = dir_walker.next_entry().await? { + let metadata = entry.metadata().await?; + let os_name = entry.file_name(); + let name = os_name.to_str(); + if name == Some(".git") { + continue; + } + + let relative_path = relative_path.join(entry.file_name()); + + if metadata.is_dir() { + dir_tasks.push(dir(context, relative_path)); + } else if metadata.is_file() { + file_tasks.push(file(context, relative_path)); + } + } + + let dir_tasks = async { + join_all(dir_tasks) + .await + .into_iter() + .collect::>>() + }; + let file_tasks = async { + join_all(file_tasks) + .await + .into_iter() + .collect::>>() + }; + + tokio::try_join!(dir_tasks, file_tasks)?; + + Ok(()) +} + +async fn file(context: &Context, relative_path: PathBuf) -> std::io::Result<()> { + let source_path = context.tree_source_dir.join(&relative_path); + let build_path = context.tree_build_dir.join(&relative_path); + + copy(source_path, build_path).await?; + Ok(()) +} diff --git a/src/check.rs b/src/check.rs new file mode 100644 index 0000000..e04f13c --- /dev/null +++ b/src/check.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use tokio::fs::read; + +use crate::{tree::get_tree_files, Context}; + +pub async fn check_tree( + context: &Context, +) -> std::io::Result<(Vec, Vec, Vec)> { + let source_files = get_tree_files(&context.tree_source_dir).await?; + let installed_files = get_tree_files(&context.tree_install_dir).await?; + + let mut deleted_files = vec![]; + let mut changed_files = vec![]; + for installed_file in installed_files.iter() { + if source_files.contains(installed_file) { + let source_file_content = read(context.tree_build_dir.join(installed_file)).await?; + let installed_file_content = + read(context.tree_install_dir.join(installed_file)).await?; + if source_file_content != installed_file_content { + changed_files.push(installed_file.clone()); + } + } else { + deleted_files.push(installed_file.clone()); + } + } + + let mut added_files = vec![]; + for source_file in source_files.iter() { + if !installed_files.contains(source_file) { + added_files.push(source_file.clone()); + } + } + + Ok((added_files, changed_files, deleted_files)) +} diff --git a/src/link.rs b/src/link.rs new file mode 100644 index 0000000..994056a --- /dev/null +++ b/src/link.rs @@ -0,0 +1,52 @@ +use std::path::PathBuf; + +use tokio::fs::{copy, create_dir_all, remove_file, symlink}; + +use crate::{check::check_tree, Context}; + +pub async fn link_tree(context: &Context) -> std::io::Result<()> { + let (add, update, remove) = check_tree(context).await?; + for file in add { + copy_and_link(context, file).await?; + } + + for file in update { + copy_and_link(context, file).await?; + } + + for file in remove { + remove_file(context.link_root_dir.join(file)).await?; + } + + Ok(()) +} + +async fn copy_and_link(context: &Context, relative_path: PathBuf) -> std::io::Result<()> { + // Create all necessary directories that are accessed + let mut current_install_dir = context.tree_install_dir.join(&relative_path); + current_install_dir.pop(); + let mut current_link_dir = context.link_root_dir.join(&relative_path); + current_link_dir.pop(); + create_dir_all(current_install_dir).await?; + create_dir_all(current_link_dir).await?; + + // Copy from build to install + copy( + context.tree_build_dir.join(&relative_path), + context.tree_install_dir.join(&relative_path), + ) + .await?; + + // Make sure symlink doesn't exist before attempting to symlink + match remove_file(context.link_root_dir.join(&relative_path)).await { + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e), + }; + symlink( + context.tree_install_dir.join(&relative_path), + context.link_root_dir.join(&relative_path), + ) + .await?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index dca5612..51db49e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,47 +1,88 @@ -use async_recursion::async_recursion; -use tokio::fs; +mod build; +mod check; +mod link; +mod tree; + +use std::path::PathBuf; + +use build::build_tree; +use check::check_tree; +use link::link_tree; + +const APP_VERSION: &'static str = env!("CARGO_PKG_VERSION"); #[tokio::main] async fn main() -> std::io::Result<()> { - let home_dir = std::env::var("HOME").expect("Could not get home dir"); - symlink_folder(&format!("{}/.df", home_dir), &home_dir).await; - Ok(()) -} + let app = clap::App::new("DotFiles Manager") + .version(APP_VERSION) + .author("Rasmus Rosengren ") + .about("Utility to manage dotfiles") + .arg( + clap::Arg::with_name("dry-run") + .long("dry-run") + .short("d") + .help("Report changes without installing"), + ) + .arg( + clap::Arg::with_name("repo-path") + .long("repo-path") + .short("r") + .default_value(".df") + .help("Location of repo, relative to $HOME"), + ); + + let matches = app.get_matches(); + + let home_dir = std::env::var("HOME").expect("$HOME variable not found"); + let xdg_dirs = xdg::BaseDirectories::with_prefix("dfm").expect("xdg dirs"); -#[async_recursion] -async fn symlink_folder(src_dir: &str, target_dir: &str) { - fs::create_dir_all(target_dir) - .await - .unwrap_or_else(|_| panic!("Could not create {}", target_dir)); - let mut dir_contents = fs::read_dir(src_dir) - .await - .unwrap_or_else(|_| panic!("Could read dir {}", src_dir)); - while let Ok(Some(entry)) = dir_contents.next_entry().await { - let os_name = entry.file_name(); - let name = os_name.to_str().unwrap(); - if name == ".git" { - continue; + let context = Context { + tree_source_dir: format!("{}/{}", home_dir, matches.value_of("repo-path").unwrap()).into(), + tree_build_dir: xdg_dirs + .create_cache_directory("tree") + .expect("xdg cache dir"), + tree_install_dir: xdg_dirs + .create_config_directory("tree") + .expect("xdg config dir"), + link_root_dir: home_dir.into(), + }; + + build_tree(&context).await?; + if matches.is_present("dry-run") { + let (add, change, delete) = check_tree(&context).await?; + if !add.is_empty() { + println!("The following files have been added:"); + for file in add { + println!("\t{}", file.to_string_lossy()); + } + println!(); } - if let Ok(metadata) = entry.metadata().await { - if metadata.is_dir() { - symlink_folder( - &format!("{}/{}", src_dir, name), - &format!("{}/{}", target_dir, name), - ) - .await; + if !change.is_empty() { + println!("The following files have been changed:"); + for file in change { + println!("\t{}", file.to_string_lossy()); } + println!(); + } - if metadata.is_file() { - #[allow(unused_must_use)] - let _ = fs::remove_file(&format!("{}/{}", target_dir, name)).await; - fs::symlink( - &format!("{}/{}", src_dir, name), - &format!("{}/{}", target_dir, name), - ) - .await - .unwrap(); + if !delete.is_empty() { + println!("The following files have been deleted:"); + for file in delete { + println!("\t{}", file.to_string_lossy()); } + println!(); } + } else { + link_tree(&context).await?; } + + Ok(()) +} + +pub struct Context { + tree_source_dir: PathBuf, + tree_build_dir: PathBuf, + tree_install_dir: PathBuf, + link_root_dir: PathBuf, } diff --git a/src/tree.rs b/src/tree.rs new file mode 100644 index 0000000..12568b2 --- /dev/null +++ b/src/tree.rs @@ -0,0 +1,44 @@ +use std::{ + collections::HashSet, + path::{Path, PathBuf}, +}; + +use async_recursion::async_recursion; +use futures::future::try_join_all; +use tokio::fs::read_dir; + +pub async fn get_tree_files(tree_root_path: &Path) -> std::io::Result> { + dir(tree_root_path, PathBuf::new()).await +} + +#[async_recursion] +async fn dir(tree_root_path: &Path, relative_path: PathBuf) -> std::io::Result> { + let current_path = tree_root_path.join(&relative_path); + + let mut dir_walker = read_dir(¤t_path).await?; + let mut dir_tasks = vec![]; + + let mut files = HashSet::new(); + + while let Some(entry) = dir_walker.next_entry().await? { + let metadata = entry.metadata().await?; + let os_name = entry.file_name(); + let name = os_name.to_string_lossy().to_string(); + if name == ".git" { + continue; + } + + if metadata.is_dir() { + dir_tasks.push(dir(tree_root_path, relative_path.join(name))); + } else if metadata.is_file() { + files.insert(relative_path.join(name)); + } + } + + let file_sets = try_join_all(dir_tasks).await?; + for file_set in file_sets { + files.extend(file_set); + } + + Ok(files) +}