1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
//! `badm` is a tool that stores your configuration files, or
//! [dotfiles](https://en.wikipedia.org/wiki/Hidden_file_and_hidden_directory), in a directory that replicates the directory hierarchy of the
//! dotfiles' original path, and creates symlinks to their original paths. This creates a
//! standardized and systematic approach for managing, deploying, and sharing dotfiles
//! among different systems and users.
//!
//! badm is ultimately "But Another Dotfiles Manager".
//!
//! # Examples
//!
//! - ferris has created a directory to store their dotfiles at `~/.dots`
//! - `badm set-dir ~/.dots` sets the BADM dotfiles dir at `~/.dots`
//! - badm will search for a badm config file at one of the two valid locations: `$HOME`
//!   and `$XDG_CONFIG_HOME`. If the config file not found, badm will create it under
//!   `$HOME`
//!
//! <pre>
//! /home
//! └── ferris
//!     └── .dots
//!         ├── .badm.toml
//!         └── .gitconfig
//! </pre>
//!
//!
//! - to store `~/.gitconfig` as a dotfile, ferris runs `badm stow ~/.gitconfig`
//!   _(relative paths work as well)_
//! - badm replicates the path of the dotfile under the `~/.dots` directory
//! - the dotfile is moved to this new path in the set dotfiles directory and symlinked at
//!   its original path which points to its new path
//!
//! <pre>
//! /home
//! └── ferris
//!     ├── .badm.toml
//!     ├── .dots
//!     │   └── home
//!     │       └── ferris
//!     │           └── .gitconfig
//!     └── .gitconfig -> /home/ferris/.dots/home/ferris/.gitconfig
//! </pre>
//!
//! # Commands
//!
//! - `badm set-dir <DIRECTORY>` - set dotfiles directory location, if the location is not
//!   created BADM has the ability to create one for you
//! - `badm stow <FILE>` - store a file in the dotfiles directory, create a symlink at the
//!   original source of the stowed file.
//! - `badm deploy <FILE>` - for new configurations, create symlinks in directories
//!   relative to the dotfile's directory hierarchy. Directories to replicate the stored
//!   dotfile's directory structure will be created if not found.
//! - `badm restore <FILE>` - restore the stored file from the dotfiles directory and
//!   replace the symlink with the original file

#![cfg_attr(test, deny(warnings))]
#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![allow(clippy::must_use_candidate)]
#![deny(
    future_incompatible,
    missing_debug_implementations,
    missing_docs,
    missing_copy_implementations,
    missing_docs,
    nonstandard_style,
    trivial_casts,
    trivial_numeric_casts,
    unsafe_code,
    unused_extern_crates,
    unused_import_braces,
    unused_qualifications,
    unused_results,
    unused_qualifications
)]

pub mod commands;
pub(crate) mod config;
mod errors;
pub mod paths;

pub use crate::config::Config;
pub use crate::errors::InputError;

#[macro_use] extern crate failure;

use std::fs::{self, File};
use std::io::{self, prelude::*, BufWriter};
use std::path::{Path, PathBuf};

// TODO: create dotfile struct

/// Struct used to traverse directories and collect entries located within.
#[derive(Debug)]
pub struct DirScanner {
    entries: Vec<PathBuf>,
    recursive: bool,
}

impl DirScanner {
    /// Given a directory, traverse path and get entries located within `dir`.
    /// If the [`DirScanner::recursive`] method is not called before `get_entries`, it
    /// will only traverse one level below.
    ///
    /// [`DirScanner::recursive`]: struct.DirScanner.html/#method.recursive
    pub fn get_entries(mut self, dir: &Path) -> io::Result<Vec<PathBuf>> {
        self.collect_entries(dir)?;

        self.entries = self
            .entries
            .into_iter()
            .map(|path| {
                if path.is_relative() {
                    fs::canonicalize(path)
                } else {
                    Ok(path)
                }
            })
            .filter_map(Result::ok)
            .collect();

        Ok(self.entries)
    }

    /// Builder method to set recursive flag to `true` when scanning directory.
    pub fn recursive(mut self) -> Self {
        self.recursive = true;
        self
    }

    fn collect_entries(&mut self, dir: &Path) -> io::Result<()> {
        if dir.is_dir() {
            for entry in fs::read_dir(dir)? {
                let path = entry.map(|e| e.path())?;

                if path.is_dir() && self.recursive {
                    if !path.ends_with(".git") {
                        self.collect_entries(&path)?;
                    }
                } else {
                    self.entries.push(path)
                }
            }
        }
        Ok(())
    }
}

impl Default for DirScanner {
    fn default() -> Self {
        Self {
            entries: vec![],
            recursive: false,
        }
    }
}

#[derive(Copy, Clone, Debug)]
/// Moves, stores, and creates files and symlinks.
pub struct FileHandler;

impl FileHandler {
    /// Store a file in the dotfiles directory, create a symlink at the original
    /// source of the stowed file.
    pub fn store_file(src: &Path, dst: &Path) -> io::Result<()> {
        Self::move_file(src, dst)?;
        Self::create_symlink(dst, src)
    }

    /// Read file at path src and write to created/truncated file at path dst.
    pub fn move_file(src: &Path, dst: &Path) -> io::Result<()> {
        // read file path to String
        let contents = crate::paths::read_path(src)?;

        // write String contents to dst file
        let dst_file = File::create(dst)?;
        let mut writer = BufWriter::new(dst_file);
        writer.write_all(contents.as_bytes())?;

        // remove file at src location
        fs::remove_file(&src)
    }

    /// Create a symlink at "dst" pointing to "src."
    ///
    /// For Unix platforms, [`std::os::unix::fs::symlink`] is used to create
    /// symlinks. For Windows, [`std::os::windows::fs::symlink_file`] is used.
    ///
    /// [`std::os::unix::fs::symlink`]: std/os/unix/fs/fn.symlink.html
    /// [`std::os::windows::fs::symlink_file`]: std/os/windows/fs/fn.symlink_file.html
    pub fn create_symlink(src: &Path, dst: &Path) -> io::Result<()> {
        #[cfg(not(target_os = "windows"))] use std::os::unix::fs::symlink;

        #[cfg(target_os = "windows")]
        use std::os::windows::fs::symlink_file as symlink;
        symlink(src, dst)
    }
}