dotfiles_rs/
lib.rs

1pub mod cli;
2pub mod path;
3pub mod readme_template;
4
5use git2::build::RepoBuilder;
6use git2::{Cred, FetchOptions, RemoteCallbacks, Repository};
7use log::info;
8use path::{file_to_path, home_path};
9use rayon::prelude::*;
10use readme_template::write_template_readme;
11use serde::{Deserialize, Serialize};
12use std::fs::{copy, create_dir_all, File};
13use std::path::{Path, PathBuf};
14use std::string::String;
15
16/// Dotfile validation schema
17#[derive(Debug, PartialEq, Serialize, Deserialize)]
18struct DotFiles {
19    // a list of dotfile file names
20    dotfiles: Vec<String>,
21}
22
23/// A function to copy file (essentially mkdir -p and cp)
24///
25/// # Args
26/// - `dest_file`: a file path pointing to the output file name
27/// - `orig_file`: the file path to the original file
28/// - `dry_run`: only print out what will be done
29///
30/// # Return
31/// - Result<u8, String>: return code for the mkdir/copy operation
32fn copy_file(dest_file: String, orig_file: String, dry_run: bool) -> Result<u8, String> {
33    //check whether the source file exists
34    let source_file_pathbuf: PathBuf = file_to_path(&orig_file, true)?;
35    let dest_file_pathbuf: PathBuf = file_to_path(&dest_file, false)?;
36    // mkdir with parents
37    let prefix: &Option<&Path> = &dest_file_pathbuf.parent();
38    if let Some(prefix_path) = prefix {
39        create_dir_all(prefix_path).map_err(|e| e.to_string())?
40    }
41
42    // copy over the file
43    let mut label: &str = "Dry run";
44    if !dry_run {
45        copy(source_file_pathbuf, dest_file_pathbuf).map_err(|e| e.to_string())?;
46        label = "Shell";
47    }
48    info!("[{}] Copied {} to {}", label, orig_file, dest_file,);
49    Ok(0)
50}
51
52/// Saving the provided list of dotfiles to the provided output folder
53///
54/// # Arguments
55/// - `dotfile_list`: a list of filenames relative to home directory
56/// - `destination_dir`: the folder to store the copies
57///
58/// # Return
59/// - Result<Vec<u8>, String>: return code for each copy
60pub fn save(
61    dotfile_list: Vec<String>,
62    destination_dir: String,
63    dry_run: bool,
64) -> Result<Vec<u8>, String> {
65    let home_dir: String = home_path()?;
66    // writing a readme for the new dotfile repo
67
68    if !dry_run {
69        write_template_readme(format!("{}/README.md", &destination_dir))?;
70    }
71
72    // copy over the dotfiles
73    dotfile_list
74        .into_par_iter()
75        .map(|dotfile| {
76            let orig_file: String = format!("{}/{}", home_dir, dotfile);
77            let dest_file: String = format!("{}/{}", destination_dir, dotfile);
78            copy_file(dest_file, orig_file, dry_run)
79        })
80        .collect()
81}
82
83/// Apply the dotfiles from a given directory
84///
85/// # Args
86/// - `dotfile_list`: a list of dot files to be installed from the dotfiles directory
87/// - `dotfiles_dir`: a give directory storing the dotfiles
88/// - `dry_run`: if true, only print, not do copy
89pub fn apply(
90    dotfile_list: Vec<String>,
91    dotfiles_dir: String,
92    dry_run: bool,
93) -> Result<Vec<u8>, String> {
94    info!("Applying dotfiles from: {}", dotfiles_dir);
95    let home_dir: String = home_path()?;
96    // copy over the files to the desinated folders
97    // from the cloned repo
98    dotfile_list
99        .into_par_iter()
100        .map(|dotfile| {
101            let orig_file: String = format!("{}/{}", dotfiles_dir, dotfile);
102            let dest_file: String = format!("{}/{}", home_dir, dotfile);
103            copy_file(dest_file, orig_file, dry_run)
104        })
105        .collect()
106}
107
108/// Installing the dotfiles from a github repo
109///
110/// # Args
111/// - `dotfile_list`: a list of dot files to be installed from the github repo
112/// - `github_url`: a valid github url for the repo (e.g. git@github.com:wckdouglas/dotfiles.git, must starts with git@github.com)
113/// - `ssh_key_file`: a ssh key file for github authentication (e.g. ~/.ssh/id_rsa)
114/// - `dry_run`: if true, only print, not do copy
115pub fn install(
116    dotfile_list: Vec<String>,
117    github_url: String,
118    ssh_key_file: String,
119    dry_run: bool,
120) -> Result<Vec<u8>, String> {
121    let home_dir: String = home_path()?;
122
123    // define where to clone the dotfile repo
124    let git_dotfiles_dir: String = format!("{}/dotfiles", &home_dir);
125    let git_dotfiles_path: &Path = Path::new(&git_dotfiles_dir);
126
127    // cloning the repo
128    let _repo = match git_dotfiles_path.exists() {
129        true => Repository::open(git_dotfiles_path)
130            .map_err(|_| format!("Folder not exists: {}", git_dotfiles_dir)),
131        _ => {
132            let repo: Result<Repository, String> =
133                git_clone(github_url, &git_dotfiles_dir, ssh_key_file);
134            info!("Clone complete");
135            repo
136        }
137    }?;
138    // applying dotfiles
139    apply(dotfile_list, git_dotfiles_dir, dry_run)
140}
141
142/// Cloning a github repo with a given ssh key file
143///
144/// # Args
145/// - `github_url`: a git repo url from github starting with git@github.com
146/// - `git_dorfiles_dir`: a directory name for cloning the repo locally
147/// - `ssh_private_key_fn`: ~/.ssh/id_rsa file, ~/.ssh/id_ecds (the ~/.ssh/id_ecds.pub should also exists!)
148fn git_clone(
149    github_url: String,
150    git_dotfiles_dir: &String,
151    ssh_private_key_fn: String,
152) -> Result<Repository, String> {
153    match github_url.starts_with("git@github.com") {
154        true => {
155            info!("Cloning {} into {}", github_url, git_dotfiles_dir);
156            let git_dotfiles_path: &Path = Path::new(&git_dotfiles_dir);
157            // make them to pathbuf
158            let ssh_pub_key_fn: String = format!("{}.pub", &ssh_private_key_fn);
159            let ssh_pub_key_file_path: PathBuf = file_to_path(&ssh_pub_key_fn, true)?;
160            let ssh_private_key_file_path: PathBuf = file_to_path(&ssh_private_key_fn, true)?;
161
162            // cloning the repo
163            let mut builder: RepoBuilder = RepoBuilder::new();
164            let mut callbacks: RemoteCallbacks = RemoteCallbacks::new();
165            let mut fetch_options: FetchOptions = FetchOptions::new();
166
167            callbacks.credentials(|_, _, _| {
168                let credentials: Cred = Cred::ssh_key(
169                    "git",
170                    Some(&ssh_pub_key_file_path),
171                    &ssh_private_key_file_path,
172                    None,
173                ).expect("Credential problem");
174                Ok(credentials)
175            });
176
177            fetch_options.remote_callbacks(callbacks);
178
179            builder.fetch_options(fetch_options);
180
181            builder
182                .clone(&github_url, git_dotfiles_path)
183                .map_err(|e| e.to_string())
184        },
185        _ => Err(String::from("We only support ssh-key cloning, which the github url should start with git@github.com prefix"))
186    }
187}
188
189/// reading the dotfile yaml
190///
191/// # Arguments
192/// - *yaml_fn*: the input yaml file path to be read
193///
194/// # Return
195/// - list of dotfiles to be copied
196///
197/// # Example
198///
199/// ```
200/// use dotfiles_rs::read_yaml;
201///
202/// let dotfile_list = read_yaml("data/dotfiles.yaml").unwrap();
203/// assert_eq!(dotfile_list.len(), 9);
204/// ```
205pub fn read_yaml(yaml_fn: &str) -> Result<Vec<String>, String> {
206    let f: File = File::open(yaml_fn).map_err(|e| e.to_string())?;
207    let data: DotFiles = serde_yaml::from_reader(f).map_err(|e| e.to_string())?;
208    Ok(data.dotfiles)
209}