From ed0baec0a3f953c99445f6842dadc5566e89cb75 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Thu, 8 Jul 2021 17:11:46 -0400 Subject: Initial commit --- .gitignore | 1 + Cargo.lock | 458 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + configs/depot.toml | 7 + configs/neovim/init.vim | 0 configs/neovim/lua/setup.lua | 0 dotup/Cargo.toml | 11 + dotup/src/archive.rs | 44 ++++ dotup/src/depot.rs | 371 +++++++++++++++++++++++++++++ dotup/src/error.rs | 30 +++ dotup/src/lib.rs | 16 ++ dotup/src/utils.rs | 96 ++++++++ dotup_cli/Cargo.toml | 20 ++ dotup_cli/src/.main.rs.rustfmt | 87 +++++++ dotup_cli/src/commands/init.rs | 20 ++ dotup_cli/src/commands/install.rs | 31 +++ dotup_cli/src/commands/link.rs | 181 ++++++++++++++ dotup_cli/src/commands/mod.rs | 11 + dotup_cli/src/commands/uninstall.rs | 31 +++ dotup_cli/src/commands/unlink.rs | 38 +++ dotup_cli/src/commands/utils.rs | 100 ++++++++ dotup_cli/src/config.rs | 6 + dotup_cli/src/main.rs | 87 +++++++ 23 files changed, 1648 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 configs/depot.toml create mode 100644 configs/neovim/init.vim create mode 100644 configs/neovim/lua/setup.lua create mode 100644 dotup/Cargo.toml create mode 100644 dotup/src/archive.rs create mode 100644 dotup/src/depot.rs create mode 100644 dotup/src/error.rs create mode 100644 dotup/src/lib.rs create mode 100644 dotup/src/utils.rs create mode 100644 dotup_cli/Cargo.toml create mode 100644 dotup_cli/src/.main.rs.rustfmt create mode 100644 dotup_cli/src/commands/init.rs create mode 100644 dotup_cli/src/commands/install.rs create mode 100644 dotup_cli/src/commands/link.rs create mode 100644 dotup_cli/src/commands/mod.rs create mode 100644 dotup_cli/src/commands/uninstall.rs create mode 100644 dotup_cli/src/commands/unlink.rs create mode 100644 dotup_cli/src/commands/utils.rs create mode 100644 dotup_cli/src/config.rs create mode 100644 dotup_cli/src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a981e03 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,458 @@ +# This file is automatically @generated by Cargo. +# 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 = "anyhow" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[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 = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotup" +version = "0.1.0" +dependencies = [ + "log", + "serde", + "slotmap", + "thiserror", + "toml", +] + +[[package]] +name = "dotup_cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "dotup", + "flexi_logger", + "log", +] + +[[package]] +name = "flexi_logger" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba2265890613939b533fa11c3728651531419ac549ccf527896201581f23991" +dependencies = [ + "atty", + "chrono", + "glob", + "lazy_static", + "log", + "regex", + "thiserror", + "yansi", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "os_str_bytes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "slotmap" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a952280edbecfb1d4bd3cf2dbc309dc6ab523e53487c438ae21a6df09fe84bc4" +dependencies = [ + "version_check", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yansi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..76e6a10 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["dotup", "dotup_cli"] diff --git a/configs/depot.toml b/configs/depot.toml new file mode 100644 index 0000000..a0bf689 --- /dev/null +++ b/configs/depot.toml @@ -0,0 +1,7 @@ +[[links]] +origin = 'neovim/init.vim' +destination = '.config/nvim/init.vim' + +[[links]] +origin = 'neovim/lua/setup.lua' +destination = '.config/nvim/lua/setup.lua' diff --git a/configs/neovim/init.vim b/configs/neovim/init.vim new file mode 100644 index 0000000..e69de29 diff --git a/configs/neovim/lua/setup.lua b/configs/neovim/lua/setup.lua new file mode 100644 index 0000000..e69de29 diff --git a/dotup/Cargo.toml b/dotup/Cargo.toml new file mode 100644 index 0000000..2545e94 --- /dev/null +++ b/dotup/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2018" +name = "dotup" +version = "0.1.0" + +[dependencies] +log = "*" +serde = { version = "*", features = ["derive"] } +thiserror = "*" +toml = "*" +slotmap = "*" diff --git a/dotup/src/archive.rs b/dotup/src/archive.rs new file mode 100644 index 0000000..ad6088d --- /dev/null +++ b/dotup/src/archive.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +use crate::internal_prelude::*; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ArchiveLink { + pub origin: PathBuf, + pub destination: PathBuf, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct Archive { + pub links: Vec, +} + +pub fn archive_exists(path: impl AsRef) -> bool { + utils::is_file(path).unwrap_or_default() +} + +pub fn archive_read(path: impl AsRef) -> Result { + let contents = std::fs::read_to_string(path)?; + archive_deserialize(contents) +} + +pub fn archive_write(path: impl AsRef, archive: &Archive) -> Result<()> { + let serialized = archive_serialize(archive)?; + std::fs::write(path, &serialized)?; + Ok(()) +} + +pub fn archive_serialize(archive: &Archive) -> Result { + match toml::to_string_pretty(archive) { + Ok(serialized) => Ok(serialized), + Err(e) => Err(Error::SerializationError(Box::new(e))), + } +} + +pub fn archive_deserialize(contents: impl AsRef) -> Result { + match toml::from_str(contents.as_ref()) { + Ok(archive) => Ok(archive), + Err(e) => Err(Error::SerializationError(Box::new(e))), + } +} diff --git a/dotup/src/depot.rs b/dotup/src/depot.rs new file mode 100644 index 0000000..6f18738 --- /dev/null +++ b/dotup/src/depot.rs @@ -0,0 +1,371 @@ +use slotmap::SlotMap; +use std::{ + fs::Metadata, + path::{Path, PathBuf}, + sync::Arc, +}; +use thiserror::Error; + +use crate::{internal_prelude::*, Archive, ArchiveLink}; + +#[derive(Debug)] +pub struct DepotConfig { + /// The archive used to initialize the depot. + /// A default archive can be create if one didnt already exist. + pub archive: Archive, + /// Path to the archive file. This path must be valid and must exist. + pub archive_path: PathBuf, +} + +slotmap::new_key_type! { pub struct LinkID; } + +#[derive(Debug)] +pub struct LinkDesc { + pub origin: PathBuf, + /// This must be a relative path + pub destination: PathBuf, +} + +impl LinkDesc { + pub fn new(origin: impl Into, destination: impl Into) -> Self { + Self { + origin: origin.into(), + destination: destination.into(), + } + } +} + +impl std::fmt::Display for LinkDesc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LinkDesc[{} -> {}]", + self.origin.display(), + self.destination.display() + ) + } +} + +impl From for LinkDesc { + fn from(archive_link: ArchiveLink) -> Self { + Self { + origin: archive_link.origin, + destination: archive_link.destination, + } + } +} + +#[derive(Debug)] +pub struct Link { + id: LinkID, + /// The origin path, when joined with the depot base path, must be valid and it point to a file that exists. + origin: PathBuf, + /// Canonical version of origin + origin_canonical: PathBuf, + /// The destination path has to be a relative path. + /// To install a link the destination path is joined with the + /// install path and the file at base path + origin path is linked + /// to this resulting destination path. + destination: PathBuf, +} + +impl Link { + pub fn id(&self) -> LinkID { + self.id + } + + /// The relative path to the origin file. Relative from depot folder. + pub fn origin(&self) -> &Path { + &self.origin + } + + pub fn origin_canonical(&self) -> &Path { + &self.origin_canonical + } + + /// The relative path to the install destination. + /// This path should be concatenated with an install destination to get the actual destination + /// for this link. + pub fn destination(&self) -> &Path { + &self.destination + } + + fn install_destination(&self, install_base: &Path) -> std::io::Result { + Ok(utils::weakly_canonical( + install_base.join(self.destination()), + )?) + } +} + +impl std::fmt::Display for Link { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Link[{} -> {}]", + self.origin().display(), + self.destination().display() + ) + } +} + +#[derive(Debug)] +struct DepotShared { + /// Must be canonical path + base_path: PathBuf, + /// Must be canonical path + archive_path: PathBuf, +} + +#[derive(Debug)] +pub struct Depot { + shared: Arc, + // Maps the origin to the link + links: SlotMap, +} + +impl Depot { + pub fn new(config: DepotConfig) -> Result { + depot_create(config) + } + + /// Creates a new link from the description. + /// The origin path must exist. + pub fn create_link(&mut self, link_desc: LinkDesc) -> Result { + let link = depot_create_link(self, link_desc)?; + let link_id = depot_insert_link(self, link); + Ok(link_id) + } + + pub fn get_link(&self, link_id: LinkID) -> Option<&Link> { + depot_get_link(self, link_id) + } + + pub fn remove_link(&mut self, link_id: LinkID) { + depot_remove_link(self, link_id) + } + + /// Archives this depot so it can be serialized + pub fn archive(&self) -> Archive { + depot_archive(self) + } + + pub fn links(&self) -> impl Iterator { + depot_links(self) + } + + pub fn install_link( + &self, + link: &Link, + install_base: impl AsRef, + ) -> Result<(), LinkInstallError> { + depot_install_link(self, link, install_base.as_ref()) + } + + pub fn uninstall_link(&self, link: &Link, install_base: impl AsRef) -> Result<()> { + depot_uninstall_link(self, link, install_base.as_ref()) + } + + pub fn base_path(&self) -> &Path { + &self.shared.base_path + } + + pub fn archive_path(&self) -> &Path { + &self.shared.archive_path + } +} + +fn depot_create(config: DepotConfig) -> Result { + let archive_path = match config.archive_path.canonicalize() { + Ok(canonicalized) => canonicalized, + Err(e) => return Err(Error::ArchiveMissing(config.archive_path, e)), + }; + if !archive_path.is_file() { + return Err(Error::ArchivePathNotFile(archive_path)); + } + let base_path = archive_path + .parent() + .expect("Failed to get parent of archive path") + .to_path_buf(); + + let depot_shared = DepotShared { + base_path, + archive_path, + }; + + let mut depot = Depot { + shared: Arc::new(depot_shared), + links: Default::default(), + }; + + for archive_link in config.archive.links { + let link_desc = LinkDesc::from(archive_link); + let link = depot_create_link(&depot, link_desc)?; + depot_insert_link(&mut depot, link); + } + + Ok(depot) +} + +fn depot_archive(depot: &Depot) -> Archive { + let mut links = Vec::new(); + + for link in depot_links(depot) { + let archive_link = link_to_archive_link(&link); + links.push(archive_link); + } + + Archive { links } +} + +/// Create a valid link for that given Depot using the given link desc. +/// The link id is corrected when the link is inserted in the depot. +fn depot_create_link(depot: &Depot, link_desc: LinkDesc) -> Result { + // link_ensure_relative_path(&link_desc.origin)?; + link_ensure_relative_path(&link_desc.destination)?; + debug_assert!(utils::is_canonical(&depot.base_path())?); + + let origin_joined = depot.base_path().join(&link_desc.origin); + let origin_result = origin_joined.canonicalize(); + let origin_canonical = match origin_result { + Ok(canonical) => canonical, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => { + return Err(Error::LinkOriginDoesntExist(origin_joined)) + } + _ => return Err(e.into()), + }, + }; + + if !origin_canonical.starts_with(depot.base_path()) { + return Err(Error::LinkOriginOutsideDepot { + depot_base: depot.base_path().to_path_buf(), + origin: origin_canonical, + }); + } + + // unwrap should be fine, this path starts with the prefix + let origin = origin_canonical + .strip_prefix(depot.base_path()) + .unwrap() + .to_path_buf(); + // let origin = origin_canonical; + let destination = link_desc.destination; + + Ok(Link { + id: Default::default(), + origin, + origin_canonical, + destination, + }) +} + +fn depot_get_link(depot: &Depot, link_id: LinkID) -> Option<&Link> { + depot.links.get(link_id) +} + +fn depot_remove_link(depot: &mut Depot, link_id: LinkID) { + depot.links.remove(link_id); +} + +#[derive(Debug, Error)] +pub enum LinkInstallError { + #[error(transparent)] + IOError(#[from] std::io::Error), + #[error("File already exists at {}", .0.display())] + FileExists(PathBuf, Metadata), + /// .0 = LinkPath , .1 = LinkDestination + #[error("Link already exists {} -> {}", .0.display(), .1.display())] + LinkExists(PathBuf, PathBuf), +} + +fn depot_install_link( + _depot: &Depot, + link: &Link, + install_base: &Path, +) -> Result<(), LinkInstallError> { + let final_origin = link.origin_canonical(); + let final_destination = link.install_destination(install_base)?; + + log::debug!("Final origin : {}", final_origin.display()); + log::debug!("Final destination : {}", final_destination.display()); + + if let Some(dest_base) = final_destination.parent() { + std::fs::create_dir_all(dest_base)?; + } + + // Exit early if there is some error or if the link already exists + match std::fs::symlink_metadata(&final_destination) { + Ok(metadata) => { + let filetype = metadata.file_type(); + if filetype.is_symlink() { + let symlink_destination = std::fs::read_link(&final_destination)?; + if symlink_destination == final_origin { + return Ok(()); + } + log::trace!( + "Symlink destinations where not equal : {} != {}", + final_origin.display(), + symlink_destination.display() + ); + return Err(LinkInstallError::LinkExists( + final_destination, + symlink_destination, + )); + } else { + return Err(LinkInstallError::FileExists(final_destination, metadata)); + } + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e.into()), + }; + + log::debug!( + "Creating symlink from {} to {}", + final_origin.display(), + final_destination.display() + ); + std::os::unix::fs::symlink(&final_origin, &final_destination)?; + + Ok(()) +} + +fn depot_uninstall_link(_depot: &Depot, link: &Link, install_base: &Path) -> Result<()> { + let origin_canonical = link.origin_canonical(); + let install_destination = link.install_destination(install_base)?; + let link_target = match std::fs::read_link(&install_destination) { + Ok(target) => target, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(e.into()), + }; + + if link_target.canonicalize()? == origin_canonical { + std::fs::remove_file(&install_destination)?; + } + + Ok(()) +} + +fn depot_insert_link(depot: &mut Depot, mut link: Link) -> LinkID { + depot.links.insert_with_key(move |k| { + link.id = k; + link + }) +} + +fn depot_links(depot: &Depot) -> impl Iterator { + depot.links.values() +} + +fn link_ensure_relative_path(path: &Path) -> Result<()> { + if !path.is_relative() { + return Err(Error::LinkPathIsNotRelative(path.to_path_buf())); + } + Ok(()) +} + +fn link_to_archive_link(depot_link: &Link) -> ArchiveLink { + ArchiveLink { + origin: depot_link.origin().to_path_buf(), + destination: depot_link.destination().to_path_buf(), + } +} diff --git a/dotup/src/error.rs b/dotup/src/error.rs new file mode 100644 index 0000000..2a6f241 --- /dev/null +++ b/dotup/src/error.rs @@ -0,0 +1,30 @@ +use crate::LinkInstallError; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Link origin is outside depot base\nDepot : {}\nLink : {}", .depot_base.display(), .origin.display())] + LinkOriginOutsideDepot { + depot_base: PathBuf, + origin: PathBuf, + }, + #[error("Link install error : {0}")] + LinkInstallError(#[from] LinkInstallError), + #[error("Link path is not relative : {}", .0.display())] + LinkPathIsNotRelative(PathBuf), + #[error("Link origin is not a file exist : {}", .0.display())] + LinkOriginIsNotFile(PathBuf), + #[error("Link origin doesnt exist : {}", .0.display())] + LinkOriginDoesntExist(PathBuf), + #[error("The archive path is not a file. It many not exist or there could be a permission's problem.")] + ArchivePathNotFile(PathBuf), + #[error("The archive path did not exist : {}\n{}", .0.display(), .1)] + ArchiveMissing(PathBuf, std::io::Error), + #[error("Deserialization error : {0}")] + SerializationError(Box), + #[error(transparent)] + IOError(#[from] std::io::Error), +} + +pub type Result = std::result::Result; diff --git a/dotup/src/lib.rs b/dotup/src/lib.rs new file mode 100644 index 0000000..6ef59e6 --- /dev/null +++ b/dotup/src/lib.rs @@ -0,0 +1,16 @@ +mod archive; +mod depot; +mod error; + +pub mod utils; + +pub use archive::{ + archive_deserialize, archive_exists, archive_read, archive_serialize, archive_write, Archive, + ArchiveLink, +}; +pub use depot::{Depot, DepotConfig, Link, LinkDesc, LinkID, LinkInstallError}; +pub use error::{Error, Result}; + +pub(crate) mod internal_prelude { + pub use super::{utils, Error, Result}; +} diff --git a/dotup/src/utils.rs b/dotup/src/utils.rs new file mode 100644 index 0000000..cfcab0f --- /dev/null +++ b/dotup/src/utils.rs @@ -0,0 +1,96 @@ +use std::path::{Component, Path, PathBuf}; + +use crate::internal_prelude::*; + +pub fn is_file(path: impl AsRef) -> Result { + let metadata = match std::fs::metadata(path) { + Ok(metadata) => metadata, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => return Ok(false), + _ => return Err(e.into()), + }, + }; + Ok(metadata.is_file()) +} + +pub fn is_directory(path: impl AsRef) -> Result { + let metadata = match std::fs::metadata(path) { + Ok(metadata) => metadata, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => return Ok(false), + _ => return Err(e.into()), + }, + }; + Ok(metadata.is_dir()) +} + +pub fn is_canonical(path: &Path) -> Result { + Ok(path == path.canonicalize()?.as_path()) +} + +pub fn weakly_canonical(path: impl AsRef) -> std::io::Result { + let cwd = std::env::current_dir()?; + Ok(weakly_canonical_cwd(path, cwd)) +} + +fn weakly_canonical_cwd(path: impl AsRef, cwd: PathBuf) -> PathBuf { + // Adapated from + // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 + let path = path.as_ref(); + + let mut components = path.components().peekable(); + let mut canonical = cwd; + let prefix = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(_) => unreachable!(), + Component::RootDir => { + canonical = prefix.clone(); + canonical.push(component.as_os_str()) + } + Component::CurDir => {} + Component::ParentDir => { + canonical.pop(); + } + Component::Normal(p) => canonical.push(p), + }; + } + + canonical +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn weak_canonical_test() { + let cwd = PathBuf::from("/home/user"); + assert_eq!( + PathBuf::from("/home/dest"), + weakly_canonical_cwd("../dest", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/home/dest/configs/init.vim"), + weakly_canonical_cwd("../dest/configs/init.vim", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/dest/configs/init.vim"), + weakly_canonical_cwd("/dest/configs/init.vim", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), + weakly_canonical_cwd("./configs/nvim/lua/setup.lua", cwd.clone()) + ); + assert_eq!( + PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), + weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd.clone()) + ); + } +} diff --git a/dotup_cli/Cargo.toml b/dotup_cli/Cargo.toml new file mode 100644 index 0000000..891d32a --- /dev/null +++ b/dotup_cli/Cargo.toml @@ -0,0 +1,20 @@ +[package] +edition = "2018" +name = "dotup_cli" +version = "0.1.0" + +[[bin]] +name = "dotup" +path = "src/main.rs" + +[dependencies] +anyhow = "*" +clap = "3.0.0-beta.2" +log = "*" + +[dependencies.dotup] +path = "../dotup" + +[dependencies.flexi_logger] +features = ["colors"] +version = "*" diff --git a/dotup_cli/src/.main.rs.rustfmt b/dotup_cli/src/.main.rs.rustfmt new file mode 100644 index 0000000..6d020e1 --- /dev/null +++ b/dotup_cli/src/.main.rs.rustfmt @@ -0,0 +1,87 @@ +#![allow(unused)] + +pub mod commands; +pub mod config; + +pub use config::Config; + +pub mod prelude { + pub use super::Config; + pub use dotup::{Archive, Depot, DepotConfig, Link, LinkDesc, LinkID}; +} + +use clap::{AppSettings, Clap}; +use flexi_logger::Logger; +use std::{ + collections::HashMap, + iter::FromIterator, + path::{Path, PathBuf}, +}; + +use prelude::*; + +const DEFAULT_DEPOT_NAME: &str = "depot.toml"; + +#[derive(Clap)] +#[clap(version = "0.1", author = "d464")] +#[clap(setting = AppSettings::ColoredHelp)] +struct Opts { + /// Path to the depot file. + #[clap(long, default_value = DEFAULT_DEPOT_NAME)] + depot: PathBuf, + + /// Disable output to the console + #[clap(short, long)] + quiet: bool, + + /// A level of verbosity, and can be used multiple times + /// + /// Level 0 - Warnings (Default) + /// Level 1 - Info + /// Level 2 - Debug + /// Level 3 - Trace + #[clap(short, long, parse(from_occurrences))] + verbose: i32, + + #[clap(subcommand)] + subcmd: SubCommand, +} + +#[derive(Clap)] +enum SubCommand { + Init(commands::init::Opts), + Link(commands::link::Opts), + Unlink(commands::unlink::Opts), + Install(commands::install::Opts), + Uninstall(commands::uninstall::Opts), +} + +fn main() -> anyhow::Result<()> { + let opts = Opts::parse(); + + if !opts.quiet { + let log_level = match opts.verbose { + 0 => "warn", + 1 => "info", + 2 => "debug", + 3 | _ => "trace", + }; + + Logger::try_with_env_or_str(log_level)? + .format(flexi_logger::colored_default_format) + //.set_palette("196;208;32;198;15".to_string()) + .start()?; + } + + let config = Config { + archive_path: opts.depot, + }; + + match opts.subcmd { + SubCommand::Init(opts) => commands::init::main(config, opts), + SubCommand::Link(opts) => commands::link::main(config, opts), + SubCommand::Unlink(opts) => commands::unlink::main(config, opts), + SubCommand::Install(opts) => commands::install::main(config, opts), + SubCommand::Uninstall(opts) => commands::uninstall::main(config, opts), + } +} diff --git a/dotup_cli/src/commands/init.rs b/dotup_cli/src/commands/init.rs new file mode 100644 index 0000000..bfec6ca --- /dev/null +++ b/dotup_cli/src/commands/init.rs @@ -0,0 +1,20 @@ +use clap::Clap; + +use super::prelude::*; + +#[derive(Clap)] +pub struct Opts {} + +pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { + if !dotup::utils::is_file(&config.archive_path)? { + let archive = Archive::default(); + log::info!("Creating archive"); + utils::write_archive(&config.archive_path, &archive)?; + } else { + log::info!( + "Archive file already exists : {}", + config.archive_path.display() + ); + } + Ok(()) +} diff --git a/dotup_cli/src/commands/install.rs b/dotup_cli/src/commands/install.rs new file mode 100644 index 0000000..72cabf3 --- /dev/null +++ b/dotup_cli/src/commands/install.rs @@ -0,0 +1,31 @@ +use clap::Clap; +use std::path::PathBuf; + +use super::prelude::*; + +#[derive(Clap)] +pub struct Opts { + /// The location where links will be installed to. + /// Defaults to home directory. + #[clap(long)] + install_base: Option, + + /// The files/directories to install + #[clap(required = true, min_values = 1)] + paths: Vec, +} + +pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { + let install_base = match opts.install_base { + Some(path) => path, + None => utils::home_directory()?, + }; + let depot = utils::read_depot(&config.archive_path)?; + + for link in utils::collect_links_by_base_paths(&depot, &opts.paths) { + log::info!("Installing link {}", link); + depot.install_link(link, &install_base)?; + } + + Ok(()) +} diff --git a/dotup_cli/src/commands/link.rs b/dotup_cli/src/commands/link.rs new file mode 100644 index 0000000..fd99253 --- /dev/null +++ b/dotup_cli/src/commands/link.rs @@ -0,0 +1,181 @@ +use clap::Clap; +use std::{ + fs::{DirEntry, Metadata}, + path::{Path, PathBuf}, +}; + +use super::prelude::*; + +#[derive(Clap)] +pub struct Opts { + #[clap(long)] + directory: bool, + + paths: Vec, +} + +/* + config/ + nvim/ + init.vim + lua/ + setup.lua + bash/ + .bashrc + + link nvim .config/nvim + nvim/init.vim -> .config/nvim/init.vim + nvim/lua/setup.lua -> config/nvim/lua/setup.lua + + link bash . + bash/.bashrc -> ./.bashrc + + link --directory scripts .scripts + scripts/ -> ./.scripts +*/ + +pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { + let mut depot = utils::read_depot(&config.archive_path)?; + + let (origins, destination) = match opts.paths.as_slice() { + p @ [] | p @ [_] => (p, None), + [o @ .., dest] => (o, Some(dest)), + _ => unreachable!(), + }; + + if let Some(destination) = destination { + for path in collect_file_type(origins, FileType::File)? { + let link_desc = LinkDesc { + origin: path, + destination: destination.clone(), + }; + log::info!("Creating link : {}", link_desc); + depot.create_link(link_desc)?; + } + } else { + let base_path = match origins { + [] => std::env::current_dir()?, + [path] => path.clone(), + _ => unreachable!(), + }; + + for link in utils::collect_links_by_base_paths(&depot, std::iter::once(base_path)) { + log::info!("{}", link); + } + } + + //if let Some(destination) = destination { + // for origin in origins { + // let origin_canonical = origin.canonicalize()?; + // let base = if origin_canonical.is_file() { + // origin_canonical.parent().unwrap().to_path_buf() + // } else { + // origin_canonical.to_path_buf() + // }; + + // link(&mut depot, origin.as_path(), destination.as_path(), &base)?; + // } + //} else { + // log::warn!("Missing destination"); + //} + + utils::write_depot(&depot)?; + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum FileType { + File, + Directory, +} + +/// Collects canonical files of the given type starting from, and including, entry_paths +fn collect_file_type( + entry_paths: impl IntoIterator>, + collect_type: FileType, +) -> anyhow::Result> { + let entry_paths: Vec = entry_paths + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .collect(); + let mut collected = Vec::new(); + let mut pending: Vec<_> = entry_paths.iter().cloned().filter(|p| p.is_dir()).collect(); + + for path in entry_paths { + let path = path.canonicalize()?; + if (path.is_file() && collect_type == FileType::File) + || (path.is_dir() && collect_type == FileType::Directory) + { + collected.push(path); + } + } + + while let Some(dir_path) = pending.pop() { + for entry in dir_path.read_dir()? { + let entry = entry?; + let filetype = entry.file_type()?; + + if filetype.is_file() && collect_type == FileType::File { + collected.push(entry.path()); + } else if filetype.is_dir() { + if collect_type == FileType::Directory { + collected.push(entry.path()); + } + pending.push(entry.path()); + } + } + } + + Ok(collected) +} + +fn link(depot: &mut Depot, origin: &Path, destination: &Path, base: &Path) -> anyhow::Result<()> { + let metadata = std::fs::metadata(origin)?; + if metadata.is_file() { + link_file(depot, origin, destination, base)?; + } else if metadata.is_dir() { + link_directory_recursive(depot, origin, destination, base)?; + } else { + unimplemented!() + } + Ok(()) +} + +fn link_file( + depot: &mut Depot, + origin: &Path, + destination: &Path, + base: &Path, +) -> anyhow::Result<()> { + let origin_canonical = origin + .canonicalize() + .expect("Failed to canonicalize origin path"); + let partial = origin_canonical + .strip_prefix(base) + .expect("Failed to remove prefix from origin path"); + let destination = destination.join(partial); + + let link_desc = LinkDesc { + origin: origin_canonical, + destination, + }; + + log::debug!("Linking file {:#?}", link_desc); + depot.create_link(link_desc)?; + + Ok(()) +} + +fn link_directory_recursive( + depot: &mut Depot, + dir_path: &Path, + destination: &Path, + base: &Path, +) -> anyhow::Result<()> { + for origin in dir_path.read_dir()? { + let origin = origin?.path(); + link(depot, &origin, destination, base)?; + } + Ok(()) +} diff --git a/dotup_cli/src/commands/mod.rs b/dotup_cli/src/commands/mod.rs new file mode 100644 index 0000000..f372662 --- /dev/null +++ b/dotup_cli/src/commands/mod.rs @@ -0,0 +1,11 @@ +pub mod init; +pub mod install; +pub mod link; +pub mod uninstall; +pub mod unlink; +pub mod utils; + +mod prelude { + pub use super::utils; + pub use crate::prelude::*; +} diff --git a/dotup_cli/src/commands/uninstall.rs b/dotup_cli/src/commands/uninstall.rs new file mode 100644 index 0000000..dad55a5 --- /dev/null +++ b/dotup_cli/src/commands/uninstall.rs @@ -0,0 +1,31 @@ +use clap::Clap; +use std::path::PathBuf; + +use super::prelude::*; + +#[derive(Clap)] +pub struct Opts { + /// The location where links will be uninstalled from. + /// Defaults to home directory. + #[clap(long)] + install_base: Option, + + /// The files/directories to uninstall + #[clap(required = true, min_values = 1)] + paths: Vec, +} + +pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { + let install_base = match opts.install_base { + Some(path) => path, + None => utils::home_directory()?, + }; + let depot = utils::read_depot(&config.archive_path)?; + + for link in utils::collect_links_by_base_paths(&depot, &opts.paths) { + log::info!("Uninstalling link : {}", link); + depot.uninstall_link(link, &install_base)?; + } + + Ok(()) +} diff --git a/dotup_cli/src/commands/unlink.rs b/dotup_cli/src/commands/unlink.rs new file mode 100644 index 0000000..7ebb19c --- /dev/null +++ b/dotup_cli/src/commands/unlink.rs @@ -0,0 +1,38 @@ +use clap::Clap; +use std::{ + fs::{DirEntry, Metadata}, + path::{Path, PathBuf}, +}; + +use super::prelude::*; + +#[derive(Clap)] +pub struct Opts { + /// Specifies the install base if the links are also to be uninstalled. + #[clap(long)] + uninstall: Option, + + #[clap(required = true, min_values = 1)] + paths: Vec, +} + +pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { + let mut depot = utils::read_depot(&config.archive_path)?; + + for link_id in utils::collect_link_ids_by_base_paths(&depot, &opts.paths) { + let link = depot.get_link(link_id).unwrap(); + log::info!( + "Unlinking(uninstall = {}) : {}", + opts.uninstall.is_some(), + link + ); + if let Some(ref install_base) = opts.uninstall { + depot.uninstall_link(link, &install_base)?; + } + depot.remove_link(link_id); + } + + utils::write_depot(&depot)?; + + Ok(()) +} diff --git a/dotup_cli/src/commands/utils.rs b/dotup_cli/src/commands/utils.rs new file mode 100644 index 0000000..4160078 --- /dev/null +++ b/dotup_cli/src/commands/utils.rs @@ -0,0 +1,100 @@ +use std::path::{Path, PathBuf}; + +use crate::prelude::*; + +pub fn home_directory() -> anyhow::Result { + match std::env::var("HOME") { + Ok(val) => Ok(PathBuf::from(val)), + Err(e) => { + log::error!("Failed to get home directory from enviornment variable"); + Err(e.into()) + } + } +} + +pub fn write_archive(path: impl AsRef, archive: &Archive) -> anyhow::Result<()> { + let path = path.as_ref(); + log::debug!("Writing archive to {}", path.display()); + match dotup::archive_write(path, archive) { + Ok(_) => Ok(()), + Err(e) => { + log::error!( + "Failed to write archive to : {}\nError : {}", + path.display(), + e + ); + Err(e.into()) + } + } +} + +pub fn write_depot(depot: &Depot) -> anyhow::Result<()> { + let write_path = depot.archive_path(); + let archive = depot.archive(); + match dotup::archive_write(write_path, &archive) { + Ok(_) => Ok(()), + Err(e) => { + log::error!( + "Failed to write depot archive to : {}\nError : {}", + write_path.display(), + e + ); + Err(e.into()) + } + } +} + +pub fn read_archive(path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + match dotup::archive_read(path) { + Ok(archive) => Ok(archive), + Err(e) => { + log::error!( + "Failed to read archive from : {}\nError : {}", + path.display(), + e + ); + Err(e.into()) + } + } +} + +pub fn read_depot(archive_path: impl AsRef) -> anyhow::Result { + let archive_path = archive_path.as_ref().to_path_buf(); + let archive = read_archive(&archive_path)?; + let depot_config = DepotConfig { + archive_path, + archive, + }; + let depot = Depot::new(depot_config)?; + Ok(depot) +} + +pub fn collect_links_by_base_paths( + depot: &Depot, + paths: impl IntoIterator>, +) -> Vec<&Link> { + let canonical_paths: Vec<_> = paths + .into_iter() + .map(|p| p.as_ref().canonicalize().unwrap()) + .collect(); + + depot + .links() + .filter(|&l| { + canonical_paths + .iter() + .any(|p| l.origin_canonical().starts_with(p)) + }) + .collect() +} + +pub fn collect_link_ids_by_base_paths( + depot: &Depot, + paths: impl IntoIterator>, +) -> Vec { + collect_links_by_base_paths(depot, paths) + .into_iter() + .map(|l| l.id()) + .collect() +} diff --git a/dotup_cli/src/config.rs b/dotup_cli/src/config.rs new file mode 100644 index 0000000..ac4fc66 --- /dev/null +++ b/dotup_cli/src/config.rs @@ -0,0 +1,6 @@ +use std::path::PathBuf; + +#[derive(Debug)] +pub struct Config { + pub archive_path: PathBuf, +} diff --git a/dotup_cli/src/main.rs b/dotup_cli/src/main.rs new file mode 100644 index 0000000..6d020e1 --- /dev/null +++ b/dotup_cli/src/main.rs @@ -0,0 +1,87 @@ +#![allow(unused)] + +pub mod commands; +pub mod config; + +pub use config::Config; + +pub mod prelude { + pub use super::Config; + pub use dotup::{Archive, Depot, DepotConfig, Link, LinkDesc, LinkID}; +} + +use clap::{AppSettings, Clap}; +use flexi_logger::Logger; +use std::{ + collections::HashMap, + iter::FromIterator, + path::{Path, PathBuf}, +}; + +use prelude::*; + +const DEFAULT_DEPOT_NAME: &str = "depot.toml"; + +#[derive(Clap)] +#[clap(version = "0.1", author = "d464")] +#[clap(setting = AppSettings::ColoredHelp)] +struct Opts { + /// Path to the depot file. + #[clap(long, default_value = DEFAULT_DEPOT_NAME)] + depot: PathBuf, + + /// Disable output to the console + #[clap(short, long)] + quiet: bool, + + /// A level of verbosity, and can be used multiple times + /// + /// Level 0 - Warnings (Default) + /// Level 1 - Info + /// Level 2 - Debug + /// Level 3 - Trace + #[clap(short, long, parse(from_occurrences))] + verbose: i32, + + #[clap(subcommand)] + subcmd: SubCommand, +} + +#[derive(Clap)] +enum SubCommand { + Init(commands::init::Opts), + Link(commands::link::Opts), + Unlink(commands::unlink::Opts), + Install(commands::install::Opts), + Uninstall(commands::uninstall::Opts), +} + +fn main() -> anyhow::Result<()> { + let opts = Opts::parse(); + + if !opts.quiet { + let log_level = match opts.verbose { + 0 => "warn", + 1 => "info", + 2 => "debug", + 3 | _ => "trace", + }; + + Logger::try_with_env_or_str(log_level)? + .format(flexi_logger::colored_default_format) + //.set_palette("196;208;32;198;15".to_string()) + .start()?; + } + + let config = Config { + archive_path: opts.depot, + }; + + match opts.subcmd { + SubCommand::Init(opts) => commands::init::main(config, opts), + SubCommand::Link(opts) => commands::link::main(config, opts), + SubCommand::Unlink(opts) => commands::unlink::main(config, opts), + SubCommand::Install(opts) => commands::install::main(config, opts), + SubCommand::Uninstall(opts) => commands::uninstall::main(config, opts), + } +} -- cgit