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
This commit is contained in:
Shaun Reed 2022-05-29 19:19:42 -04:00
parent eabc227c09
commit a01ab6b532
15 changed files with 869 additions and 376 deletions

2
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

153
Cargo.lock generated
View File

@ -3,10 +3,19 @@
version = 3 version = 3
[[package]] [[package]]
name = "ansi_term" name = "aho-corasick"
version = "0.11.0" version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
@ -22,6 +31,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -29,10 +44,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "clap" name = "chrono"
version = "2.33.3" version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"winapi",
]
[[package]]
name = "clap"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"atty", "atty",
@ -69,9 +97,11 @@ dependencies = [
[[package]] [[package]]
name = "kot" name = "kot"
version = "0.1.0" version = "0.1.5"
dependencies = [ dependencies = [
"chrono",
"fs_extra", "fs_extra",
"regex",
"structopt", "structopt",
] ]
@ -83,9 +113,34 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.102" version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
@ -113,22 +168,39 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.29" version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
dependencies = [ dependencies = [
"unicode-xid", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
dependencies = [ dependencies = [
"proc-macro2", "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]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@ -137,9 +209,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]] [[package]]
name = "structopt" name = "structopt"
version = "0.3.23" version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa" checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
dependencies = [ dependencies = [
"clap", "clap",
"lazy_static", "lazy_static",
@ -148,9 +220,9 @@ dependencies = [
[[package]] [[package]]
name = "structopt-derive" name = "structopt-derive"
version = "0.4.16" version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba" checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
@ -161,13 +233,13 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.77" version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-xid", "unicode-ident",
] ]
[[package]] [[package]]
@ -180,10 +252,27 @@ dependencies = [
] ]
[[package]] [[package]]
name = "unicode-segmentation" name = "time"
version = "1.8.0" version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" 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.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
@ -191,12 +280,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.2" version = "0.8.2"
@ -205,9 +288,15 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.3" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]] [[package]]
name = "winapi" name = "winapi"

View File

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

203
README.md
View File

@ -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. 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). 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, `kot` can install dotfiles from any source directory, to any target directory.
you could point `--install-dir` to any directory that you've created for testing. 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. `kot` will attempt This directory could be empty, or it could contain another set of dotfiles.
to install the configurations. If conflicts are detected, output shows the conflicts and 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. prompts to abort or continue. An example of this is seen below.
This prompt will be skipped if the `--force` flag is set.
```bash ```bash
kot dotfiles/dot/ kot --dry-run 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 }
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: The following configurations already exist:
"/home/kapper/Code/kot/dry-runs/kapper/.bashrc" "/home/kapper/.local/share/kot/dry-runs/kapper/.git"
"/home/kapper/Code/kot/dry-runs/kapper/.config" "/home/kapper/.local/share/kot/dry-runs/kapper/.vimrc"
"/home/kapper/Code/kot/dry-runs/kapper/README.md" "/home/kapper/.local/share/kot/dry-runs/kapper/.bash_aliases"
"/home/kapper/Code/kot/dry-runs/kapper/VimScreenshot.png" "/home/kapper/.local/share/kot/dry-runs/kapper/.vim"
"/home/kapper/Code/kot/dry-runs/kapper/fix-vbox.sh" "/home/kapper/.local/share/kot/dry-runs/kapper/VimScreenshot.png"
"/home/kapper/Code/kot/dry-runs/kapper/.git" "/home/kapper/.local/share/kot/dry-runs/kapper/.gitignore"
"/home/kapper/Code/kot/dry-runs/kapper/.bash_aliases" "/home/kapper/.local/share/kot/dry-runs/kapper/.config"
"/home/kapper/Code/kot/dry-runs/kapper/.gitignore" "/home/kapper/.local/share/kot/dry-runs/kapper/fix-vbox.sh"
"/home/kapper/Code/kot/dry-runs/kapper/.gitmodules" "/home/kapper/.local/share/kot/dry-runs/kapper/.gitmodules"
"/home/kapper/Code/kot/dry-runs/kapper/.vimrc" "/home/kapper/.local/share/kot/dry-runs/kapper/.bashrc"
"/home/kapper/Code/kot/dry-runs/kapper/.vim" "/home/kapper/.local/share/kot/dry-runs/kapper/README.md"
If you continue, backups will be made in "/home/kapper/Code/kot/backups/kapper". Any configurations there will be overwritten. If you continue, backups will be made in "/home/kapper/.local/share/kot/backups/dot:2022-05-29T19:03:27".
Abort? Enter y/n or Y/N: 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. #### User Data
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.
`kot` stores user data within `$HOME/.local/share/kot/`
```bash When we provide a repository URL as our `dotfiles` to install, the repo will be *recursively* cloned into
kot dotfiles/dot/ `$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 ' When we encounter conflicts during installation of these dotfiles, backups will be created in
Error: Backups already exist at "/home/kapper/Code/kot/backups/kapper" `$HOME/.local/share/kot/backups/<DOTFILES_NAME>:<DATE(%Y-%m-%dT%H:%M:%S)>`
Set the --force flag to overwrite configurations stored here', src/kot/kcli.rs:94:17 If there are no conflicts found during installation, no backup is created.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace Configurations are said to be conflicting if the `--install` path contains configuration files that are
``` also within the dotfiles we are currently installing.
#### Installation 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.
#### Installing kot
Follow [Rustup instructions](https://rustup.rs/) to setup the Rust toolchain Follow [Rustup instructions](https://rustup.rs/) to setup the Rust toolchain
@ -60,65 +71,97 @@ git clone https://gitlab.com/shaunrd0/kot && cd kot
cargo install --path . cargo install --path .
kot --help kot --help
kot 0.1.0 kot 0.1.5
CLI for managing Linux user configurations CLI for managing Linux user configurations
USAGE: USAGE:
kot [FLAGS] [OPTIONS] <dotfiles-dir> kot [FLAGS] [OPTIONS] <dotfiles> --install <install>
FLAGS: FLAGS:
-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
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>
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/
```
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
cd path/to/kot
cargo build
cargo run -- --help
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 -f, --force Overwrites existing backups
-h, --help Prints help information -h, --help Prints help information
-V, --version Prints version information -V, --version Prints version information
OPTIONS: OPTIONS:
-b, --backup-dir <backup-dir> The location to store backups for this user [default: backups/kapper] -b, --backup-dir <backup-dir> The location to store backups for this user
-i, --install-dir <install-dir> The location to attempt installation of user configurations [default: dry- -c, --clone-dir <clone-dir> An alternate path to clone a dotfiles repository to
runs/kapper] -i, --install <install> The location to attempt installation of user configurations [env:
HOME=/home/kapper]
ARGS: 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 #### TODO
To store dotfiles, this repository uses submodules. To update surface-level submodules, we can run the following commands * Ensure empty backups are not created
```bash * Provide interface for managing agreed-upon /etc/skel/ configurations
git submodule update --init * Provide more CLI options for git functionality; Branches, update submodules, etc
* Clean up warnings during build / installation
Submodule path 'dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843' * Automate testing
``` *
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'
```

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

View File

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

View File

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

View File

@ -6,15 +6,31 @@
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## ## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################*/ ##############################################################################*/
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use crate::kot::kfs::check_collisions;
pub mod kcli; pub mod kcli;
pub mod kfs; pub mod kfs;
pub mod kio; pub mod kio;
pub mod kgit;
pub mod kerror;
use kerror::Error;
/// Result alias to return result with Error of various types /// 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 // IMPLEMENTATION
@ -22,24 +38,55 @@ pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
/// Creates symbolic links to the configurations we're installing pub fn handle_args(args: &mut kcli::Cli) -> Result<()> {
// TODO: On error, revert to last good state if args.is_repo {
// TODO: User script to execute after installing configs successfully // Attempt to install dotfiles from a dotfiles repository
pub fn install_configs(args: & kcli::Cli) -> Result<()> { // + No specific configuration required on behalf of dotfiles repo
// Get the configurations and their target installation paths kgit::clone(&args.dotfiles.to_str().unwrap(),
// + Checks for conflicts and prompts user to abort or continue &args.clone_dir.as_ref().unwrap())?;
let config_map = kfs::get_target_paths(&args)?; }
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)
}
}
}
/// 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 // Check if there are any existing files in the install directory that are also within the dotfiles to install
handle_collisions(&args, &config_map)?;
// 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 // At this point there are either no conflicts or the user agreed to them
println!("Installing configs:"); println!("Installing configs:");
for (config_path, target_path) in &config_map { for (config_path, target_path) in &config_map {
println!(" + {:?}", target_path); println!(" + {:?}", target_path);
match std::os::unix::fs::symlink(config_path, target_path) { std::os::unix::fs::symlink(config_path, target_path)
Ok(()) => (), // Configuration installed successfully .or_else(|err| -> Result<()> {
Err(_e) => { 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 // Attempt to remove the file or directory first, and then symlink the new config
match target_path.is_dir() { match target_path.is_dir() {
true => fs_extra::dir::remove(target_path) true => fs_extra::dir::remove(target_path)
@ -48,23 +95,30 @@ pub fn install_configs(args: & kcli::Cli) -> Result<()> {
.expect(&format!("Error: Unable to remove file: {:?}", target_path)), .expect(&format!("Error: Unable to remove file: {:?}", target_path)),
}; };
// Try to symlink the config again, if failure exit with error // Try to symlink the config again, if failure exit with error
std::os::unix::fs::symlink(config_path, target_path)?; 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(());
})?;
} }
Ok(()) return Ok(());
} }
/// Handles collisions between existing files and dotfiles we're installing /// Handles collisions between existing files and dotfiles we're installing
fn handle_collisions(args : & kcli::Cli, /// + If --force is not set, prompt user to continue based on conflicts found
config_map : & kfs::HashMap<PathBuf, PathBuf>) -> Result<()> { /// + 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 // Check if we found any collisions in the configurations
match check_collisions(&config_map) { return match check_collisions(&config_map) {
None => { None => Ok(()), // There were no collisions, configurations pass pre-install checks
return Ok(()) // There were no collisions, configurations pass pre-install checks
},
Some(conflicts) => { Some(conflicts) => {
args.conflicts = conflicts.to_owned();
// Ask client if they would like to abort given the config collisions // Ask client if they would like to abort given the config collisions
let mut msg = format!("The following configurations already exist:"); let mut msg = format!("The following configurations already exist:");
for config in conflicts.iter() { for config in conflicts.iter() {
@ -72,61 +126,111 @@ fn handle_collisions(args : & kcli::Cli,
} }
msg += format!("\nIf you continue, backups will be made in {:?}. \ msg += format!("\nIf you continue, backups will be made in {:?}. \
Any configurations there will be overwritten.\ Any configurations there will be overwritten.\
\nAbort? Enter y/n or Y/N: ", &args.backup_dir).as_str(); \nContinue? Enter Y/y or N/n: ",
args.backup_dir.as_ref().unwrap()).as_str();
// If we abort, exit; If we continue, back up the configs // If the --force flag is set, short-circuit boolean and skip prompt
// TODO: Group this in with the --force flag?; Or make a new --adopt flag? match args.force || kio::prompt(msg) {
match kio::prompt(msg) { true => {
true => return Ok(()),
false => {
// Backup each conflicting config at the install location // Backup each conflicting config at the install location
for backup_target in conflicts.iter() { for backup_target in conflicts.iter() {
backup_config(backup_target, &args)?; backup_config(backup_target, &args)?;
} }
},
};
},
};
Ok(()) Ok(())
},
false => err!(Other("User aborted installation".to_string()), "Aborted".to_string())
}
}
};
}
/// 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 // Creates a backup of configurations that conflict
// + Backup directory location is specified by CLI --backup-dir // + Backup directory location is specified by CLI --backup-dir
// TODO: .kotignore in dotfiles repo to specify files to not install / backup // 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? // 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<()> { fn backup_config(config_path: &kfs::PathBuf, args: &kcli::Cli) -> Result<()> {
let mut backup_path = args.backup_dir.to_owned(); let mut backup_path = args.backup_dir.as_ref().unwrap().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 // Check if the configuration we're backing up is a directory or a single file
match config_path.is_dir() { match config_path.is_dir() {
true => { true => {
// Copy directory with recursion using move_dir() wrapper function // Copy directory with recursion using move_dir() wrapper function
let mut options = kfs::dir::CopyOptions::new(); let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true; options.copy_inside = true;
options.overwrite = args.force; options.overwrite = args.force;
if let Err(e) = kfs::move_dir(config_path, &backup_path, Some(&options)) kfs::move_dir(config_path, &backup_path, Some(&options))?;
.map_err(|e| e.into()) {
return Err(e)
} }
},
false => { false => {
backup_path.push(config_path.file_name().unwrap());
// Copy single configuration file // Copy single configuration file
let mut options = fs_extra::file::CopyOptions::new(); kfs::move_file(config_path, &backup_path)?;
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)
} }
},
} }
Ok(()) return Ok(());
} }
// TODO: Function to uninstall configs. // Loops through dotfiles to restore backup files or delete unused configs
// + Loops through dotfiles and restore backup files or delete configs pub fn uninstall_configs(args: &kcli::Cli) -> Result<()> {
fn _uninstall_configs() -> Result<()> { //
Ok(()) // 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(());
} }

View File

@ -6,8 +6,16 @@
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## ## 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 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 // STRUCTS
@ -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)] #[derive(Debug, StructOpt)]
#[structopt( #[structopt(name = "kot")]
name="kot",
about="CLI for managing Linux user configurations"
)]
pub struct Cli { pub struct Cli {
#[structopt( /// Local or full path to user configurations to install. Can also be a git repository.
help="Local or full path to user configurations to install", ///
parse(from_os_str) /// 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.
pub dotfiles_dir: std::path::PathBuf, /// 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( #[structopt(
help="The location to attempt installation of user configurations", env = "HOME", // Default value to env variable $HOME
default_value="dry-runs/kapper", // TODO: Remove temp default value after tests name = "install",
// env = "HOME", // Default value to env variable $HOME
name="install-dir",
short, long, short, long,
parse(from_os_str) parse(from_os_str)
)] )]
pub install_dir: std::path::PathBuf, 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( #[structopt(
help="The location to store backups for this user", name = "backup-dir",
default_value="backups/kapper",
name="backup-dir",
short, long, short, long,
parse(from_os_str) parse(from_os_str)
)] )]
pub backup_dir: std::path::PathBuf, 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( #[structopt(
help="Overwrites existing backups", 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 short, long
)] )]
pub force: bool, 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>,
} }
// ============================================================================= // =============================================================================
@ -70,44 +121,73 @@ pub fn from_args() -> super::Result<Cli> {
impl Cli { impl Cli {
/// Helper function to normalize arguments passed to program /// 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> { pub fn normalize(mut self) -> super::Result<Self> {
// If the path to the dotfiles doesn't exist, exit with error // Determine if the dotfiles were provided as a github repository URL
if !&self.dotfiles_dir.exists() { let re_git = Regex::new(
panic!("Error: Dotfiles configuration at {:?} does not exist", self.dotfiles_dir); r"^(([A-Za-z0-9]+@|http(|s)://)|(http(|s)://[A-Za-z0-9]+@))([A-Za-z0-9.]+(:\d+)?)(?::|/)([\d/\w.-]+?)(\.git){1}$"
} );
self.dotfiles_dir = self.dotfiles_dir.canonicalize()?; self.is_repo = re_git.unwrap().is_match(&self.dotfiles.to_str().unwrap());
// If either the install or backup dir don't exist, create them if self.is_repo {
std::fs::create_dir_all(&self.install_dir)?; // If the dotfiles were provided as a repository URL initialize clone_dir
self.install_dir = self.install_dir.canonicalize()?; self.clone_dir = match &self.clone_dir {
std::fs::create_dir_all(&self.backup_dir)?; Some(d) => {
self.backup_dir = self.backup_dir.canonicalize()?; kfs::create_dir_all(d)?;
Some(kfs::abs(d)?)
// + To enforce the correction when error is encountered },
// Get the number of configs currently in backup directory None => Some(kfs::get_repo_path(self.dotfiles.to_str().unwrap()))
// + 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 else {
// + Move backups to /tmp/<BACKUP_DIRNAME> // If the dotfiles were provided as a path, canonicalize it
// + If we encounter an error, we can move these temp files back to args.backup_dir self.dotfiles = kfs::abs(&self.dotfiles)?;
// + 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) //
// 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 Normal file
View File

@ -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());
}
}
// -----------------------------------------------------------------------------

View File

@ -6,12 +6,15 @@
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## ## 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::path::{Path, PathBuf};
pub use std::collections::HashMap; pub use std::collections::HashMap;
pub use fs_extra::dir; pub use fs_extra::dir;
use std::fs; use std::fs;
use crate::kot::err;
use crate::kot::kerror::{Error, ErrorKind};
use super::kgit;
// ============================================================================= // =============================================================================
// IMPLEMENTATION // IMPLEMENTATION
@ -19,16 +22,29 @@ 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> /// Initialize and return a HashMap<config_dir, config_install_location>
/// + Later used to check each install location for conflicts before installing /// + Later used to check each install location for conflicts before installing
/// + This function does not create or modify any files or directories /// + 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>> { pub fn get_target_paths(install_dir: &PathBuf, dotfiles: &PathBuf)
-> super::Result<HashMap<PathBuf, PathBuf>> {
let mut config_map = HashMap::new(); let mut config_map = HashMap::new();
// Local variable for the installation directory as an absolute path // Local variable for the installation directory as an absolute path
let mut config_target = args.install_dir.to_owned(); let mut config_target = install_dir.to_owned();
// For each file or directory within the dotfiles we're installing // For each file or directory within the dotfiles we're installing
for config_entry in fs::read_dir(&args.dotfiles_dir)? { for config_entry in fs::read_dir(&dotfiles)? {
let entry = config_entry?; let entry = config_entry?;
// Create full path to target config file (or directory) by push onto install path // Create full path to target config file (or directory) by push onto install path
config_target.push(entry.file_name()); config_target.push(entry.file_name());
@ -36,55 +52,116 @@ pub fn get_target_paths(args: & super::kcli::Cli) -> super::Result<HashMap<PathB
// If the entry doesn't already exist, insert it into the config_map // 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 // + Key is full path to source config from dotfiles repo we're installing
// + Value is desired full path to config at final install location // + 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()) config_map.entry(entry.path().to_owned())
.or_insert(config_target.to_owned()); .or_insert(config_target.to_owned());
// Reset config_target to be equal to requested install_dir // Reset config_target to be equal to requested install_dir
config_target.pop(); config_target.pop();
} }
Ok(config_map) return Ok(config_map);
}
/// 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
} }
/// Moves a single file from one location to another; Can be used to rename files /// 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 /// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided
pub fn move_file(src: & PathBuf, dst: & PathBuf, pub fn move_file(src: &PathBuf, dst: &PathBuf) -> super::Result<()> {
options: Option< & fs_extra::file::CopyOptions>) -> super::Result<()> { std::fs::copy(src, dst)?;
if options.is_none() { std::fs::remove_file(src)?;
// Default CopyOptions for moving files return Ok(());
let mut options = fs_extra::file::CopyOptions::new();
options.overwrite = false;
}
fs_extra::file::move_file(src, dst, options.unwrap())?;
Ok(())
} }
/// Moves a directory and all of it's contents recursively /// 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 /// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided
pub fn move_dir(src: & PathBuf, dst: & PathBuf, /// TODO: Implement this using std::fs to remove fs_extra dependency
options: Option< & fs_extra::dir::CopyOptions>) -> super::Result<()> { pub fn move_dir(src: &PathBuf, dst: &PathBuf,
if options.is_none() { options: Option<&fs_extra::dir::CopyOptions>)
-> super::Result<()> {
let copy_options = match options {
Some(opts) => opts.to_owned(),
None => {
// Default CopyOptions for moving directories // Default CopyOptions for moving directories
let mut opts = fs_extra::dir::CopyOptions::new();
opts.copy_inside = true;
opts.overwrite = false;
opts
}
};
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(); let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true; options.copy_inside = true;
options.overwrite = false; 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)?;
} }
fs_extra::dir::move_dir(src, dst, options.unwrap())?; return Ok(());
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 Normal file
View File

@ -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"));
}
}
}
/// Extracts repository name from URL
pub fn repo_name(repo_url: &str) -> String {
return repo_url.rsplit_once('/').unwrap().1
.strip_suffix(".git").unwrap().to_owned();
}

View File

@ -6,9 +6,6 @@
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## ## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################*/ ##############################################################################*/
// Allow use of kot::io::Result
pub use std::io::Result;
use std::io; use std::io;
// ============================================================================= // =============================================================================
@ -28,6 +25,6 @@ pub fn prompt(msg: String) -> bool {
"y" | "Y" => true, "y" | "Y" => true,
"n" | "N" => false, "n" | "N" => false,
// Handle garbage input // Handle garbage input
_ => prompt("Please enter y/n or Y/N\n".to_owned()), _ => prompt("Please enter Y/y or N/n\n".to_owned()),
} }
} }

View File

@ -6,7 +6,7 @@
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## ## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################*/ ##############################################################################*/
use std::path::PathBuf; use crate::kot::kerror::ErrorKind;
mod kot; mod kot;
@ -18,29 +18,10 @@ mod kot;
fn main() -> kot::Result<()> { fn main() -> kot::Result<()> {
// Call augmented kot::cli::from_args() to parse CLI arguments // Call augmented kot::cli::from_args() to parse CLI arguments
let args = kot::kcli::from_args()?; let mut args = kot::kcli::from_args()?;
// At this point all paths exist and have been converted to absolute paths // At this point all paths exist and have been converted to absolute paths
println!("args: {:?}\n", args); println!("args: {:?}\n", args);
// Attempt to install the configurations, checking for collisions // Apply CLI arguments and attempt to install dotfiles
match kot::install_configs(&args) { return kot::handle_args(&mut args);
Err(e) => {
// If there was an error, show the error type and run settings
println!(
"Error: {:?}\n+ Configs used: {:?}\n+ Install directory: {:?}\n",
e, args.dotfiles_dir, args.install_dir
);
// If we were forcing a backup and met some error, revert backups to last good state
// TODO: Isolate this to limit error scope to backup related functions
if args.force {
let mut temp_path : PathBuf = kot::kfs::Path::new("/tmp/").to_path_buf();
temp_path.push(args.backup_dir.file_name().unwrap());
kot::kfs::move_dir(&temp_path, &args.backup_dir, None)?;
}
},
_ => ()
}
// Configurations installed successfully
Ok(())
} }