Browse Source

Improve CLI

+ Add repository URL as valid input for dotfiles
+ Add regex, chrono crates
+ Add custom error types for Kot
+ Add uninstallation of dotfiles to revert changes when error is
  encountered
+ Update README, help text
master
Shaun Reed 4 months ago
parent
commit
a01ab6b532
  1. 2
      .gitignore
  2. 3
      .gitmodules
  3. 147
      Cargo.lock
  4. 4
      Cargo.toml
  5. 203
      README.md
  6. 1
      dotfiles/dot
  7. 2
      dry-runs/kapper/.config/README.md
  8. 1
      dry-runs/kapper/README.md
  9. 290
      src/kot.rs
  10. 236
      src/kot/kcli.rs
  11. 78
      src/kot/kerror.rs
  12. 189
      src/kot/kfs.rs
  13. 47
      src/kot/kgit.rs
  14. 23
      src/kot/kio.rs
  15. 27
      src/main.rs

2
.gitignore vendored

@ -1,2 +1,4 @@ @@ -1,2 +1,4 @@
**/.idea/**
/target
dotfiles/**
dry-runs/**

3
.gitmodules vendored

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
[submodule "dotfiles/dot"]
path = dotfiles/dot
url = https://gitlab.com/shaunrd0/dot

147
Cargo.lock generated

@ -2,11 +2,20 @@ @@ -2,11 +2,20 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
@ -22,17 +31,36 @@ dependencies = [ @@ -22,17 +31,36 @@ dependencies = [
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"winapi",
]
[[package]]
name = "clap"
version = "2.33.3"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"ansi_term",
"atty",
@ -69,9 +97,11 @@ dependencies = [ @@ -69,9 +97,11 @@ dependencies = [
[[package]]
name = "kot"
version = "0.1.0"
version = "0.1.5"
dependencies = [
"chrono",
"fs_extra",
"regex",
"structopt",
]
@ -83,9 +113,34 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" @@ -83,9 +113,34 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.102"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "proc-macro-error"
@ -113,22 +168,39 @@ dependencies = [ @@ -113,22 +168,39 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.29"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
dependencies = [
"unicode-xid",
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.9"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
[[package]]
name = "strsim"
version = "0.8.0"
@ -137,9 +209,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" @@ -137,9 +209,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "structopt"
version = "0.3.23"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa"
checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
dependencies = [
"clap",
"lazy_static",
@ -148,9 +220,9 @@ dependencies = [ @@ -148,9 +220,9 @@ dependencies = [
[[package]]
name = "structopt-derive"
version = "0.4.16"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba"
checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
dependencies = [
"heck",
"proc-macro-error",
@ -161,13 +233,13 @@ dependencies = [ @@ -161,13 +233,13 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.77"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0"
checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
"unicode-ident",
]
[[package]]
@ -179,11 +251,28 @@ dependencies = [ @@ -179,11 +251,28 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]]
name = "unicode-ident"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-width"
@ -191,12 +280,6 @@ version = "0.1.9" @@ -191,12 +280,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "vec_map"
version = "0.8.2"
@ -205,9 +288,15 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" @@ -205,9 +288,15 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "winapi"

4
Cargo.toml

@ -1,11 +1,13 @@ @@ -1,11 +1,13 @@
[package]
name = "kot"
version = "0.1.0"
version = "0.1.5"
edition = "2018"
# See more keys and their definitions at
# https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
regex = "1"
structopt = "0.3.23"
fs_extra = "1.2.0"
chrono = "0.4"

203
README.md

@ -6,50 +6,61 @@ This helps to protect against installing broken dotfiles by providing a way to r @@ -6,50 +6,61 @@ This helps to protect against installing broken dotfiles by providing a way to r
and return the system back to the previous state.
The installation process creates symbolic links, much like what you would expect when using [stow](https://linux.die.net/man/8/stow).
`kot` can install dotfiles from any directory, using any target directory. To test how `kot` might behave,
you could point `--install-dir` to any directory that you've created for testing.
This directory could be empty, or it could contain another set of dotfiles. `kot` will attempt
to install the configurations. If conflicts are detected, output shows the conflicts and
`kot` can install dotfiles from any source directory, to any target directory.
To test how `kot` might behave, you could point `--install` to any directory that you've created for testing.
This directory could be empty, or it could contain another set of dotfiles.
Alternatively, you could set the `--dry-run` flag that will automatically install to a predefined path (`$HOME/.local/share/kot/dry-runs/$USER`)
Note that this directory will never be cleared automatically, each subsequent `--dry-run`
will stack configurations into this default directory until it is manually cleared.
If conflicts are detected, `kot` shows the conflicts found and
prompts to abort or continue. An example of this is seen below.
This prompt will be skipped if the `--force` flag is set.
```bash
kot dotfiles/dot/
args: Cli { dotfiles_dir: "/home/kapper/Code/kot/dotfiles/dot", install_dir: "/home/kapper/Code/kot/dry-runs/kapper", backup_dir: "/home/kapper/Code/kot/backups/kapper", force: false }
kot --dry-run dotfiles/dot/
args: Cli { dotfiles: "/home/kapper/Code/kot/dotfiles/dot", install_dir: "/home/kapper/.local/share/kot/dry-runs/kapper", backup_dir: Some("/home/kapper/.local/share/kot/backups/dot:2022-05-29T19:03:27"), clone_dir: None, force: false, dry_run: true, is_repo: false, conflicts: [] }
The following configurations already exist:
"/home/kapper/Code/kot/dry-runs/kapper/.bashrc"
"/home/kapper/Code/kot/dry-runs/kapper/.config"
"/home/kapper/Code/kot/dry-runs/kapper/README.md"
"/home/kapper/Code/kot/dry-runs/kapper/VimScreenshot.png"
"/home/kapper/Code/kot/dry-runs/kapper/fix-vbox.sh"
"/home/kapper/Code/kot/dry-runs/kapper/.git"
"/home/kapper/Code/kot/dry-runs/kapper/.bash_aliases"
"/home/kapper/Code/kot/dry-runs/kapper/.gitignore"
"/home/kapper/Code/kot/dry-runs/kapper/.gitmodules"
"/home/kapper/Code/kot/dry-runs/kapper/.vimrc"
"/home/kapper/Code/kot/dry-runs/kapper/.vim"
If you continue, backups will be made in "/home/kapper/Code/kot/backups/kapper". Any configurations there will be overwritten.
Abort? Enter y/n or Y/N:
"/home/kapper/.local/share/kot/dry-runs/kapper/.git"
"/home/kapper/.local/share/kot/dry-runs/kapper/.vimrc"
"/home/kapper/.local/share/kot/dry-runs/kapper/.bash_aliases"
"/home/kapper/.local/share/kot/dry-runs/kapper/.vim"
"/home/kapper/.local/share/kot/dry-runs/kapper/VimScreenshot.png"
"/home/kapper/.local/share/kot/dry-runs/kapper/.gitignore"
"/home/kapper/.local/share/kot/dry-runs/kapper/.config"
"/home/kapper/.local/share/kot/dry-runs/kapper/fix-vbox.sh"
"/home/kapper/.local/share/kot/dry-runs/kapper/.gitmodules"
"/home/kapper/.local/share/kot/dry-runs/kapper/.bashrc"
"/home/kapper/.local/share/kot/dry-runs/kapper/README.md"
If you continue, backups will be made in "/home/kapper/.local/share/kot/backups/dot:2022-05-29T19:03:27".
Any configurations there will be overwritten.
Continue? Enter Y/y or N/n:
```
If there are already files within the backup directory, `kot` will exit and show an error message.
This is to protect existing backups from being merged with configs from subsequent runs.
If you want to erase these backups and create a new backup, rerun the command with the `--force` flag set.
Otherwise, specify a different backup directory with the `--backup-dir` option.
If the backup directory does not exist, it will be created.
#### User Data
`kot` stores user data within `$HOME/.local/share/kot/`
```bash
kot dotfiles/dot/
When we provide a repository URL as our `dotfiles` to install, the repo will be *recursively* cloned into
`$HOME/.local/share/kot/dotfiles/<REPO_NAME>`.
This is to ensure each user of `kot` maintains their own dotfiles in a location that is accessible but not easy to accidentally modify or erase.
If needed, the user can provide a preferred clone directory to the CLI by setting the `--clone-dir` option
thread 'main' panicked at '
Error: Backups already exist at "/home/kapper/Code/kot/backups/kapper"
Set the --force flag to overwrite configurations stored here', src/kot/kcli.rs:94:17
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
When we encounter conflicts during installation of these dotfiles, backups will be created in
`$HOME/.local/share/kot/backups/<DOTFILES_NAME>:<DATE(%Y-%m-%dT%H:%M:%S)>`
If there are no conflicts found during installation, no backup is created.
Configurations are said to be conflicting if the `--install` path contains configuration files that are
also within the dotfiles we are currently installing.
Backups are intended to reverse changes applied during installation of dotfiles.
These backups are not exhaustive of all configurations tied to the system or user.
The backups only include files that were direct conflicts with configurations being installed.
When we reach an error during installation, `kot` will restore the configurations within the last backup, and then removes unused configurations.
#### Installation
#### Installing kot
Follow [Rustup instructions](https://rustup.rs/) to setup the Rust toolchain
@ -60,65 +71,97 @@ git clone https://gitlab.com/shaunrd0/kot && cd kot @@ -60,65 +71,97 @@ git clone https://gitlab.com/shaunrd0/kot && cd kot
cargo install --path .
kot --help
kot 0.1.0
kot 0.1.5
CLI for managing Linux user configurations
USAGE:
kot [FLAGS] [OPTIONS] <dotfiles-dir>
kot [FLAGS] [OPTIONS] <dotfiles> --install <install>
FLAGS:
-f, --force Overwrites existing backups
-h, --help Prints help information
-V, --version Prints version information
-d, --dry-run
Installs configurations to $HOME/.local/shared/kot/dry-runs
Useful flag to set when testing what an install would do to your home directory. This is synonymous with
setting --install $HOME/.local/shared/kot/dry-runs/$USER. Subsequent runs with this flag set will not delete
the contents of this directory.
-f, --force
Overwrites existing backups
This flag will replace existing backups if during installation we encounter conflicts and the backup-dir
provided already contains previous backups.
-h, --help
Prints help information
-V, --version
Prints version information
OPTIONS:
-b, --backup-dir <backup-dir> The location to store backups for this user [default: backups/kapper]
-i, --install-dir <install-dir> The location to attempt installation of user configurations [default: dry-
runs/kapper]
-b, --backup-dir <backup-dir>
The location to store backups for this user
If no backup-dir is provided, we create one within the default kot data directory:
$HOME/.local/share/kot/backups/
-c, --clone-dir <clone-dir>
An alternate path to clone a dotfiles repository to
If the clone-dir option is provided to the CLI, kot will clone the dotfiles repository into this directory.
If clone-dir is not provided, the repository is cloned into $HOME/.local/share/kot/dotfiles Custom clone-dir
will be used literally, and no subdirectory is created to store the cloned repository For example, clone-dir
of $HOME/clonedir for repo named Dotfiles We will clone into $HOME/clonedir, and NOT $HOME/clonedir/Dotfiles
The default path for cloned repos is $HOME/.local/share/kot/dotfiles/
-i, --install <install>
The location to attempt installation of user configurations
The desired installation directory for user configurations. By default this is your $HOME directory This
could optionally point to some other directory to perform a dry run, or the --dry-run flag could be set
[env: HOME=/home/kapper]
ARGS:
<dotfiles-dir> Local or full path to user configurations to install
```
<dotfiles>
Local or full path to user configurations to install. Can also be a git repository.
#### Dotfiles Management
System path or repository URL for dotfiles we want to install. If a path is used, it can either be local to
CWD or absolute. If a URL is used for a dotfiles repository, the repo is cloned into
$HOME/.local/shared/kot/dotfiles/
```
To store dotfiles, this repository uses submodules. To update surface-level submodules, we can run the following commands
If you don't want to install `kot`, you can also use the following `cargo` command
where all arguments after the `--` are passed as arguments to `kot` and not `cargo`.
Below is an example of the short-help output text provided with the `-h` flag
```bash
git submodule update --init
cd path/to/kot
cargo build
cargo run -- --help
Submodule path 'dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843'
```
But in the case of my dotfiles repository, [shaunrd0/dot](https://gitlab.com/shaunrd0/dot), I use submodules to clone repositories for vim plugins. To update all submodules *and their nested submodules*, we can run the following commands
```bash
git submodule update --init --recursive
Submodule 'dotfiles/dot' (https://gitlab.com/shaunrd0/dot) registered for path 'dotfiles/dot'
Cloning into '/home/kapper/Code/kotd/dotfiles/dot'...
warning: redirecting to https://gitlab.com/shaunrd0/dot.git/
Submodule path 'dotfiles/dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843'
Submodule '.vim/bundle/Colorizer' (https://github.com/chrisbra/Colorizer) registered for path 'dotfiles/dot/.vim/bundle/Colorizer'
Submodule '.vim/bundle/ale' (https://github.com/dense-analysis/ale) registered for path 'dotfiles/dot/.vim/bundle/ale'
Submodule '.vim/bundle/clang_complete' (https://github.com/xavierd/clang_complete) registered for path 'dotfiles/dot/.vim/bundle/clang_complete'
Submodule '.vim/bundle/supertab' (https://github.com/ervandew/supertab) registered for path 'dotfiles/dot/.vim/bundle/supertab'
Submodule '.vim/bundle/unicode.vim' (https://github.com/chrisbra/unicode.vim) registered for path 'dotfiles/dot/.vim/bundle/unicode.vim'
Submodule '.vim/bundle/vim-airline' (https://github.com/vim-airline/vim-airline) registered for path 'dotfiles/dot/.vim/bundle/vim-airline'
Submodule '.vim/bundle/vim-airline-themes' (https://github.com/vim-airline/vim-airline-themes) registered for path 'dotfiles/dot/.vim/bundle/vim-airline-themes'
Submodule '.vim/bundle/vim-signify' (https://github.com/mhinz/vim-signify) registered for path 'dotfiles/dot/.vim/bundle/vim-signify'
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/Colorizer'...
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/ale'...
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/clang_complete'...
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/supertab'...
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/unicode.vim'...
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-airline'...
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-airline-themes'...
Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-signify'...
Submodule path 'dotfiles/dot/.vim/bundle/Colorizer': checked out '826d5691ac7d36589591314621047b1b9d89ed34'
Submodule path 'dotfiles/dot/.vim/bundle/ale': checked out '3ea887d2f4d43dd55d81213517344226f6399ed6'
Submodule path 'dotfiles/dot/.vim/bundle/clang_complete': checked out '293a1062274a06be61797612034bd8d87851406e'
Submodule path 'dotfiles/dot/.vim/bundle/supertab': checked out 'd80e8e2c1fa08607fa34c0ca5f1b66d8a906c5ef'
Submodule path 'dotfiles/dot/.vim/bundle/unicode.vim': checked out 'afb8db4f81580771c39967e89bc5772e72b9018e'
Submodule path 'dotfiles/dot/.vim/bundle/vim-airline': checked out 'cb1bc19064d3762e4e08103afb37a246b797d902'
Submodule path 'dotfiles/dot/.vim/bundle/vim-airline-themes': checked out 'd148d42d9caf331ff08b6cae683d5b210003cde7'
Submodule path 'dotfiles/dot/.vim/bundle/vim-signify': checked out 'b2a0450e23c63b75bbeabf4f0c28f9b4b2480689'
kot 0.1.5
CLI for managing Linux user configurations
USAGE:
kot [FLAGS] [OPTIONS] <dotfiles> --install <install>
FLAGS:
-d, --dry-run Installs configurations to $HOME/.local/shared/kot/dry-runs
-f, --force Overwrites existing backups
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-b, --backup-dir <backup-dir> The location to store backups for this user
-c, --clone-dir <clone-dir> An alternate path to clone a dotfiles repository to
-i, --install <install> The location to attempt installation of user configurations [env:
HOME=/home/kapper]
ARGS:
<dotfiles> Local or full path to user configurations to install. Can also be a git repository
```
#### TODO
* Ensure empty backups are not created
* Provide interface for managing agreed-upon /etc/skel/ configurations
* Provide more CLI options for git functionality; Branches, update submodules, etc
* Clean up warnings during build / installation
* Automate testing
*

1
dotfiles/dot

@ -1 +0,0 @@ @@ -1 +0,0 @@
Subproject commit 7877117d5bd413ecf35c86efb4514742d8136843

2
dry-runs/kapper/.config/README.md

@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
This is a test directory to test config collisions

1
dry-runs/kapper/README.md

@ -1 +0,0 @@ @@ -1 +0,0 @@
This directory is for testing the installation of user configurations against an existing configuration set.

290
src/kot.rs

@ -6,15 +6,31 @@ @@ -6,15 +6,31 @@
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################*/
use std::collections::HashMap;
use std::path::PathBuf;
use crate::kot::kfs::check_collisions;
pub mod kcli;
pub mod kfs;
pub mod kio;
pub mod kgit;
pub mod kerror;
use kerror::Error;
/// Result alias to return result with Error of various types
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub type Result<T> = std::result::Result<T, kerror::Error>;
macro_rules! err {
($type:expr, $msg:expr) => {
return Err(Error::new($type, $msg))
};
($msg:expr) => {
return Err(Error::new(ErrorKind::Other("Unclassified kot error"), $msg))
};
}
pub (crate) use err;
use crate::ErrorKind::Other;
use crate::kot::kfs::get_target_paths;
// =============================================================================
// IMPLEMENTATION
@ -22,111 +38,199 @@ pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; @@ -22,111 +38,199 @@ pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
// -----------------------------------------------------------------------------
/// Creates symbolic links to the configurations we're installing
// TODO: On error, revert to last good state
// TODO: User script to execute after installing configs successfully
pub fn install_configs(args: & kcli::Cli) -> Result<()> {
// Get the configurations and their target installation paths
// + Checks for conflicts and prompts user to abort or continue
let config_map = kfs::get_target_paths(&args)?;
// Check if there are any existing files in the install directory that are also within the dotfiles to install
handle_collisions(&args, &config_map)?;
// At this point there are either no conflicts or the user agreed to them
println!("Installing configs:");
for (config_path, target_path) in &config_map {
println!(" + {:?}", target_path);
match std::os::unix::fs::symlink(config_path, target_path) {
Ok(()) => (), // Configuration installed successfully
Err(_e) => {
// Attempt to remove the file or directory first, and then symlink the new config
match target_path.is_dir() {
true => fs_extra::dir::remove(target_path)
.expect(&format!("Error: Unable to remove directory: {:?}", target_path)),
false => fs_extra::file::remove(target_path)
.expect(&format!("Error: Unable to remove file: {:?}", target_path)),
};
// Try to symlink the config again, if failure exit with error
std::os::unix::fs::symlink(config_path, target_path)?;
},
}
pub fn handle_args(args: &mut kcli::Cli) -> Result<()> {
if args.is_repo {
// Attempt to install dotfiles from a dotfiles repository
// + No specific configuration required on behalf of dotfiles repo
kgit::clone(&args.dotfiles.to_str().unwrap(),
&args.clone_dir.as_ref().unwrap())?;
}
return match install_configs(args) {
Ok(_) => Ok(()),
Err(e) => {
// If we reach an error, use our backup_dir to restore configs
// + Remove configs we applied that weren't previously on the system
uninstall_configs(args)?;
Err(e)
}
}
}
Ok(())
/// Creates symbolic links to the configurations we're installing
pub fn install_configs(args: &mut kcli::Cli) -> Result<()> {
//
// Find path that points us to the dotfiles we are installing
let dotfiles = match args.is_repo {
// If the dotfiles were provided as a system path, use it
false => args.dotfiles.to_owned(),
// If the dotfiles to install was a repository, find the path we cloned to
true => args.clone_dir.as_ref().unwrap().to_path_buf()
};
//
// Check if there are any existing files in the install directory that are also within the dotfiles to install
// Get the configurations and their target installation paths in a hashmap<config, target_path>
// + Using target_path, check for conflicts and prompts user to abort or continue
let config_map = kfs::get_target_paths(&args.install_dir, &dotfiles)?;
handle_collisions(args, &config_map)?;
//
// Install the dotfiles configurations
// At this point there are either no conflicts or the user agreed to them
println!("Installing configs:");
for (config_path, target_path) in &config_map {
println!(" + {:?}", target_path);
std::os::unix::fs::symlink(config_path, target_path)
.or_else(|err| -> Result<()> {
eprintln!("Error: Unable to create symlink {:?} -> {:?} ({:?})",
target_path, config_path, err);
// Attempt to remove the file or directory first, and then symlink the new config
match target_path.is_dir() {
true => fs_extra::dir::remove(target_path)
.expect(&format!("Error: Unable to remove directory: {:?}", target_path)),
false => fs_extra::file::remove(target_path)
.expect(&format!("Error: Unable to remove file: {:?}", target_path)),
};
// Try to symlink the config again, if failure exit with error
std::os::unix::fs::symlink(config_path, target_path).or_else(|err| {
eprintln!("Error: Unable to symlink config: {:?} -> {:?}",
target_path, config_path);
return Err(err);
})?;
return Ok(());
})?;
}
return Ok(());
}
/// Handles collisions between existing files and dotfiles we're installing
fn handle_collisions(args : & kcli::Cli,
config_map : & kfs::HashMap<PathBuf, PathBuf>) -> Result<()> {
// Check if we found any collisions in the configurations
match check_collisions(&config_map) {
None => {
return Ok(()) // There were no collisions, configurations pass pre-install checks
},
Some(conflicts) => {
// Ask client if they would like to abort given the config collisions
let mut msg = format!("The following configurations already exist:");
for config in conflicts.iter() {
msg += format!("\n {:?}", config).as_str();
}
msg += format!("\nIf you continue, backups will be made in {:?}. \
Any configurations there will be overwritten.\
\nAbort? Enter y/n or Y/N: ", &args.backup_dir).as_str();
// If we abort, exit; If we continue, back up the configs
// TODO: Group this in with the --force flag?; Or make a new --adopt flag?
match kio::prompt(msg) {
true => return Ok(()),
false => {
// Backup each conflicting config at the install location
for backup_target in conflicts.iter() {
backup_config(backup_target, &args)?;
}
},
};
/// + If --force is not set, prompt user to continue based on conflicts found
/// + If --force is set or user chooses to continue,
/// move conflicting files to a backup directory
fn handle_collisions(args: &mut kcli::Cli,
config_map: &kfs::HashMap<PathBuf, PathBuf>) -> Result<()> {
// Check if we found any collisions in the configurations
return match check_collisions(&config_map) {
None => Ok(()), // There were no collisions, configurations pass pre-install checks
Some(conflicts) => {
args.conflicts = conflicts.to_owned();
// Ask client if they would like to abort given the config collisions
let mut msg = format!("The following configurations already exist:");
for config in conflicts.iter() {
msg += format!("\n {:?}", config).as_str();
}
msg += format!("\nIf you continue, backups will be made in {:?}. \
Any configurations there will be overwritten.\
\nContinue? Enter Y/y or N/n: ",
args.backup_dir.as_ref().unwrap()).as_str();
// If the --force flag is set, short-circuit boolean and skip prompt
match args.force || kio::prompt(msg) {
true => {
// Backup each conflicting config at the install location
for backup_target in conflicts.iter() {
backup_config(backup_target, &args)?;
}
Ok(())
},
};
false => err!(Other("User aborted installation".to_string()), "Aborted".to_string())
}
}
};
}
Ok(())
/// Checks if any config to install collides with existing files or directories
/// + Returns a list of collisions within Some(), else returns None
pub fn check_collisions(config_map: &HashMap<PathBuf, PathBuf>)
-> Option<Vec<PathBuf>> {
let mut config_conflicts = vec![];
for (_path, target_config) in config_map.iter() {
// If the target configuration file or directory already exists
if target_config.exists() {
config_conflicts.push(target_config.to_owned());
}
}
if !config_conflicts.is_empty() {
return Some(config_conflicts);
}
return None;
}
// Creates a backup of configurations that conflict
// + Backup directory location is specified by CLI --backup-dir
// TODO: .kotignore in dotfiles repo to specify files to not install / backup
// TODO: .kotrc in dotfiles repo or home dir to set backup-dir and install-dir?
fn backup_config(config_path: & kfs::PathBuf, args: & kcli::Cli) -> Result<()> {
let mut backup_path = args.backup_dir.to_owned();
backup_path.push(config_path.file_name().unwrap());
// Check if the configuration we're backing up is a directory or a single file
match config_path.is_dir() {
true => {
// Copy directory with recursion using move_dir() wrapper function
let mut options = kfs::dir::CopyOptions::new();
options.copy_inside = true;
options.overwrite = args.force;
if let Err(e) = kfs::move_dir(config_path, &backup_path, Some(&options))
.map_err(|e| e.into()) {
return Err(e)
}
},
false => {
// Copy single configuration file
let mut options = fs_extra::file::CopyOptions::new();
options.overwrite = args.force;
if let Err(e) = kfs::move_file(config_path, &backup_path, Some(&options))
.map_err(|e| e.into()) {
return Err(e)
}
},
fn backup_config(config_path: &kfs::PathBuf, args: &kcli::Cli) -> Result<()> {
let mut backup_path = args.backup_dir.as_ref().unwrap().to_owned();
// Check if the configuration we're backing up is a directory or a single file
match config_path.is_dir() {
true => {
// Copy directory with recursion using move_dir() wrapper function
let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true;
options.overwrite = args.force;
kfs::move_dir(config_path, &backup_path, Some(&options))?;
}
false => {
backup_path.push(config_path.file_name().unwrap());
// Copy single configuration file
kfs::move_file(config_path, &backup_path)?;
}
Ok(())
}
return Ok(());
}
// TODO: Function to uninstall configs.
// + Loops through dotfiles and restore backup files or delete configs
fn _uninstall_configs() -> Result<()> {
Ok(())
// Loops through dotfiles to restore backup files or delete unused configs
pub fn uninstall_configs(args: &kcli::Cli) -> Result<()> {
//
// Replace previous configs we stored in backup_dir
for config in args.backup_dir.as_ref().unwrap().read_dir()? {
match config.as_ref().unwrap().path().is_dir() {
true => {
let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true;
options.overwrite = args.force;
kfs::move_dir(&config.as_ref().unwrap().path(), &args.install_dir,
Some(&options)
)?;
},
false => {
kfs::move_file(&config.unwrap().path(), &args.install_dir)?;
}
};
}
//
// Remove configurations only required by the dotfiles we attempted to install
// Check each config in the dotfiles we want to uninstall
let dotfile_path = match args.is_repo {
true => args.clone_dir.as_ref().unwrap(),
false => &args.dotfiles
};
for dotfile in dotfile_path.read_dir()? {
let path = dotfile.unwrap().path();
// If the configuration was not a conflict initially
// then we didn't have it before we installed; It is not being used
if !args.conflicts.contains(&path) {
let mut unused_config: PathBuf = args.install_dir.to_owned();
unused_config.push(std::path::Path::new(&path.file_name().unwrap()));
// Verify the file was already installed before we hit an error
if !unused_config.exists() {
continue;
}
// Remove the unused config from install_dir
std::fs::remove_file(unused_config)?;
}
}
return Ok(());
}

236
src/kot/kcli.rs

@ -6,8 +6,16 @@ @@ -6,8 +6,16 @@
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################*/
use std::path::Path;
use std::borrow::Borrow;
use std::path::{Path, PathBuf};
use regex::Regex;
use structopt::StructOpt;
use crate::kot::kerror::{Error, ErrorKind};
use crate::kot::err;
use crate::kot::kfs::create_dir_all;
use chrono;
use super::kfs;
// =============================================================================
// STRUCTS
@ -15,43 +23,86 @@ use structopt::StructOpt; @@ -15,43 +23,86 @@ use structopt::StructOpt;
// -----------------------------------------------------------------------------
/// Struct to outline behavior and features of kot CLI
/// CLI for managing Linux user configurations
#[derive(Debug, StructOpt)]
#[structopt(
name="kot",
about="CLI for managing Linux user configurations"
)]
#[structopt(name = "kot")]
pub struct Cli {
#[structopt(
help="Local or full path to user configurations to install",
parse(from_os_str)
)]
pub dotfiles_dir: std::path::PathBuf,
#[structopt(
help="The location to attempt installation of user configurations",
default_value="dry-runs/kapper", // TODO: Remove temp default value after tests
// env = "HOME", // Default value to env variable $HOME
name="install-dir",
short, long,
parse(from_os_str)
)]
pub install_dir: std::path::PathBuf,
#[structopt(
help="The location to store backups for this user",
default_value="backups/kapper",
name="backup-dir",
short, long,
parse(from_os_str)
)]
pub backup_dir: std::path::PathBuf,
#[structopt(
help="Overwrites existing backups",
short, long
)]
pub force: bool,
/// Local or full path to user configurations to install. Can also be a git repository.
///
/// System path or repository URL for dotfiles we want to install.
/// If a path is used, it can either be local to CWD or absolute.
/// If a URL is used for a dotfiles repository, the repo is cloned into $HOME/.local/shared/kot/dotfiles/
#[structopt(parse(from_os_str))]
pub dotfiles: PathBuf,
/// The location to attempt installation of user configurations
///
/// The desired installation directory for user configurations.
/// By default this is your $HOME directory
/// This could optionally point to some other directory to perform a dry run, or the --dry-run flag could be set
#[structopt(
env = "HOME", // Default value to env variable $HOME
name = "install",
short, long,
parse(from_os_str)
)]
pub install_dir: PathBuf,
/// The location to store backups for this user
///
/// If no backup-dir is provided, we create one within the default kot data directory:
/// $HOME/.local/share/kot/backups/
#[structopt(
name = "backup-dir",
short, long,
parse(from_os_str)
)]
pub backup_dir: Option<PathBuf>,
/// An alternate path to clone a dotfiles repository to
///
/// If the clone-dir option is provided to the CLI, kot will clone the dotfiles repository into this directory.
/// If clone-dir is not provided, the repository is cloned into $HOME/.local/share/kot/dotfiles
/// Custom clone-dir will be used literally, and no subdirectory is created to store the cloned repository
/// For example, clone-dir of $HOME/clonedir for repo named Dotfiles
/// We will clone into $HOME/clonedir, and NOT $HOME/clonedir/Dotfiles
/// The default path for cloned repos is $HOME/.local/share/kot/dotfiles/
#[structopt(
name = "clone-dir",
short, long,
parse(from_os_str)
)]
pub clone_dir: Option<PathBuf>,
/// Overwrites existing backups
///
/// This flag will replace existing backups if during installation we encounter conflicts
/// and the backup-dir provided already contains previous backups.
#[structopt(
name = "force",
short, long
)]
pub force: bool,
/// Installs configurations to $HOME/.local/shared/kot/dry-runs
///
/// Useful flag to set when testing what an install would do to your home directory.
/// This is synonymous with setting --install $HOME/.local/shared/kot/dry-runs/$USER.
/// Subsequent runs with this flag set will not delete the contents of this directory.
#[structopt(
name = "dry-run",
short, long
)]
pub dry_run: bool,
// Indicates if dotfiles is a git repository URL; Not used by CLI directly
// + Initialized with result of regex pattern matching
#[structopt(skip)]
pub is_repo: bool,
// Not used by CLI, used to uninstall dotfiles when error is hit
#[structopt(skip)]
pub conflicts: Vec<PathBuf>,
}
// =============================================================================
@ -64,50 +115,79 @@ pub struct Cli { @@ -64,50 +115,79 @@ pub struct Cli {
/// + Also enforces use of Cli::normalize()
/// + https://docs.rs/structopt/0.3.23/src/structopt/lib.rs.html#1121-1126
pub fn from_args() -> super::Result<Cli> {
let s = Cli::from_clap(&Cli::clap().get_matches());
s.normalize()
let s = Cli::from_clap(&Cli::clap().get_matches());
s.normalize()
}
impl Cli {
/// Helper function to normalize arguments passed to program
pub fn normalize(mut self) -> super::Result<Self> {
// If the path to the dotfiles doesn't exist, exit with error
if !&self.dotfiles_dir.exists() {
panic!("Error: Dotfiles configuration at {:?} does not exist", self.dotfiles_dir);
}
self.dotfiles_dir = self.dotfiles_dir.canonicalize()?;
// If either the install or backup dir don't exist, create them
std::fs::create_dir_all(&self.install_dir)?;
self.install_dir = self.install_dir.canonicalize()?;
std::fs::create_dir_all(&self.backup_dir)?;
self.backup_dir = self.backup_dir.canonicalize()?;
// + To enforce the correction when error is encountered
// Get the number of configs currently in backup directory
// + An empty backup directory returns a count of 1
let current_backups = self.backup_dir.read_dir()?.count();
// If there are files in the backup directory already
if current_backups > 1 {
// If the --force flag is not set, warn and abort
if !self.force {
panic!("\n Error: Backups already exist at {:?}\
\n Set the --force flag to overwrite configurations stored here" , self.backup_dir)
}
// If the --force flag is set, remove backups and create new
// + Move backups to /tmp/<BACKUP_DIRNAME>
// + If we encounter an error, we can move these temp files back to args.backup_dir
// + On success we can delete them since new backups will have been created at args.backup_dir
let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true;
options.overwrite = true;
let mut temp_path = Path::new("/tmp/").to_path_buf();
temp_path.push(self.backup_dir.file_name().unwrap());
// Move the old backups to /tmp/ and create a new empty backup directory
super::kfs::move_dir(&self.backup_dir, &temp_path, Some(&options))?;
std::fs::create_dir_all(&self.backup_dir)?;
}
Ok(self)
/// Helper function to normalize arguments passed to program
/// + Checks if dotfiles path is a repository URL
/// + If dotfiles path is not a repo URL, checks the path exists on the system
/// + Verifies install directory exists
/// + Verifies backup directory exists and does not already contain backups
pub fn normalize(mut self) -> super::Result<Self> {
// Determine if the dotfiles were provided as a github repository URL
let re_git = Regex::new(
r"^(([A-Za-z0-9]+@|http(|s)://)|(http(|s)://[A-Za-z0-9]+@))([A-Za-z0-9.]+(:\d+)?)(?::|/)([\d/\w.-]+?)(\.git){1}$"
);
self.is_repo = re_git.unwrap().is_match(&self.dotfiles.to_str().unwrap());
if self.is_repo {
// If the dotfiles were provided as a repository URL initialize clone_dir
self.clone_dir = match &self.clone_dir {
Some(d) => {
kfs::create_dir_all(d)?;
Some(kfs::abs(d)?)
},
None => Some(kfs::get_repo_path(self.dotfiles.to_str().unwrap()))
};
}
else {
// If the dotfiles were provided as a path, canonicalize it
self.dotfiles = kfs::abs(&self.dotfiles)?;
}
//
// If either the install, backup, or clone dir does not exist, create them
if self.dry_run {
self.install_dir = Path::new(
&(env!("HOME").to_owned() + &"/.local/share/kot/dry-runs/" + env!("USER"))
).to_path_buf();
}
self.install_dir = kfs::create_dir_all(&self.install_dir)?;
// If the CLI was not provided a backup_dir, use default naming convention
match self.backup_dir {
None => {
let mut backup_dir = kfs::get_data_dir();
backup_dir.push("backups/");
backup_dir.push(self.dotfiles.file_name().unwrap().to_str().unwrap().to_owned()
+ ":" + &*chrono::offset::Local::now()
.format("%Y-%m-%dT%H:%M:%S").to_string()
);
self.backup_dir = Some(kfs::create_dir_all(&backup_dir)?);
}
Some(dir) => {
// If a backup_dir was given to CLI, use it instead of default
self.backup_dir = Some(kfs::create_dir_all(&dir)?);
}
}
//
// Check if the backup directory provided is empty
// If there are files and the --force flag is not set, warn and abort
if !self.force && kfs::dir_entries(&self.backup_dir.as_ref().unwrap())? > 1 {
return err!(
ErrorKind::ConfigError(format!("Backups already exist at: {:?}", &self.backup_dir)),
"Set the --force flag to overwrite configurations stored here".to_owned()
);
}
// If the --force flag is set, stash backup files in /tmp/ and create new
kfs::stash_dir(&self.backup_dir.as_ref().unwrap())?;
// Available CLI options pass initial checks; Return them to caller
return Ok(self);
}
}

78
src/kot/kerror.rs

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
/*##############################################################################
## Author: Shaun Reed ##
## Legal: All Content (c) 2021 Shaun Reed, all rights reserved ##
## About: Error module for dotfiles manager kot ##
## This module supports converting errors to custom types using ? operator ##
## ##
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################*/
use std::fmt::{Debug, Display, Formatter};
// Error types for kot application
#[derive(Debug)]
pub enum ErrorKind {
ConfigError(String),
GitError(String),
IOError(String),
FileError(String),
DirError(String),
Other(String),
}
// =============================================================================
// IMPLEMENTATION
// =============================================================================
#[derive(Debug)]
pub struct Error {
pub kind: ErrorKind,
message: String,
}
// Implement Display trait for printing found errors
impl std::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "Kot {:?}", self)
}
}
impl std::error::Error for Error { }
impl Error {
pub fn new(kind: ErrorKind, message: String) -> Error {
Error {
kind: kind,
message: message.to_string(),
}
}
}
// Implement From<T> for each error type T that we want to handle
// These implementations handle converting from T to kot::kerror::Error using ?
// Converting from std::io::Error to kot::kerror::Error::GitError
impl std::convert::From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
return Error::new(ErrorKind::IOError(error.to_string()),
"(std::io error)".to_owned());
}
}
// Converting from fs_extra::error::Error to kot::kerror::Error::GitError
impl std::convert::From<fs_extra::error::Error> for Error {
fn from(error: fs_extra::error::Error) -> Self {
return Error::new(ErrorKind::FileError(error.to_string()),
"(fs_extra error)".to_owned());
}
}
// -----------------------------------------------------------------------------

189
src/kot/kfs.rs

@ -6,12 +6,15 @@ @@ -6,12 +6,15 @@
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################*/
// Allow the use of kot::fs::Path and kot::fs::PathBuf from std::path::
pub use std::path::{Path, PathBuf};
pub use std::collections::HashMap;
pub use fs_extra::dir;
use std::fs;
use crate::kot::err;
use crate::kot::kerror::{Error, ErrorKind};
use super::kgit;
// =============================================================================
// IMPLEMENTATION
@ -19,72 +22,146 @@ use std::fs; @@ -19,72 +22,146 @@ use std::fs;
// -----------------------------------------------------------------------------
pub fn abs(dir: &PathBuf) -> super::Result<PathBuf> {
return match dir.canonicalize() {
Ok(result) => Ok(result),
Err(e) => {
err!(
ErrorKind::IOError(e.to_string()),
format!("Unable to canonicalize dir: {:?}", dir)
);
}
};
}
/// Initialize and return a HashMap<config_dir, config_install_location>
/// + Later used to check each install location for conflicts before installing
/// + This function does not create or modify any files or directories
pub fn get_target_paths(args: & super::kcli::Cli) -> super::Result<HashMap<PathBuf, PathBuf>> {
let mut config_map = HashMap::new();
// Local variable for the installation directory as an absolute path
let mut config_target = args.install_dir.to_owned();
// For each file or directory within the dotfiles we're installing
for config_entry in fs::read_dir(&args.dotfiles_dir)? {
let entry = config_entry?;
// Create full path to target config file (or directory) by push onto install path
config_target.push(entry.file_name());
// If the entry doesn't already exist, insert it into the config_map
// + Key is full path to source config from dotfiles repo we're installing
// + Value is desired full path to config at final install location
// TODO: If the entry does exist, should there be an exception?
config_map.entry(entry.path().to_owned())
.or_insert(config_target.to_owned());
// Reset config_target to be equal to requested install_dir
config_target.pop();
}
Ok(config_map)
}
pub fn get_target_paths(install_dir: &PathBuf, dotfiles: &PathBuf)
-> super::Result<HashMap<PathBuf, PathBuf>> {
let mut config_map = HashMap::new();
/// Checks if any config to install collides with existing files or directories
/// + Returns a count of collisions within Some(), else returns None
pub fn check_collisions(config_map : & HashMap<PathBuf, PathBuf>) -> Option<Vec<PathBuf>> {
let mut config_conflicts = vec![];
for (_path, target_config) in config_map.iter() {
// If the target configuration file or directory already exists
if target_config.exists() {
config_conflicts.push(target_config.to_owned());
}
}
if config_conflicts.len() > 0 {
return Some(config_conflicts)
}
return None
// Local variable for the installation directory as an absolute path
let mut config_target = install_dir.to_owned();
// For each file or directory within the dotfiles we're installing
for config_entry in fs::read_dir(&dotfiles)? {
let entry = config_entry?;
// Create full path to target config file (or directory) by push onto install path
config_target.push(entry.file_name());
// If the entry doesn't already exist, insert it into the config_map
// + Key is full path to source config from dotfiles repo we're installing
// + Value is desired full path to config at final install location
config_map.entry(entry.path().to_owned())
.or_insert(config_target.to_owned());
// Reset config_target to be equal to requested install_dir
config_target.pop();
}
return Ok(config_map);
}
/// Moves a single file from one location to another; Can be used to rename files
/// + Overwrites file at the dst location with the src file
/// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided
pub fn move_file(src: & PathBuf, dst: & PathBuf,
options: Option< & fs_extra::file::CopyOptions>) -> super::Result<()> {
if options.is_none() {
// Default CopyOptions for moving files
let mut options = fs_extra::file::CopyOptions::new();
options.overwrite = false;
}
fs_extra::file::move_file(src, dst, options.unwrap())?;
Ok(())
pub fn move_file(src: &PathBuf, dst: &PathBuf) -> super::Result<()> {
std::fs::copy(src, dst)?;
std::fs::remove_file(src)?;
return Ok(());
}
/// Moves a directory and all of it's contents recursively
/// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided
pub fn move_dir(src: & PathBuf, dst: & PathBuf,
options: Option< & fs_extra::dir::CopyOptions>) -> super::Result<()> {
if options.is_none() {
// Default CopyOptions for moving directories
let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true;
options.overwrite = false;
/// TODO: Implement this using std::fs to remove fs_extra dependency
pub fn move_dir(src: &PathBuf, dst: &PathBuf,
options: Option<&fs_extra::dir::CopyOptions>)
-> super::Result<()> {
let copy_options = match options {
Some(opts) => opts.to_owned(),
None => {
// Default CopyOptions for moving directories
let mut opts = fs_extra::dir::CopyOptions::new();
opts.copy_inside = true;
opts.overwrite = false;
opts
}
fs_extra::dir::move_dir(src, dst, options.unwrap())?;
Ok(())
};
if let Err(e) = fs_extra::dir::move_dir(src, dst, &copy_options) {
return err!(
ErrorKind::DirError(e.to_string()),
format!("Cannot move directory from {:?} to {:?}", src, dst)
);
}
return Ok(());
}
/// Recursively creates a directory
/// Returns a result that contains the absolute path to the new directory
pub fn create_dir_all(dir: &PathBuf) -> super::Result<PathBuf> {
return match fs::create_dir_all(dir) {
Ok(_) => {
Ok(dir.to_owned())
},
Err(e) => {
err!(
ErrorKind::IOError(e.to_string()),
format!("Unable to create directory: {:?}", dir)
)
}
};
}
/// Returns the total number of entries within a directory
/// + Returns 1 for empty directories
pub fn dir_entries(dir: &PathBuf) -> super::Result<usize> {
if !dir.exists() {
return Ok(0)
}
let count = dir.read_dir().and_then(|dir| Ok(dir.count()))?;
return Ok(count);
}
/// Stash a directory in the temp folder, staging it for deletion
/// + We stash first instead of delete to allow recovery of these files if we run into an error
pub fn stash_dir(dir: &PathBuf) -> super::Result<()> {
// Get the number of configs currently in backup directory
// + An empty backup directory returns a count of 1
if dir_entries(&dir)? > 1 {
// Move backups to /tmp/<BACKUP_DIRNAME>
// + If we encounter an error, we can move these temp files back to args.backup_dir
// + On success we can delete them since new backups will have been created at args.backup_dir
let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true;
options.overwrite = true;
let mut temp_path = get_temp_dir();
temp_path.push(dir.file_name().unwrap());
// Move the old backups to /tmp/ and create a new empty backup directory
super::kfs::move_dir(&dir, &temp_path, Some(&options))?;
std::fs::create_dir_all(&dir)?;
}
return Ok(());
}
/// Gets the root temp directory used by kot to store expired files as an owned PathBuf
pub fn get_temp_dir() -> PathBuf {
// Get temp directory from current user environment
let mut temp = std::env::temp_dir();
temp.push("kot/expired/");
return temp;
}
/// Constructs a new PathBuf pointing to the default data directory used by kot
pub fn get_data_dir() -> PathBuf {
let mut data_dir = std::path::Path::new(env!("HOME")).to_path_buf();
data_dir.push(".local/share/kot/");
return data_dir;
}
/// Constructs a new PathBuf pointing to the default clone directory used by kot
pub fn get_repo_path(repo_url: &str) -> PathBuf {
let mut repo_path = get_data_dir();
// Store the new dotfiles repo in a subdirectory using it's name
repo_path.push("dotfiles/".to_owned() + &kgit::repo_name(repo_url) + "/");
return repo_path;
}

47
src/kot/kgit.rs

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
/*##############################################################################
## Author: Shaun Reed ##
## Legal: All Content (c) 2021 Shaun Reed, all rights reserved ##
## About: Wrapper module for git written in Rust ##
## ##
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################*/
use std::os::linux::raw::stat;
use std::path::{PathBuf};
use std::process::{Command};
use crate::kot::err;
use super::kerror::{Error, ErrorKind};
use super::kfs;
// =============================================================================
// IMPLEMENTATION
// =============================================================================
// -----------------------------------------------------------------------------
/// Clones a Git repository using https or ssh
/// + By default, cloned repositories are stored in $HOME/.local/share/kot/dotfiles/
pub fn clone(repo_url: &str, clone_dir: &PathBuf)
-> super::Result<PathBuf> {
// Clone the repository, check that status return value is 0
let status = Command::new("git")
.args(["clone", repo_url, clone_dir.to_str().unwrap(), "--recursive"])
.status().unwrap();
return match status.code() {
Some(0) => Ok(clone_dir.to_owned()),
_ => {
return
err!(ErrorKind::GitError(status.to_string()),
format!("Unable to clone repository"));
}
}
}