parent
9e267dc6f5
commit
41ac587c34
@ -1,16 +1,14 @@ |
|||||||
[package] |
[package] |
||||||
name = "dfm" |
name = "dfm" |
||||||
version = "0.1.0" |
version = "0.1.0" |
||||||
edition = "2018" |
edition = "2021" |
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||||
|
|
||||||
[profile.release] |
|
||||||
lto = true |
|
||||||
codegen-units = 1 |
|
||||||
|
|
||||||
[dependencies] |
[dependencies] |
||||||
async-recursion = "0.3" |
async-recursion = "0.3" |
||||||
clap = "2.33" |
clap = "3.0.0-beta.5" |
||||||
|
directories = "4.0" |
||||||
futures = "0.3" |
futures = "0.3" |
||||||
tokio = { version = "1.10", features = ["fs", "macros", "rt-multi-thread"] } |
terminal_size = "0.1" |
||||||
|
tokio = { version = "1.10", features = ["fs", "macros", "process", "rt-multi-thread"] } |
||||||
xdg = "2.2" |
xdg = "2.2" |
||||||
|
@ -0,0 +1 @@ |
|||||||
|
hard_tabs = true |
@ -1,71 +1,30 @@ |
|||||||
use std::path::PathBuf; |
use crate::{ |
||||||
|
config::Config, |
||||||
use async_recursion::async_recursion; |
utils::{get_tree_files, remove_dir_if_empty}, |
||||||
use futures::future::join_all; |
}; |
||||||
use tokio::fs::{copy, create_dir, read_dir}; |
|
||||||
|
pub async fn build(config: &Config) -> std::io::Result<()> { |
||||||
use crate::Context; |
let source_files = get_tree_files(config, &config.source_dir).await?; |
||||||
|
let build_files = get_tree_files(config, &config.build_dir).await?; |
||||||
pub async fn build_tree(context: &Context) -> std::io::Result<()> { |
|
||||||
dir(context, PathBuf::new()).await |
for file_path in source_files.iter() { |
||||||
} |
if let Some(folder_path) = file_path.parent() { |
||||||
|
let dir = config.build_dir.join(folder_path); |
||||||
#[async_recursion] |
tokio::fs::create_dir_all(dir).await?; |
||||||
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); |
let from = config.source_dir.join(file_path); |
||||||
|
let to = config.build_dir.join(file_path); |
||||||
match create_dir(build_path).await { |
tokio::fs::copy(from, to).await?; |
||||||
Err(e) if e.kind() != std::io::ErrorKind::AlreadyExists => { |
} |
||||||
return Err(e); |
|
||||||
} |
for file_path in build_files { |
||||||
_ => (), |
if !source_files.contains(&file_path) { |
||||||
} |
let file = config.build_dir.join(file_path); |
||||||
|
tokio::fs::remove_file(file.clone()).await?; |
||||||
let mut dir_walker = read_dir(source_path).await?; |
remove_dir_if_empty(file.parent().unwrap()).await?; |
||||||
|
} |
||||||
let mut dir_tasks = vec![]; |
} |
||||||
let mut file_tasks = vec![]; |
|
||||||
|
Ok(()) |
||||||
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::<std::io::Result<Vec<_>>>() |
|
||||||
}; |
|
||||||
let file_tasks = async { |
|
||||||
join_all(file_tasks) |
|
||||||
.await |
|
||||||
.into_iter() |
|
||||||
.collect::<std::io::Result<Vec<_>>>() |
|
||||||
}; |
|
||||||
|
|
||||||
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(()) |
|
||||||
} |
} |
||||||
|
@ -1,36 +0,0 @@ |
|||||||
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<PathBuf>, Vec<PathBuf>, Vec<PathBuf>)> { |
|
||||||
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)) |
|
||||||
} |
|
@ -0,0 +1,10 @@ |
|||||||
|
use std::path::PathBuf; |
||||||
|
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct Config { |
||||||
|
pub source_dir: PathBuf, |
||||||
|
pub build_dir: PathBuf, |
||||||
|
pub install_dir: PathBuf, |
||||||
|
pub link_dir: PathBuf, |
||||||
|
pub ignored_dirs: Vec<String>, |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
use std::{io::Write, path::PathBuf}; |
||||||
|
|
||||||
|
use terminal_size::{terminal_size, Width}; |
||||||
|
|
||||||
|
use crate::{config::Config, utils::get_tree_files}; |
||||||
|
|
||||||
|
pub async fn diff(config: &Config, diff_command: String) -> std::io::Result<()> { |
||||||
|
let built_files = get_tree_files(config, &config.build_dir).await?; |
||||||
|
let installed_files = get_tree_files(config, &config.install_dir).await?; |
||||||
|
|
||||||
|
let mut all_files = built_files.clone(); |
||||||
|
all_files.extend(installed_files.clone()); |
||||||
|
let mut all_files: Vec<_> = all_files.iter().collect(); |
||||||
|
all_files.sort(); |
||||||
|
|
||||||
|
let (Width(terminal_width), _) = terminal_size().expect("Could not get terminal size"); |
||||||
|
for file in all_files { |
||||||
|
let is_added = built_files.get(file).is_some() && installed_files.get(file).is_none(); |
||||||
|
let is_removed = built_files.get(file).is_none() && installed_files.get(file).is_some(); |
||||||
|
|
||||||
|
let (workdir, first_path, second_path) = if is_added { |
||||||
|
( |
||||||
|
config.build_dir.clone(), |
||||||
|
file.clone(), |
||||||
|
PathBuf::from("/dev/null"), |
||||||
|
) |
||||||
|
} else if is_removed { |
||||||
|
( |
||||||
|
config.install_dir.clone(), |
||||||
|
PathBuf::from("/dev/null"), |
||||||
|
file.clone(), |
||||||
|
) |
||||||
|
} else { |
||||||
|
( |
||||||
|
config.build_dir.clone(), |
||||||
|
file.clone(), |
||||||
|
config.install_dir.join(file), |
||||||
|
) |
||||||
|
}; |
||||||
|
|
||||||
|
let output = tokio::process::Command::new(&diff_command) |
||||||
|
.current_dir(workdir) |
||||||
|
.arg(second_path) |
||||||
|
.arg(first_path) |
||||||
|
.arg("-w") |
||||||
|
.arg(terminal_width.to_string()) |
||||||
|
.output() |
||||||
|
.await?; |
||||||
|
std::io::stdout().write_all(&output.stdout)?; |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
use crate::{ |
||||||
|
config::Config, |
||||||
|
utils::{get_tree_files, remove_dir_if_empty}, |
||||||
|
}; |
||||||
|
|
||||||
|
pub async fn install(config: &Config) -> std::io::Result<()> { |
||||||
|
let built_files = get_tree_files(config, &config.build_dir).await?; |
||||||
|
let installed_files = get_tree_files(config, &config.install_dir).await?; |
||||||
|
|
||||||
|
for file_path in built_files.iter() { |
||||||
|
if let Some(folder_path) = file_path.parent() { |
||||||
|
let dir = config.build_dir.join(folder_path); |
||||||
|
tokio::fs::create_dir_all(dir).await?; |
||||||
|
} |
||||||
|
|
||||||
|
let from = config.source_dir.join(file_path); |
||||||
|
let to = config.build_dir.join(file_path); |
||||||
|
tokio::fs::copy(from, to).await?; |
||||||
|
|
||||||
|
if let Some(folder_path) = file_path.parent() { |
||||||
|
let dir = config.link_dir.join(folder_path); |
||||||
|
tokio::fs::create_dir_all(dir).await?; |
||||||
|
} |
||||||
|
|
||||||
|
tokio::fs::symlink( |
||||||
|
config.install_dir.join(&file_path), |
||||||
|
config.link_dir.join(&file_path), |
||||||
|
) |
||||||
|
.await?; |
||||||
|
} |
||||||
|
|
||||||
|
for file_path in installed_files { |
||||||
|
if !built_files.contains(&file_path) { |
||||||
|
let installed_file = config.install_dir.join(&file_path); |
||||||
|
let linked_file = config.link_dir.join(&file_path); |
||||||
|
tokio::fs::remove_file(linked_file.clone()).await?; |
||||||
|
remove_dir_if_empty(linked_file.parent().unwrap()).await?; |
||||||
|
tokio::fs::remove_file(installed_file.clone()).await?; |
||||||
|
remove_dir_if_empty(installed_file.parent().unwrap()).await?; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
@ -1,52 +0,0 @@ |
|||||||
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(()) |
|
||||||
} |
|
@ -1,88 +1,51 @@ |
|||||||
mod build; |
mod build; |
||||||
mod check; |
mod config; |
||||||
mod link; |
mod diff; |
||||||
mod tree; |
mod install; |
||||||
|
mod opts; |
||||||
|
mod utils; |
||||||
|
|
||||||
use std::path::PathBuf; |
use std::path::PathBuf; |
||||||
|
|
||||||
use build::build_tree; |
use clap::Parser; |
||||||
use check::check_tree; |
use directories::{ProjectDirs, UserDirs}; |
||||||
use link::link_tree; |
|
||||||
|
|
||||||
const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); |
use crate::{ |
||||||
|
build::build, config::Config, diff::diff, install::install, opts::Opts, |
||||||
|
utils::remove_dir_if_empty, |
||||||
|
}; |
||||||
|
|
||||||
#[tokio::main] |
#[tokio::main] |
||||||
async fn main() -> std::io::Result<()> { |
async fn main() -> std::io::Result<()> { |
||||||
let app = clap::App::new("DotFiles Manager") |
let opts = Opts::parse(); |
||||||
.version(APP_VERSION) |
|
||||||
.author("Rasmus Rosengren <rasmus.rosengren@protonmail.com>") |
let user_dirs = UserDirs::new().expect("Could not find user directories"); |
||||||
.about("Utility to manage dotfiles") |
let project_dirs = |
||||||
.arg( |
ProjectDirs::from("se", "rsrp", "dfm").expect("Could not find project directories"); |
||||||
clap::Arg::with_name("dry-run") |
|
||||||
.long("dry-run") |
let repo_path = if opts.repo_path.is_relative() { |
||||||
.short("d") |
let mut repo_path = PathBuf::from(user_dirs.home_dir()); |
||||||
.help("Report changes without installing"), |
repo_path.push(opts.repo_path); |
||||||
) |
repo_path |
||||||
.arg( |
} else { |
||||||
clap::Arg::with_name("repo-path") |
opts.repo_path |
||||||
.long("repo-path") |
}; |
||||||
.short("r") |
|
||||||
.default_value(".df") |
let config = Config { |
||||||
.help("Location of repo, relative to $HOME"), |
source_dir: repo_path, |
||||||
); |
build_dir: project_dirs.cache_dir().to_path_buf().join("tree"), |
||||||
|
install_dir: project_dirs.config_dir().to_path_buf().join("tree"), |
||||||
let matches = app.get_matches(); |
link_dir: user_dirs.home_dir().to_path_buf(), |
||||||
|
ignored_dirs: vec![".git".to_string()], |
||||||
let home_dir = std::env::var("HOME").expect("$HOME variable not found"); |
}; |
||||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("dfm").expect("xdg dirs"); |
|
||||||
|
remove_dir_if_empty(&PathBuf::from("/home/rosen/test")).await?; |
||||||
let context = Context { |
build(&config).await?; |
||||||
tree_source_dir: format!("{}/{}", home_dir, matches.value_of("repo-path").unwrap()).into(), |
if opts.install { |
||||||
tree_build_dir: xdg_dirs |
install(&config).await?; |
||||||
.create_cache_directory("tree") |
} else { |
||||||
.expect("xdg cache dir"), |
diff(&config, opts.diff_command).await?; |
||||||
tree_install_dir: xdg_dirs |
} |
||||||
.create_config_directory("tree") |
|
||||||
.expect("xdg config dir"), |
Ok(()) |
||||||
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 !change.is_empty() { |
|
||||||
println!("The following files have been changed:"); |
|
||||||
for file in change { |
|
||||||
println!("\t{}", file.to_string_lossy()); |
|
||||||
} |
|
||||||
println!(); |
|
||||||
} |
|
||||||
|
|
||||||
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, |
|
||||||
} |
} |
||||||
|
@ -0,0 +1,22 @@ |
|||||||
|
use std::path::PathBuf; |
||||||
|
|
||||||
|
use clap::Parser; |
||||||
|
|
||||||
|
use crate::utils::APP_VERSION; |
||||||
|
|
||||||
|
#[derive(Debug, Parser)] |
||||||
|
#[clap(
|
||||||
|
version = APP_VERSION, |
||||||
|
author = "Rasmus Rosengren <rasmus.rosengren@protonmail.com>", |
||||||
|
about = "Utility to manage dotfiles" |
||||||
|
)] |
||||||
|
pub struct Opts { |
||||||
|
#[clap(short, long)] |
||||||
|
pub install: bool, |
||||||
|
|
||||||
|
#[clap(short, long, default_value = ".df")] |
||||||
|
pub repo_path: PathBuf, |
||||||
|
|
||||||
|
#[clap(short, long, default_value = "delta")] |
||||||
|
pub diff_command: String, |
||||||
|
} |
@ -1,44 +0,0 @@ |
|||||||
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<HashSet<PathBuf>> { |
|
||||||
dir(tree_root_path, PathBuf::new()).await |
|
||||||
} |
|
||||||
|
|
||||||
#[async_recursion] |
|
||||||
async fn dir(tree_root_path: &Path, relative_path: PathBuf) -> std::io::Result<HashSet<PathBuf>> { |
|
||||||
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) |
|
||||||
} |
|
@ -0,0 +1,68 @@ |
|||||||
|
use std::{ |
||||||
|
collections::HashSet, |
||||||
|
path::{Path, PathBuf}, |
||||||
|
}; |
||||||
|
|
||||||
|
use futures::future::try_join_all; |
||||||
|
use tokio::fs::read_dir; |
||||||
|
|
||||||
|
use crate::config::Config; |
||||||
|
|
||||||
|
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); |
||||||
|
|
||||||
|
pub async fn get_tree_files( |
||||||
|
config: &Config, |
||||||
|
tree_root: &Path, |
||||||
|
) -> std::io::Result<HashSet<PathBuf>> { |
||||||
|
get_tree_files_recursively(config, tree_root, PathBuf::new()).await |
||||||
|
} |
||||||
|
|
||||||
|
#[async_recursion::async_recursion] |
||||||
|
async fn get_tree_files_recursively( |
||||||
|
config: &Config, |
||||||
|
tree_root: &Path, |
||||||
|
relative_path: PathBuf, |
||||||
|
) -> std::io::Result<HashSet<PathBuf>> { |
||||||
|
let current_path = tree_root.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 config.ignored_dirs.contains(&name) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if metadata.is_dir() { |
||||||
|
dir_tasks.push(get_tree_files_recursively( |
||||||
|
config, |
||||||
|
tree_root, |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn remove_dir_if_empty(path: &Path) -> std::io::Result<()> { |
||||||
|
let mut dir_walker = tokio::fs::read_dir(path).await?; |
||||||
|
|
||||||
|
if dir_walker.next_entry().await?.is_none() { |
||||||
|
tokio::fs::remove_dir(path).await?; |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
Loading…
Reference in new issue