parent
9e267dc6f5
commit
41ac587c34
@ -1,16 +1,14 @@ |
||||
[package] |
||||
name = "dfm" |
||||
version = "0.1.0" |
||||
edition = "2018" |
||||
edition = "2021" |
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[profile.release] |
||||
lto = true |
||||
codegen-units = 1 |
||||
|
||||
[dependencies] |
||||
async-recursion = "0.3" |
||||
clap = "2.33" |
||||
clap = "3.0.0-beta.5" |
||||
directories = "4.0" |
||||
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" |
||||
|
@ -0,0 +1 @@ |
||||
hard_tabs = true |
@ -1,71 +1,30 @@ |
||||
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::<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(()) |
||||
use crate::{ |
||||
config::Config, |
||||
utils::{get_tree_files, remove_dir_if_empty}, |
||||
}; |
||||
|
||||
pub async fn build(config: &Config) -> std::io::Result<()> { |
||||
let source_files = get_tree_files(config, &config.source_dir).await?; |
||||
let build_files = get_tree_files(config, &config.build_dir).await?; |
||||
|
||||
for file_path in source_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?; |
||||
} |
||||
|
||||
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?; |
||||
remove_dir_if_empty(file.parent().unwrap()).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 check; |
||||
mod link; |
||||
mod tree; |
||||
mod config; |
||||
mod diff; |
||||
mod install; |
||||
mod opts; |
||||
mod utils; |
||||
|
||||
use std::path::PathBuf; |
||||
|
||||
use build::build_tree; |
||||
use check::check_tree; |
||||
use link::link_tree; |
||||
use clap::Parser; |
||||
use directories::{ProjectDirs, UserDirs}; |
||||
|
||||
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] |
||||
async fn main() -> std::io::Result<()> { |
||||
let app = clap::App::new("DotFiles Manager") |
||||
.version(APP_VERSION) |
||||
.author("Rasmus Rosengren <rasmus.rosengren@protonmail.com>") |
||||
.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"); |
||||
|
||||
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 !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, |
||||
let opts = Opts::parse(); |
||||
|
||||
let user_dirs = UserDirs::new().expect("Could not find user directories"); |
||||
let project_dirs = |
||||
ProjectDirs::from("se", "rsrp", "dfm").expect("Could not find project directories"); |
||||
|
||||
let repo_path = if opts.repo_path.is_relative() { |
||||
let mut repo_path = PathBuf::from(user_dirs.home_dir()); |
||||
repo_path.push(opts.repo_path); |
||||
repo_path |
||||
} else { |
||||
opts.repo_path |
||||
}; |
||||
|
||||
let config = Config { |
||||
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"), |
||||
link_dir: user_dirs.home_dir().to_path_buf(), |
||||
ignored_dirs: vec![".git".to_string()], |
||||
}; |
||||
|
||||
remove_dir_if_empty(&PathBuf::from("/home/rosen/test")).await?; |
||||
build(&config).await?; |
||||
if opts.install { |
||||
install(&config).await?; |
||||
} else { |
||||
diff(&config, opts.diff_command).await?; |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
@ -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