From 472cf96ba0b21f3530b62ffc887017a2dabf014b Mon Sep 17 00:00:00 2001 From: diogo464 Date: Tue, 8 Feb 2022 09:49:16 +0000 Subject: split code into different files --- src/depot.rs | 850 ++++++++++++++++++++++++++++++++ src/dotup.rs | 533 ++++++++++++++++++++ src/main.rs | 1561 +--------------------------------------------------------- src/utils.rs | 156 ++++++ 4 files changed, 1542 insertions(+), 1558 deletions(-) create mode 100644 src/depot.rs create mode 100644 src/dotup.rs create mode 100644 src/utils.rs (limited to 'src') diff --git a/src/depot.rs b/src/depot.rs new file mode 100644 index 0000000..b2d4e3c --- /dev/null +++ b/src/depot.rs @@ -0,0 +1,850 @@ +use anyhow::Context; +use std::{ + collections::HashSet, + ffi::{OsStr, OsString}, + ops::Index, + path::{Path, PathBuf}, +}; +use thiserror::Error; + +use slotmap::{Key, SlotMap}; + +//pub type Result = std::result::Result; +pub use anyhow::Result; +pub use disk::{read, write}; + +slotmap::new_key_type! {pub struct LinkID;} +slotmap::new_key_type! {struct NodeID;} + +#[derive(Debug, Error)] +enum DepotError { + #[error("path must be relative")] + InvalidPath, + #[error("path must be relative and not empty")] + InvalidLinkPath, +} + +#[derive(Debug, Clone)] +struct Node { + comp: OsString, + parent: NodeID, + kind: NodeKind, +} + +#[derive(Debug, Clone)] +enum NodeKind { + Link(LinkID), + Directory(HashSet), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum NodeSearchResult { + Found(NodeID), + /// the closest NodeID up the the search point. + NotFound(NodeID), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DirNode { + Link(LinkID), + Directory(PathBuf), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SearchResult { + Found(LinkID), + Ancestor(LinkID), + NotFound, +} + +#[derive(Debug, Clone)] +struct Link { + origin: PathBuf, + destination: PathBuf, + origin_id: NodeID, +} + +#[derive(Debug)] +pub struct LinkView<'a> { + link_id: LinkID, + depot: &'a Depot, +} + +impl<'a> LinkView<'a> { + pub fn origin(&self) -> &Path { + &self.depot.links[self.link_id].origin + } + + pub fn destination(&self) -> &Path { + &self.depot.links[self.link_id].destination + } +} + +#[derive(Debug, Clone)] +struct DepotTree { + root: NodeID, + nodes: SlotMap, +} + +impl Default for DepotTree { + fn default() -> Self { + let mut nodes = SlotMap::::default(); + let root = nodes.insert(Node { + comp: Default::default(), + parent: Default::default(), + kind: NodeKind::Directory(Default::default()), + }); + Self { root, nodes } + } +} + +impl Index for DepotTree { + type Output = Node; + + fn index(&self, index: NodeID) -> &Self::Output { + self.nodes.index(index) + } +} + +impl DepotTree { + /// create a node of kind [`NodeKind::Link`]. + pub fn link_create(&mut self, path: &Path, link_id: LinkID) -> Result { + debug_assert!(path_verify_link(path).is_ok()); + + let path_search_result = self.search(path); + + // handle the error cases + match path_search_result { + NodeSearchResult::Found(node_id) => { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(_) => Err(anyhow::anyhow!("link already exists")), + NodeKind::Directory(_) => { + Err(anyhow::anyhow!("path already has links under it")) + } + } + } + NodeSearchResult::NotFound(ancestor_node_id) => { + let ancestor_node = &self.nodes[ancestor_node_id]; + match &ancestor_node.kind { + NodeKind::Link(_) => Err(anyhow::anyhow!( + "an ancestor of this path is already linked" + )), + NodeKind::Directory(_) => Ok(()), + } + } + }?; + + // create the node + // unwrap: this is a verfied link path, it must have atleast one component + let filename = path.file_name().unwrap(); + let parent_path = path_parent_or_empty(path); + let node_id = self.nodes.insert(Node { + comp: filename.to_owned(), + parent: Default::default(), + kind: NodeKind::Link(link_id), + }); + let parent_id = self.directory_get_or_create(parent_path, node_id); + self.nodes[node_id].parent = parent_id; + Ok(node_id) + } + + pub fn link_update_id(&mut self, node_id: NodeID, link_id: LinkID) { + let node = &mut self.nodes[node_id]; + match &mut node.kind { + NodeKind::Link(lid) => *lid = link_id, + NodeKind::Directory(_) => unreachable!(), + } + } + + /// attempts to moves a node of kind [`NodeKind::Link`] to `destination`. + pub fn link_move(&mut self, node_id: NodeID, destination: &Path) -> Result<()> { + let parent_id = self.nodes[node_id].parent; + let parent = &mut self.nodes[parent_id]; + + // remove the node from its parent temporarily so that the search never returns this + // link and that way any link will find means an error. + // if an error does happen then we re-add this node to its parent to keep the data + // consistent. + match &mut parent.kind { + NodeKind::Link(_) => unreachable!(), + NodeKind::Directory(children) => children.remove(&node_id), + }; + + let search_result = self.search(destination); + // handle the error cases + match search_result { + NodeSearchResult::Found(found_id) => { + assert!(found_id != node_id); + self.directory_add_child(parent_id, node_id); + return Err(anyhow::anyhow!("link already exists at that path")); + } + NodeSearchResult::NotFound(ancestor_id) => { + let ancestor = &self.nodes[ancestor_id]; + match &ancestor.kind { + NodeKind::Link(_) => { + self.directory_add_child(parent_id, node_id); + return Err(anyhow::anyhow!("ancestor path is already linked")); + } + NodeKind::Directory(_) => {} + } + } + }; + + let destination_parent = path_parent_or_empty(destination); + let new_parent_id = self.directory_get_or_create(destination_parent, node_id); + if new_parent_id != parent_id { + self.nodes[node_id].parent = new_parent_id; + + // we have to re-add and call the remove function because it could lead to the removal + // of several directories if they become empty after this remove. + self.directory_add_child(parent_id, node_id); + self.directory_remove_child(parent_id, node_id); + } + + // unwrap: destination is a verified link path so it has atleast 1 component + let comp = destination.file_name().unwrap(); + let node = &mut self.nodes[node_id]; + if node.comp != comp { + node.comp = comp.to_owned(); + } + + Ok(()) + } + + pub fn link_search(&self, path: &Path) -> SearchResult { + match self.search(path) { + NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { + NodeKind::Link(link_id) => SearchResult::Found(*link_id), + NodeKind::Directory(_) => SearchResult::NotFound, + }, + NodeSearchResult::NotFound(node_id) => match &self.nodes[node_id].kind { + NodeKind::Link(link_id) => SearchResult::Ancestor(*link_id), + NodeKind::Directory(_) => SearchResult::NotFound, + }, + } + } + + /// remove a node of kind [`NodeKind::Link`]. + pub fn link_remove(&mut self, node_id: NodeID) { + let node = &self.nodes[node_id]; + assert!(std::matches!(node.kind, NodeKind::Link(_))); + let parent_id = node.parent; + self.nodes.remove(node_id); + self.directory_remove_child(parent_id, node_id); + } + + pub fn links_under(&self, path: &Path) -> impl Iterator + '_ { + let links = match self.search(path) { + NodeSearchResult::Found(node_id) => { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(link_id) => vec![*link_id], + NodeKind::Directory(children) => { + let mut links = Vec::new(); + let mut node_ids = Vec::from_iter(children.iter().copied()); + while let Some(child_id) = node_ids.pop() { + let child = &self.nodes[child_id]; + match &child.kind { + NodeKind::Link(link_id) => links.push(*link_id), + NodeKind::Directory(extra_children) => { + node_ids.extend(extra_children.iter().copied()) + } + } + } + links + } + } + } + NodeSearchResult::NotFound(_) => vec![], + }; + links.into_iter() + } + + pub fn has_links_under(&self, path: &Path) -> bool { + // it does not matter what type of node is found. if a directory exists then there + // must be atleast one link under it. + match self.search(path) { + NodeSearchResult::Found(_) => true, + NodeSearchResult::NotFound(_) => false, + } + } + + pub fn read_dir(&self, path: &Path) -> Result + '_> { + match self.search(path) { + NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { + NodeKind::Link(_) => Err(anyhow::anyhow!("read dir called on a link")), + NodeKind::Directory(children) => Ok(children.iter().map(|child_id| { + let child = &self.nodes[*child_id]; + match &child.kind { + NodeKind::Link(link_id) => DirNode::Link(*link_id), + NodeKind::Directory(_) => DirNode::Directory(self.build_path(*child_id)), + } + })), + }, + NodeSearchResult::NotFound(_) => Err(anyhow::anyhow!("directory not found")), + } + } + + pub fn build_path(&self, node_id: NodeID) -> PathBuf { + fn recursive_helper(nodes: &SlotMap, nid: NodeID, pbuf: &mut PathBuf) { + if nid.is_null() { + return; + } + let parent_id = nodes[nid].parent; + recursive_helper(nodes, parent_id, pbuf); + pbuf.push(&nodes[nid].comp); + } + + let mut node_path = PathBuf::default(); + recursive_helper(&self.nodes, node_id, &mut node_path); + node_path + } + + fn search(&self, path: &Path) -> NodeSearchResult { + debug_assert!(path_verify(path).is_ok()); + + let mut curr_node_id = self.root; + let mut comp_iter = path_iter_comps(path).peekable(); + while let Some(comp) = comp_iter.next() { + if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { + let child = &self.nodes[child_id]; + match &child.kind { + NodeKind::Link(_) => { + if comp_iter.peek().is_some() { + return NodeSearchResult::NotFound(child_id); + } else { + return NodeSearchResult::Found(child_id); + } + } + NodeKind::Directory(_) => curr_node_id = child_id, + } + } else { + return NodeSearchResult::NotFound(curr_node_id); + } + } + NodeSearchResult::Found(curr_node_id) + } + + // creates directories all the way up to and including path. + // there cannot be any links up to `path`. + fn directory_get_or_create(&mut self, path: &Path, initial_child: NodeID) -> NodeID { + // TODO: this could be replaced if the search function also returned the depth of the + // node and we skip those components and just start creating directories up to the + // path. + let mut curr_node_id = self.root; + for comp in path_iter_comps(path) { + if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { + debug_assert!(std::matches!( + self.nodes[child_id].kind, + NodeKind::Directory(_) + )); + curr_node_id = child_id; + } else { + let new_node_id = self.nodes.insert(Node { + comp: comp.to_owned(), + parent: curr_node_id, + kind: NodeKind::Directory(Default::default()), + }); + self.directory_add_child(curr_node_id, new_node_id); + curr_node_id = new_node_id; + } + } + self.directory_add_child(curr_node_id, initial_child); + curr_node_id + } + + fn directory_search_children(&self, node_id: NodeID, comp: &OsStr) -> Option { + let node = &self.nodes[node_id]; + match &node.kind { + NodeKind::Link(_) => unreachable!(), + NodeKind::Directory(children) => { + for &child_id in children { + let child = &self.nodes[child_id]; + if child.comp == comp { + return Some(child_id); + } + } + } + } + None + } + + fn directory_add_child(&mut self, node_id: NodeID, child_id: NodeID) { + let node = &mut self.nodes[node_id]; + match &mut node.kind { + NodeKind::Link(_) => unreachable!(), + NodeKind::Directory(children) => children.insert(child_id), + }; + } + + fn directory_remove_child(&mut self, node_id: NodeID, child_id: NodeID) { + let node = &mut self.nodes[node_id]; + match &mut node.kind { + NodeKind::Link(_) => unreachable!(), + NodeKind::Directory(children) => { + children.remove(&child_id); + if children.is_empty() && !node.parent.is_null() { + let parent_id = node.parent; + self.directory_remove_child(parent_id, node_id); + } + } + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct Depot { + links: SlotMap, + origin: DepotTree, +} + +impl Depot { + pub fn link_create( + &mut self, + origin: impl AsRef, + destination: impl AsRef, + ) -> Result { + let origin = origin.as_ref(); + let destination = destination.as_ref(); + path_verify_link(origin)?; + path_verify_link(destination)?; + self.link_create_unchecked(origin, destination) + } + + pub fn link_remove(&mut self, link_id: LinkID) { + let node_id = self.links[link_id].origin_id; + self.links.remove(link_id); + self.origin.link_remove(node_id); + } + + /// moves the link specified by `link_id` to the path at `destination`. + /// if the link is already at the destination nothing is done. + /// if the destination is another link that that link is removed. + /// if the destination is under another link then an error is returned. + /// `destination` will be the link's new origin. + pub fn link_move(&mut self, link_id: LinkID, destination: impl AsRef) -> Result<()> { + let destination = destination.as_ref(); + path_verify_link(destination)?; + self.link_move_unchecked(link_id, destination) + } + + #[allow(unused)] + pub fn link_search(&self, path: impl AsRef) -> Result { + let path = path.as_ref(); + path_verify(path)?; + Ok(self.link_search_unchecked(path)) + } + + pub fn link_find(&self, path: impl AsRef) -> Result> { + let path = path.as_ref(); + path_verify(path)?; + Ok(self.link_find_unchecked(path)) + } + + pub fn links_under(&self, path: impl AsRef) -> Result + '_> { + let path = path.as_ref(); + path_verify(path)?; + Ok(self.links_under_unchecked(path)) + } + + pub fn has_links_under(&self, path: impl AsRef) -> Result { + let path = path.as_ref(); + path_verify(path)?; + Ok(self.has_links_under_unchecked(path)) + } + + pub fn links_verify_install(&self, link_ids: impl Iterator) -> Result<()> { + let mut destination = DepotTree::default(); + for link_id in link_ids { + let link = &self.links[link_id]; + destination + .link_create(&link.destination, link_id) + .context("link destinations overlap")?; + } + Ok(()) + } + + pub fn link_view(&self, link_id: LinkID) -> LinkView { + LinkView { + link_id, + depot: self, + } + } + + pub fn read_dir(&self, path: impl AsRef) -> Result + '_> { + let path = path.as_ref(); + path_verify(path)?; + self.read_dir_unchecked(path) + } + + fn link_create_unchecked(&mut self, origin: &Path, destination: &Path) -> Result { + let node_id = self.origin.link_create(origin, Default::default())?; + let link_id = self.links.insert(Link { + origin: origin.to_owned(), + destination: destination.to_owned(), + origin_id: node_id, + }); + self.origin.link_update_id(node_id, link_id); + Ok(link_id) + } + + fn link_move_unchecked(&mut self, link_id: LinkID, destination: &Path) -> Result<()> { + let link = &self.links[link_id]; + if link.origin == destination { + return Ok(()); + } + let node_id = link.origin_id; + self.origin.link_move(node_id, destination)?; + self.links[link_id].origin = destination.to_owned(); + Ok(()) + } + + fn link_search_unchecked(&self, path: &Path) -> SearchResult { + self.origin.link_search(path) + } + + fn link_find_unchecked(&self, path: &Path) -> Option { + match self.link_search_unchecked(path) { + SearchResult::Found(link_id) => Some(link_id), + _ => None, + } + } + + fn links_under_unchecked(&self, path: &Path) -> impl Iterator + '_ { + self.origin.links_under(path) + } + + fn has_links_under_unchecked(&self, path: &Path) -> bool { + self.origin.has_links_under(path) + } + + fn read_dir_unchecked(&self, path: &Path) -> Result + '_> { + self.origin.read_dir(path) + } +} + +/// a verified link path is a path that: +/// + is not empty +/// + is relative +/// + does not contain Prefix/RootDir/ParentDir +fn path_verify_link(path: &Path) -> Result<()> { + // make sure the path is not empty + if path.components().next().is_none() { + return Err(DepotError::InvalidLinkPath.into()); + } + path_verify(path).map_err(|_| DepotError::InvalidLinkPath.into()) +} + +/// a verified path is a path that: +/// + is not empty +/// + is relative +/// + does not contain Prefix/RootDir/ParentDir +fn path_verify(path: &Path) -> Result<()> { + // make sure the path is relative + // make sure the path does not contain '.' or '..' + for component in path.components() { + match component { + std::path::Component::Prefix(_) + | std::path::Component::RootDir + | std::path::Component::CurDir + | std::path::Component::ParentDir => return Err(DepotError::InvalidPath.into()), + std::path::Component::Normal(_) => {} + } + } + Ok(()) +} + +fn path_parent_or_empty(path: &Path) -> &Path { + path.parent().unwrap_or_else(|| Path::new("")) +} + +/// Iterate over the components of a path. +/// # Pre +/// The path can only have "Normal" components. +fn path_iter_comps(path: &Path) -> impl Iterator { + debug_assert!(path_verify(path).is_ok()); + path.components().map(|component| match component { + std::path::Component::Normal(comp) => comp, + _ => unreachable!(), + }) +} + +mod disk { + use std::path::{Path, PathBuf}; + + use anyhow::Context; + use serde::{Deserialize, Serialize}; + + use super::Depot; + + #[derive(Debug, Serialize, Deserialize)] + struct DiskLink { + origin: PathBuf, + destination: PathBuf, + } + + #[derive(Debug, Serialize, Deserialize)] + struct DiskLinks { + links: Vec, + } + + pub fn read(path: &Path) -> anyhow::Result { + let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; + let disk_links = toml::from_str::(&contents) + .context("Failed to parse depot file")? + .links; + let mut depot = Depot::default(); + for disk_link in disk_links { + depot + .link_create(disk_link.origin, disk_link.destination) + .context("Failed to build depot from file. File is in an invalid state")?; + } + Ok(depot) + } + + pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { + let mut links = Vec::with_capacity(depot.links.len()); + for (_, link) in depot.links.iter() { + links.push(DiskLink { + origin: link.origin.clone(), + destination: link.destination.clone(), + }); + } + let contents = + toml::to_string_pretty(&DiskLinks { links }).context("Failed to serialize depot")?; + std::fs::write(path, contents).context("Failed to write depot to file")?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_depot_link_create() { + let mut depot = Depot::default(); + let f1 = depot.link_create("f1", "f1").unwrap(); + let f2 = depot.link_create("f2", "f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d1/d2/d4").unwrap(); + + assert_eq!(depot.link_find("f1").unwrap(), Some(f1)); + assert_eq!(depot.link_find("f2").unwrap(), Some(f2)); + assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); + assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); + + depot.link_create("f2", "").unwrap_err(); + depot.link_create("", "d4").unwrap_err(); + depot.link_create("f1/f3", "f3").unwrap_err(); + } + + #[test] + fn test_depot_link_remove() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.link_create("d3", "d3").unwrap(); + + depot.link_remove(f2); + assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); + assert_eq!(depot.link_find("d1/f2").unwrap(), None); + depot.link_remove(f4); + assert_eq!(depot.link_find("d1/d2/f4").unwrap(), None); + depot.link_remove(d3); + assert_eq!(depot.link_find("d3").unwrap(), None); + } + + #[test] + fn test_depot_link_move() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + + depot.link_move(f1, "").unwrap_err(); + depot.link_move(f1, "d1/f2/f1").unwrap_err(); + depot.link_move(f1, "d1/f2").unwrap_err(); + + depot.link_move(f1, "f1").unwrap(); + assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); + depot.link_move(f1, "f2").unwrap(); + assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); + assert_eq!(depot.link_find("f2").unwrap(), Some(f1)); + } + + #[test] + fn test_depot_link_search() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let _d3 = depot.link_create("d3", "d3").unwrap(); + + assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); + assert_eq!( + depot.link_search("d1/f1/f5").unwrap(), + SearchResult::Ancestor(f1) + ); + assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound); + assert_eq!( + depot.link_search("d1/d2/f5").unwrap(), + SearchResult::NotFound + ); + } + + #[test] + fn test_depot_link_find() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.link_create("d3", "d3").unwrap(); + + assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); + assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); + assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); + assert_eq!(depot.link_find("d3").unwrap(), Some(d3)); + + assert_eq!(depot.link_find("d5").unwrap(), None); + assert_eq!(depot.link_find("d3/d5").unwrap(), None); + assert_eq!(depot.link_find("d1/d2/f5").unwrap(), None); + } + + #[test] + fn test_depot_links_under() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let d3 = depot.link_create("d3", "d3").unwrap(); + + let under_f1 = depot.links_under("d1/f1").unwrap().collect::>(); + assert_eq!(under_f1, vec![f1]); + + let under_d1 = depot.links_under("d1").unwrap().collect::>(); + let expected_under_d1 = vec![f1, f2, f3, f4]; + assert!( + under_d1.len() == expected_under_d1.len() + && expected_under_d1.iter().all(|x| under_d1.contains(x)) + ); + + let under_d2 = depot.links_under("d2").unwrap().collect::>(); + assert_eq!(under_d2, vec![]); + + let under_d3 = depot.links_under("d3").unwrap().collect::>(); + assert_eq!(under_d3, vec![d3]); + + let under_root = depot.links_under("").unwrap().collect::>(); + let expected_under_root = vec![f1, f2, f3, f4, d3]; + assert!( + under_root.len() == expected_under_root.len() + && expected_under_root.iter().all(|x| under_root.contains(x)) + ); + } + + #[test] + fn test_depot_has_links_under() { + let mut depot = Depot::default(); + let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let _d3 = depot.link_create("d3", "d3").unwrap(); + + assert!(depot.has_links_under("").unwrap()); + assert!(depot.has_links_under("d1").unwrap()); + assert!(depot.has_links_under("d3").unwrap()); + assert!(depot.has_links_under("d1/f1").unwrap()); + assert!(depot.has_links_under("d1/d2").unwrap()); + assert!(depot.has_links_under("d1/d2/f4").unwrap()); + + assert!(!depot.has_links_under("d2").unwrap()); + assert!(!depot.has_links_under("d4").unwrap()); + assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); + } + + #[test] + fn test_depot_links_verify_install() { + let mut depot = Depot::default(); + let f1 = depot.link_create("nvim", ".config/nvim").unwrap(); + let f2 = depot.link_create("alacritty", ".config/alacritty").unwrap(); + let f3 = depot.link_create("bash/.bashrc", ".bashrc").unwrap(); + let f4 = depot.link_create("bash_laptop/.bashrc", ".bashrc").unwrap(); + + depot + .links_verify_install(vec![f1, f2, f3].into_iter()) + .unwrap(); + depot + .links_verify_install(vec![f1, f2, f3, f4].into_iter()) + .unwrap_err(); + } + + #[test] + fn test_depot_read_dir() { + let mut depot = Depot::default(); + let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); + let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); + let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); + let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); + let _d3 = depot.link_create("d3", "d3").unwrap(); + + let read_dir = depot.read_dir("d1").unwrap().collect::>(); + let expected_read_dir = vec![ + DirNode::Link(f1), + DirNode::Link(f2), + DirNode::Link(f3), + DirNode::Directory(PathBuf::from("d1/d2")), + ]; + assert!( + read_dir.len() == expected_read_dir.len() + && expected_read_dir.iter().all(|x| read_dir.contains(x)) + ); + } + + #[test] + fn test_path_verify() { + path_verify(Path::new("")).unwrap(); + path_verify(Path::new("f1")).unwrap(); + path_verify(Path::new("d1/f1")).unwrap(); + path_verify(Path::new("d1/f1.txt")).unwrap(); + path_verify(Path::new("d1/./f1.txt")).unwrap(); + + path_verify(Path::new("/")).unwrap_err(); + path_verify(Path::new("./f1")).unwrap_err(); + path_verify(Path::new("/d1/f1")).unwrap_err(); + path_verify(Path::new("d1/../f1.txt")).unwrap_err(); + path_verify(Path::new("/d1/../f1.txt")).unwrap_err(); + } + + #[test] + fn test_path_verify_link() { + path_verify_link(Path::new("f1")).unwrap(); + path_verify_link(Path::new("d1/f1")).unwrap(); + path_verify_link(Path::new("d1/f1.txt")).unwrap(); + path_verify_link(Path::new("d1/./f1.txt")).unwrap(); + + path_verify_link(Path::new("")).unwrap_err(); + path_verify_link(Path::new("/")).unwrap_err(); + path_verify_link(Path::new("./f1")).unwrap_err(); + path_verify_link(Path::new("/d1/f1")).unwrap_err(); + path_verify_link(Path::new("d1/../f1.txt")).unwrap_err(); + path_verify_link(Path::new("/d1/../f1.txt")).unwrap_err(); + } + + #[test] + fn test_path_iter_comps() { + let path = Path::new("comp1/comp2/./comp3/file.txt"); + let mut iter = path_iter_comps(path); + assert_eq!(iter.next(), Some(OsStr::new("comp1"))); + assert_eq!(iter.next(), Some(OsStr::new("comp2"))); + assert_eq!(iter.next(), Some(OsStr::new("comp3"))); + assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); + assert_eq!(iter.next(), None); + } +} diff --git a/src/dotup.rs b/src/dotup.rs new file mode 100644 index 0000000..882d245 --- /dev/null +++ b/src/dotup.rs @@ -0,0 +1,533 @@ +use std::{ + cmp::Ordering, + collections::HashSet, + path::{Path, PathBuf}, +}; + +use ansi_term::Color; +use anyhow::Context; + +use crate::{ + depot::{self, Depot, DirNode, LinkID}, + utils, +}; + +#[derive(Debug)] +struct CanonicalPair { + origin: PathBuf, + destination: PathBuf, +} + +#[derive(Debug, Clone)] +enum StatusItem { + Link { + origin: PathBuf, + destination: PathBuf, + is_directory: bool, + }, + Directory { + origin: PathBuf, + items: Vec, + }, + Unlinked { + origin: PathBuf, + is_directory: bool, + }, +} + +impl StatusItem { + fn display_ord_cmp(&self, other: &Self) -> Ordering { + match (self, other) { + ( + StatusItem::Link { + origin: l_origin, .. + }, + StatusItem::Link { + origin: r_origin, .. + }, + ) => l_origin.cmp(r_origin), + (StatusItem::Link { .. }, StatusItem::Directory { .. }) => Ordering::Less, + ( + StatusItem::Link { + is_directory: l_is_dir, + .. + }, + StatusItem::Unlinked { + is_directory: u_is_dir, + .. + }, + ) => { + if *u_is_dir && !*l_is_dir { + Ordering::Less + } else { + Ordering::Greater + } + } + (StatusItem::Directory { .. }, StatusItem::Link { .. }) => Ordering::Greater, + ( + StatusItem::Directory { + origin: l_origin, .. + }, + StatusItem::Directory { + origin: r_origin, .. + }, + ) => l_origin.cmp(r_origin), + (StatusItem::Directory { .. }, StatusItem::Unlinked { .. }) => Ordering::Greater, + ( + StatusItem::Unlinked { + is_directory: u_is_dir, + .. + }, + StatusItem::Link { + is_directory: l_is_dir, + .. + }, + ) => { + if *u_is_dir && !*l_is_dir { + Ordering::Greater + } else { + Ordering::Less + } + } + (StatusItem::Unlinked { .. }, StatusItem::Directory { .. }) => Ordering::Less, + ( + StatusItem::Unlinked { + origin: l_origin, .. + }, + StatusItem::Unlinked { + origin: r_origin, .. + }, + ) => l_origin.cmp(r_origin), + } + } +} + +#[derive(Debug)] +pub struct Dotup { + depot: Depot, + depot_dir: PathBuf, + depot_path: PathBuf, + install_base: PathBuf, +} + +impl Dotup { + fn new(depot: Depot, depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { + assert!(depot_path.is_absolute()); + assert!(depot_path.is_file()); + assert!(install_base.is_absolute()); + assert!(install_base.is_dir()); + let depot_dir = { + let mut d = depot_path.clone(); + d.pop(); + d + }; + Ok(Self { + depot, + depot_dir, + depot_path, + install_base, + }) + } + + pub fn link(&mut self, origin: impl AsRef, destination: impl AsRef) { + let link_result: anyhow::Result<()> = try { + let origin = self.prepare_relative_path(origin.as_ref())?; + let destination = destination.as_ref(); + self.depot.link_create(origin, destination)?; + }; + match link_result { + Ok(_) => {} + Err(e) => println!("Failed to create link : {e}"), + } + } + + pub fn unlink(&mut self, paths: impl Iterator>, uninstall: bool) { + for origin in paths { + let unlink_result: anyhow::Result<()> = try { + let origin = self.prepare_relative_path(origin.as_ref())?; + let links_under: Vec<_> = self.depot.links_under(&origin)?.collect(); + for link_id in links_under { + if uninstall && self.symlink_is_installed_by_link_id(link_id)? { + self.symlink_uninstall_by_link_id(link_id)?; + } + self.depot.link_remove(link_id); + } + }; + match unlink_result { + Ok(_) => {} + Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()), + } + } + } + + pub fn install(&self, paths: impl Iterator>) { + let install_result: anyhow::Result<()> = try { + let link_ids = self.link_ids_from_paths_iter(paths)?; + self.depot.links_verify_install(link_ids.iter().copied())?; + + for link_id in link_ids { + self.symlink_install_by_link_id(link_id)?; + } + }; + if let Err(e) = install_result { + println!("error while installing : {e}"); + } + } + + pub fn uninstall(&self, paths: impl Iterator>) { + let uninstall_result: anyhow::Result<()> = try { + let link_ids = self.link_ids_from_paths_iter(paths)?; + for link_id in link_ids { + if self.symlink_is_installed_by_link_id(link_id)? { + self.symlink_uninstall_by_link_id(link_id)?; + } + } + }; + if let Err(e) = uninstall_result { + println!("error while uninstalling {e}",); + } + } + + pub fn mv( + &mut self, + origins: impl Iterator>, + destination: impl AsRef, + ) { + let origins = { + let mut v = Vec::new(); + for origin in origins { + match self.prepare_relative_path(origin.as_ref()) { + Ok(origin) => v.push(origin), + Err(e) => { + println!("invalid link {} : {e}", origin.as_ref().display()); + return; + } + } + } + v + }; + let destination = destination.as_ref(); + + // if we are moving multiple links then the destination must be a directory + if origins.len() > 1 && destination.is_dir() { + println!("destination must be a directory"); + return; + } + + for origin in origins { + if let Err(e) = self.mv_one(&origin, destination) { + println!("error moving link {} : {e}", origin.display()); + } + } + } + + fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> { + let link_id = match self.depot.link_find(origin)? { + Some(link_id) => link_id, + None => { + return Err(anyhow::anyhow!(format!( + "{} is not a link", + origin.display() + ))) + } + }; + let is_installed = self.symlink_is_installed_by_link_id(link_id)?; + let original_origin = self.depot.link_view(link_id).origin().to_owned(); + self.depot.link_move(link_id, destination)?; + // move the actual file on disk + if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file") { + // unwrap: moving the link back to its origin place has to work + self.depot.link_move(link_id, original_origin).unwrap(); + return Err(e); + } + // reinstall because we just moved the origin + if is_installed { + self.symlink_install_by_link_id(link_id) + .context("failed to reinstall link while moving")?; + } + Ok(()) + } + + pub fn status(&self) { + let status_result: anyhow::Result<()> = try { + let canonical_dir = utils::current_working_directory(); + let item = self.status_path_to_item(&canonical_dir)?; + self.status_print_item(item, 0)?; + }; + if let Err(e) = status_result { + println!("error while displaying status : {e}"); + } + } + fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result { + debug_assert!(canonical_path.is_absolute()); + debug_assert!(canonical_path.exists()); + let relative_path = self.prepare_relative_path(canonical_path)?; + + let item = if canonical_path.is_dir() { + if let Some(link_id) = self.depot.link_find(&relative_path)? { + let destination = self.depot.link_view(link_id).destination().to_owned(); + StatusItem::Link { + origin: relative_path, + destination, + is_directory: true, + } + } else if self.depot.has_links_under(&relative_path)? { + let mut items = Vec::new(); + let mut collected_rel_paths = HashSet::::new(); + let directory_paths = utils::collect_paths_in_dir(&canonical_path)?; + for canonical_item_path in directory_paths { + let item = self.status_path_to_item(&canonical_item_path)?; + match &item { + StatusItem::Link { origin, .. } | StatusItem::Directory { origin, .. } => { + collected_rel_paths.insert(origin.to_owned()); + } + _ => {} + } + items.push(item); + } + + for dir_node in self.depot.read_dir(&relative_path)? { + match dir_node { + DirNode::Link(link_id) => { + let link_view = self.depot.link_view(link_id); + let link_rel_path = link_view.origin(); + let link_rel_dest = link_view.destination(); + if !collected_rel_paths.contains(link_rel_path) { + items.push(StatusItem::Link { + origin: link_rel_path.to_owned(), + destination: link_rel_dest.to_owned(), + is_directory: false, + }); + } + } + DirNode::Directory(_) => {} + } + } + + StatusItem::Directory { + origin: relative_path, + items, + } + } else { + StatusItem::Unlinked { + origin: relative_path, + is_directory: true, + } + } + } else if let Some(link_id) = self.depot.link_find(&relative_path)? { + let destination = self.depot.link_view(link_id).destination().to_owned(); + StatusItem::Link { + origin: relative_path, + destination, + is_directory: false, + } + } else { + StatusItem::Unlinked { + origin: relative_path, + is_directory: false, + } + }; + Ok(item) + } + fn status_print_item(&self, item: StatusItem, depth: u32) -> anyhow::Result<()> { + fn print_depth(d: u32) { + for _ in 0..d.saturating_sub(1) { + print!(" "); + } + } + fn origin_color(exists: bool, is_installed: bool) -> Color { + if !exists { + Color::Red + } else if is_installed { + Color::Green + } else { + Color::RGB(255, 127, 0) + } + } + + let destination_color = Color::Blue; + + print_depth(depth); + match item { + StatusItem::Link { + origin, + destination, + is_directory, + } => { + let canonical_origin = self.depot_dir.join(&origin); + let canonical_destination = self.install_base.join(&destination); + let file_name = Self::status_get_filename(&canonical_origin); + let is_installed = + self.symlink_is_installed(&canonical_origin, &canonical_destination)?; + let exists = canonical_origin.exists(); + let origin_color = origin_color(exists, is_installed); + let directory_extra = if is_directory { "/" } else { "" }; + println!( + "{}{} -> {}", + origin_color.paint(file_name), + directory_extra, + destination_color.paint(destination.display().to_string()) + ); + } + StatusItem::Directory { origin, mut items } => { + items.sort_by(|a, b| StatusItem::display_ord_cmp(a, b).reverse()); + let directory_name = Self::status_get_filename(&origin); + if depth != 0 { + println!("{}/", directory_name); + } + for item in items { + self.status_print_item(item, depth + 1)?; + } + } + StatusItem::Unlinked { + origin, + is_directory, + } => { + let file_name = Self::status_get_filename(&origin); + let directory_extra = if is_directory { "/" } else { "" }; + println!("{}{}", file_name, directory_extra); + } + } + Ok(()) + } + fn status_get_filename(path: &Path) -> &str { + path.file_name() + .and_then(|s| s.to_str()) + .unwrap_or_default() + } + + fn prepare_relative_path(&self, origin: &Path) -> anyhow::Result { + let canonical = utils::weakly_canonical(origin); + let relative = canonical + .strip_prefix(&self.depot_dir) + .context("Invalid origin path, not under depot directory")?; + Ok(relative.to_owned()) + } + + fn link_ids_from_paths_iter( + &self, + paths: impl Iterator>, + ) -> anyhow::Result> { + let mut link_ids = HashSet::::default(); + for path in paths { + let path = self.prepare_relative_path(path.as_ref())?; + link_ids.extend(self.depot.links_under(&path)?); + } + Ok(Vec::from_iter(link_ids.into_iter())) + } + + fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result { + let canonical_pair = self.canonical_pair_from_link_id(link_id); + self.symlink_is_installed(&canonical_pair.origin, &canonical_pair.destination) + } + + fn symlink_is_installed(&self, origin: &Path, destination: &Path) -> anyhow::Result { + debug_assert!(origin.is_absolute()); + debug_assert!(destination.is_absolute()); + + if destination.is_symlink() { + let symlink_destination = destination.read_link()?; + match symlink_destination.canonicalize() { + Ok(canonicalized) => Ok(origin == canonicalized), + Err(_) => Ok(false), + } + } else { + Ok(false) + } + } + + fn symlink_install_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { + let canonical_pair = self.canonical_pair_from_link_id(link_id); + self.symlink_install(&canonical_pair.origin, &canonical_pair.destination) + } + + fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { + debug_assert!(origin.is_absolute()); + debug_assert!(destination.is_absolute()); + log::debug!( + "symlink_install : {} -> {}", + origin.display(), + destination.display() + ); + + let destination_parent = destination + .parent() + .ok_or_else(|| anyhow::anyhow!("destination has no parent component"))?; + std::fs::create_dir_all(destination_parent).context("Failed to create directories")?; + // need to do this beacause if the destination path ends in '/' because the symlink + // functions will treat it as a directory but we want a file with that name. + let destination = destination.with_file_name(destination.file_name().unwrap()); + + let destination_exists = destination.exists(); + let destination_is_symlink = destination.is_symlink(); + + if destination_exists && !destination_is_symlink { + return Err(anyhow::anyhow!("destination already exists")); + } + + if destination_is_symlink { + log::debug!("symlink already exists, removing before recreating"); + std::fs::remove_file(&destination)?; + } + + log::debug!( + "creating filesystem symlink {} -> {}", + origin.display(), + destination.display() + ); + std::os::unix::fs::symlink(origin, destination).context("failed to create symlink")?; + + Ok(()) + } + + fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { + debug_assert!(origin.is_absolute()); + debug_assert!(destination.is_absolute()); + let destination = destination.with_file_name(destination.file_name().unwrap()); + + if destination.is_symlink() { + let symlink_destination = destination.read_link()?.canonicalize()?; + if symlink_destination == origin { + std::fs::remove_file(&destination)?; + } + } + + Ok(()) + } + + fn symlink_uninstall_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { + let canonical_pair = self.canonical_pair_from_link_id(link_id); + self.symlink_uninstall(&canonical_pair.origin, &canonical_pair.destination) + } + + fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair { + let link_view = self.depot.link_view(link_id); + let relative_origin = link_view.origin(); + let relative_destination = link_view.destination(); + let canonical_origin = self.depot_dir.join(relative_origin); + let canonical_destination = self.install_base.join(relative_destination); + CanonicalPair { + origin: canonical_origin, + destination: canonical_destination, + } + } +} + +pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { + let depot_path = depot_path + .canonicalize() + .context("Failed to canonicalize depot path")?; + let install_base = install_base + .canonicalize() + .context("Failed to canonicalize install base")?; + if !install_base.is_dir() { + return Err(anyhow::anyhow!("Install base must be a directory")); + } + let depot = depot::read(&depot_path)?; + Dotup::new(depot, depot_path, install_base) +} + +pub fn write(dotup: &Dotup) -> anyhow::Result<()> { + depot::write(&dotup.depot_path, &dotup.depot)?; + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 7b38357..f933f6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,1563 +1,8 @@ #![feature(try_blocks)] -// TODO: rewrite all errors so they start with lower case - -mod depot { - use anyhow::Context; - use std::{ - collections::HashSet, - ffi::{OsStr, OsString}, - ops::Index, - path::{Path, PathBuf}, - }; - use thiserror::Error; - - use slotmap::{Key, SlotMap}; - - //pub type Result = std::result::Result; - pub use anyhow::Result; - pub use disk::{read, write}; - - slotmap::new_key_type! {pub struct LinkID;} - slotmap::new_key_type! {struct NodeID;} - - #[derive(Debug, Error)] - enum DepotError { - #[error("path must be relative")] - InvalidPath, - #[error("path must be relative and not empty")] - InvalidLinkPath, - } - - #[derive(Debug, Clone)] - struct Node { - comp: OsString, - parent: NodeID, - kind: NodeKind, - } - - #[derive(Debug, Clone)] - enum NodeKind { - Link(LinkID), - Directory(HashSet), - } - - #[derive(Debug, Clone, PartialEq, Eq)] - enum NodeSearchResult { - Found(NodeID), - /// the closest NodeID up the the search point. - NotFound(NodeID), - } - - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum DirNode { - Link(LinkID), - Directory(PathBuf), - } - - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum SearchResult { - Found(LinkID), - Ancestor(LinkID), - NotFound, - } - - #[derive(Debug, Clone)] - struct Link { - origin: PathBuf, - destination: PathBuf, - origin_id: NodeID, - } - - #[derive(Debug)] - pub struct LinkView<'a> { - link_id: LinkID, - depot: &'a Depot, - } - - impl<'a> LinkView<'a> { - pub fn origin(&self) -> &Path { - &self.depot.links[self.link_id].origin - } - - pub fn destination(&self) -> &Path { - &self.depot.links[self.link_id].destination - } - } - - #[derive(Debug, Clone)] - struct DepotTree { - root: NodeID, - nodes: SlotMap, - } - - impl Default for DepotTree { - fn default() -> Self { - let mut nodes = SlotMap::::default(); - let root = nodes.insert(Node { - comp: Default::default(), - parent: Default::default(), - kind: NodeKind::Directory(Default::default()), - }); - Self { root, nodes } - } - } - - impl Index for DepotTree { - type Output = Node; - - fn index(&self, index: NodeID) -> &Self::Output { - self.nodes.index(index) - } - } - - impl DepotTree { - /// create a node of kind [`NodeKind::Link`]. - pub fn link_create(&mut self, path: &Path, link_id: LinkID) -> Result { - debug_assert!(path_verify_link(path).is_ok()); - - let path_search_result = self.search(path); - - // handle the error cases - match path_search_result { - NodeSearchResult::Found(node_id) => { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(_) => Err(anyhow::anyhow!("link already exists")), - NodeKind::Directory(_) => { - Err(anyhow::anyhow!("path already has links under it")) - } - } - } - NodeSearchResult::NotFound(ancestor_node_id) => { - let ancestor_node = &self.nodes[ancestor_node_id]; - match &ancestor_node.kind { - NodeKind::Link(_) => Err(anyhow::anyhow!( - "an ancestor of this path is already linked" - )), - NodeKind::Directory(_) => Ok(()), - } - } - }?; - - // create the node - // unwrap: this is a verfied link path, it must have atleast one component - let filename = path.file_name().unwrap(); - let parent_path = path_parent_or_empty(path); - let node_id = self.nodes.insert(Node { - comp: filename.to_owned(), - parent: Default::default(), - kind: NodeKind::Link(link_id), - }); - let parent_id = self.directory_get_or_create(parent_path, node_id); - self.nodes[node_id].parent = parent_id; - Ok(node_id) - } - - pub fn link_update_id(&mut self, node_id: NodeID, link_id: LinkID) { - let node = &mut self.nodes[node_id]; - match &mut node.kind { - NodeKind::Link(lid) => *lid = link_id, - NodeKind::Directory(_) => unreachable!(), - } - } - - /// attempts to moves a node of kind [`NodeKind::Link`] to `destination`. - pub fn link_move(&mut self, node_id: NodeID, destination: &Path) -> Result<()> { - let parent_id = self.nodes[node_id].parent; - let parent = &mut self.nodes[parent_id]; - - // remove the node from its parent temporarily so that the search never returns this - // link and that way any link will find means an error. - // if an error does happen then we re-add this node to its parent to keep the data - // consistent. - match &mut parent.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => children.remove(&node_id), - }; - - let search_result = self.search(destination); - // handle the error cases - match search_result { - NodeSearchResult::Found(found_id) => { - assert!(found_id != node_id); - self.directory_add_child(parent_id, node_id); - return Err(anyhow::anyhow!("link already exists at that path")); - } - NodeSearchResult::NotFound(ancestor_id) => { - let ancestor = &self.nodes[ancestor_id]; - match &ancestor.kind { - NodeKind::Link(_) => { - self.directory_add_child(parent_id, node_id); - return Err(anyhow::anyhow!("ancestor path is already linked")); - } - NodeKind::Directory(_) => {} - } - } - }; - - let destination_parent = path_parent_or_empty(destination); - let new_parent_id = self.directory_get_or_create(destination_parent, node_id); - if new_parent_id != parent_id { - self.nodes[node_id].parent = new_parent_id; - - // we have to re-add and call the remove function because it could lead to the removal - // of several directories if they become empty after this remove. - self.directory_add_child(parent_id, node_id); - self.directory_remove_child(parent_id, node_id); - } - - // unwrap: destination is a verified link path so it has atleast 1 component - let comp = destination.file_name().unwrap(); - let node = &mut self.nodes[node_id]; - if node.comp != comp { - node.comp = comp.to_owned(); - } - - Ok(()) - } - - pub fn link_search(&self, path: &Path) -> SearchResult { - match self.search(path) { - NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(link_id) => SearchResult::Found(*link_id), - NodeKind::Directory(_) => SearchResult::NotFound, - }, - NodeSearchResult::NotFound(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(link_id) => SearchResult::Ancestor(*link_id), - NodeKind::Directory(_) => SearchResult::NotFound, - }, - } - } - - /// remove a node of kind [`NodeKind::Link`]. - pub fn link_remove(&mut self, node_id: NodeID) { - let node = &self.nodes[node_id]; - assert!(std::matches!(node.kind, NodeKind::Link(_))); - let parent_id = node.parent; - self.nodes.remove(node_id); - self.directory_remove_child(parent_id, node_id); - } - - pub fn links_under(&self, path: &Path) -> impl Iterator + '_ { - let links = match self.search(path) { - NodeSearchResult::Found(node_id) => { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(link_id) => vec![*link_id], - NodeKind::Directory(children) => { - let mut links = Vec::new(); - let mut node_ids = Vec::from_iter(children.iter().copied()); - while let Some(child_id) = node_ids.pop() { - let child = &self.nodes[child_id]; - match &child.kind { - NodeKind::Link(link_id) => links.push(*link_id), - NodeKind::Directory(extra_children) => { - node_ids.extend(extra_children.iter().copied()) - } - } - } - links - } - } - } - NodeSearchResult::NotFound(_) => vec![], - }; - links.into_iter() - } - - pub fn has_links_under(&self, path: &Path) -> bool { - // it does not matter what type of node is found. if a directory exists then there - // must be atleast one link under it. - match self.search(path) { - NodeSearchResult::Found(_) => true, - NodeSearchResult::NotFound(_) => false, - } - } - - pub fn read_dir(&self, path: &Path) -> Result + '_> { - match self.search(path) { - NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(_) => Err(anyhow::anyhow!("read dir called on a link")), - NodeKind::Directory(children) => Ok(children.iter().map(|child_id| { - let child = &self.nodes[*child_id]; - match &child.kind { - NodeKind::Link(link_id) => DirNode::Link(*link_id), - NodeKind::Directory(_) => { - DirNode::Directory(self.build_path(*child_id)) - } - } - })), - }, - NodeSearchResult::NotFound(_) => Err(anyhow::anyhow!("directory not found")), - } - } - - pub fn build_path(&self, node_id: NodeID) -> PathBuf { - fn recursive_helper(nodes: &SlotMap, nid: NodeID, pbuf: &mut PathBuf) { - if nid.is_null() { - return; - } - let parent_id = nodes[nid].parent; - recursive_helper(nodes, parent_id, pbuf); - pbuf.push(&nodes[nid].comp); - } - - let mut node_path = PathBuf::default(); - recursive_helper(&self.nodes, node_id, &mut node_path); - node_path - } - - fn search(&self, path: &Path) -> NodeSearchResult { - debug_assert!(path_verify(path).is_ok()); - - let mut curr_node_id = self.root; - let mut comp_iter = path_iter_comps(path).peekable(); - while let Some(comp) = comp_iter.next() { - if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { - let child = &self.nodes[child_id]; - match &child.kind { - NodeKind::Link(_) => { - if comp_iter.peek().is_some() { - return NodeSearchResult::NotFound(child_id); - } else { - return NodeSearchResult::Found(child_id); - } - } - NodeKind::Directory(_) => curr_node_id = child_id, - } - } else { - return NodeSearchResult::NotFound(curr_node_id); - } - } - NodeSearchResult::Found(curr_node_id) - } - - // creates directories all the way up to and including path. - // there cannot be any links up to `path`. - fn directory_get_or_create(&mut self, path: &Path, initial_child: NodeID) -> NodeID { - // TODO: this could be replaced if the search function also returned the depth of the - // node and we skip those components and just start creating directories up to the - // path. - let mut curr_node_id = self.root; - for comp in path_iter_comps(path) { - if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { - debug_assert!(std::matches!( - self.nodes[child_id].kind, - NodeKind::Directory(_) - )); - curr_node_id = child_id; - } else { - let new_node_id = self.nodes.insert(Node { - comp: comp.to_owned(), - parent: curr_node_id, - kind: NodeKind::Directory(Default::default()), - }); - self.directory_add_child(curr_node_id, new_node_id); - curr_node_id = new_node_id; - } - } - self.directory_add_child(curr_node_id, initial_child); - curr_node_id - } - - fn directory_search_children(&self, node_id: NodeID, comp: &OsStr) -> Option { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => { - for &child_id in children { - let child = &self.nodes[child_id]; - if child.comp == comp { - return Some(child_id); - } - } - } - } - None - } - - fn directory_add_child(&mut self, node_id: NodeID, child_id: NodeID) { - let node = &mut self.nodes[node_id]; - match &mut node.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => children.insert(child_id), - }; - } - - fn directory_remove_child(&mut self, node_id: NodeID, child_id: NodeID) { - let node = &mut self.nodes[node_id]; - match &mut node.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => { - children.remove(&child_id); - if children.is_empty() && !node.parent.is_null() { - let parent_id = node.parent; - self.directory_remove_child(parent_id, node_id); - } - } - } - } - } - - #[derive(Debug, Default, Clone)] - pub struct Depot { - links: SlotMap, - origin: DepotTree, - } - - impl Depot { - pub fn link_create( - &mut self, - origin: impl AsRef, - destination: impl AsRef, - ) -> Result { - let origin = origin.as_ref(); - let destination = destination.as_ref(); - path_verify_link(origin)?; - path_verify_link(destination)?; - self.link_create_unchecked(origin, destination) - } - - pub fn link_remove(&mut self, link_id: LinkID) { - let node_id = self.links[link_id].origin_id; - self.links.remove(link_id); - self.origin.link_remove(node_id); - } - - /// moves the link specified by `link_id` to the path at `destination`. - /// if the link is already at the destination nothing is done. - /// if the destination is another link that that link is removed. - /// if the destination is under another link then an error is returned. - /// `destination` will be the link's new origin. - pub fn link_move(&mut self, link_id: LinkID, destination: impl AsRef) -> Result<()> { - let destination = destination.as_ref(); - path_verify_link(destination)?; - self.link_move_unchecked(link_id, destination) - } - - #[allow(unused)] - pub fn link_search(&self, path: impl AsRef) -> Result { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.link_search_unchecked(path)) - } - - pub fn link_find(&self, path: impl AsRef) -> Result> { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.link_find_unchecked(path)) - } - - pub fn links_under( - &self, - path: impl AsRef, - ) -> Result + '_> { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.links_under_unchecked(path)) - } - - pub fn has_links_under(&self, path: impl AsRef) -> Result { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.has_links_under_unchecked(path)) - } - - pub fn links_verify_install(&self, link_ids: impl Iterator) -> Result<()> { - let mut destination = DepotTree::default(); - for link_id in link_ids { - let link = &self.links[link_id]; - destination - .link_create(&link.destination, link_id) - .context("link destinations overlap")?; - } - Ok(()) - } - - pub fn link_view(&self, link_id: LinkID) -> LinkView { - LinkView { - link_id, - depot: self, - } - } - - pub fn read_dir( - &self, - path: impl AsRef, - ) -> Result + '_> { - let path = path.as_ref(); - path_verify(path)?; - self.read_dir_unchecked(path) - } - - fn link_create_unchecked(&mut self, origin: &Path, destination: &Path) -> Result { - let node_id = self.origin.link_create(origin, Default::default())?; - let link_id = self.links.insert(Link { - origin: origin.to_owned(), - destination: destination.to_owned(), - origin_id: node_id, - }); - self.origin.link_update_id(node_id, link_id); - Ok(link_id) - } - - fn link_move_unchecked(&mut self, link_id: LinkID, destination: &Path) -> Result<()> { - let link = &self.links[link_id]; - if link.origin == destination { - return Ok(()); - } - let node_id = link.origin_id; - self.origin.link_move(node_id, destination)?; - self.links[link_id].origin = destination.to_owned(); - Ok(()) - } - - fn link_search_unchecked(&self, path: &Path) -> SearchResult { - self.origin.link_search(path) - } - - fn link_find_unchecked(&self, path: &Path) -> Option { - match self.link_search_unchecked(path) { - SearchResult::Found(link_id) => Some(link_id), - _ => None, - } - } - - fn links_under_unchecked(&self, path: &Path) -> impl Iterator + '_ { - self.origin.links_under(path) - } - - fn has_links_under_unchecked(&self, path: &Path) -> bool { - self.origin.has_links_under(path) - } - - fn read_dir_unchecked(&self, path: &Path) -> Result + '_> { - self.origin.read_dir(path) - } - } - - /// a verified link path is a path that: - /// + is not empty - /// + is relative - /// + does not contain Prefix/RootDir/ParentDir - fn path_verify_link(path: &Path) -> Result<()> { - // make sure the path is not empty - if path.components().next().is_none() { - return Err(DepotError::InvalidLinkPath.into()); - } - path_verify(path).map_err(|_| DepotError::InvalidLinkPath.into()) - } - - /// a verified path is a path that: - /// + is not empty - /// + is relative - /// + does not contain Prefix/RootDir/ParentDir - fn path_verify(path: &Path) -> Result<()> { - // make sure the path is relative - // make sure the path does not contain '.' or '..' - for component in path.components() { - match component { - std::path::Component::Prefix(_) - | std::path::Component::RootDir - | std::path::Component::CurDir - | std::path::Component::ParentDir => return Err(DepotError::InvalidPath.into()), - std::path::Component::Normal(_) => {} - } - } - Ok(()) - } - - fn path_parent_or_empty(path: &Path) -> &Path { - path.parent().unwrap_or_else(|| Path::new("")) - } - - /// Iterate over the components of a path. - /// # Pre - /// The path can only have "Normal" components. - fn path_iter_comps(path: &Path) -> impl Iterator { - debug_assert!(path_verify(path).is_ok()); - path.components().map(|component| match component { - std::path::Component::Normal(comp) => comp, - _ => unreachable!(), - }) - } - - mod disk { - use std::path::{Path, PathBuf}; - - use anyhow::Context; - use serde::{Deserialize, Serialize}; - - use super::Depot; - - #[derive(Debug, Serialize, Deserialize)] - struct DiskLink { - origin: PathBuf, - destination: PathBuf, - } - - #[derive(Debug, Serialize, Deserialize)] - struct DiskLinks { - links: Vec, - } - - pub fn read(path: &Path) -> anyhow::Result { - let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; - let disk_links = toml::from_str::(&contents) - .context("Failed to parse depot file")? - .links; - let mut depot = Depot::default(); - for disk_link in disk_links { - depot - .link_create(disk_link.origin, disk_link.destination) - .context("Failed to build depot from file. File is in an invalid state")?; - } - Ok(depot) - } - - pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { - let mut links = Vec::with_capacity(depot.links.len()); - for (_, link) in depot.links.iter() { - links.push(DiskLink { - origin: link.origin.clone(), - destination: link.destination.clone(), - }); - } - let contents = toml::to_string_pretty(&DiskLinks { links }) - .context("Failed to serialize depot")?; - std::fs::write(path, contents).context("Failed to write depot to file")?; - Ok(()) - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn test_depot_link_create() { - let mut depot = Depot::default(); - let f1 = depot.link_create("f1", "f1").unwrap(); - let f2 = depot.link_create("f2", "f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d1/d2/d4").unwrap(); - - assert_eq!(depot.link_find("f1").unwrap(), Some(f1)); - assert_eq!(depot.link_find("f2").unwrap(), Some(f2)); - assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); - assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); - - depot.link_create("f2", "").unwrap_err(); - depot.link_create("", "d4").unwrap_err(); - depot.link_create("f1/f3", "f3").unwrap_err(); - } - - #[test] - fn test_depot_link_remove() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - depot.link_remove(f2); - assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); - assert_eq!(depot.link_find("d1/f2").unwrap(), None); - depot.link_remove(f4); - assert_eq!(depot.link_find("d1/d2/f4").unwrap(), None); - depot.link_remove(d3); - assert_eq!(depot.link_find("d3").unwrap(), None); - } - - #[test] - fn test_depot_link_move() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - - depot.link_move(f1, "").unwrap_err(); - depot.link_move(f1, "d1/f2/f1").unwrap_err(); - depot.link_move(f1, "d1/f2").unwrap_err(); - - depot.link_move(f1, "f1").unwrap(); - assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); - depot.link_move(f1, "f2").unwrap(); - assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); - assert_eq!(depot.link_find("f2").unwrap(), Some(f1)); - } - - #[test] - fn test_depot_link_search() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); - assert_eq!( - depot.link_search("d1/f1/f5").unwrap(), - SearchResult::Ancestor(f1) - ); - assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound); - assert_eq!( - depot.link_search("d1/d2/f5").unwrap(), - SearchResult::NotFound - ); - } - - #[test] - fn test_depot_link_find() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); - assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); - assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); - assert_eq!(depot.link_find("d3").unwrap(), Some(d3)); - - assert_eq!(depot.link_find("d5").unwrap(), None); - assert_eq!(depot.link_find("d3/d5").unwrap(), None); - assert_eq!(depot.link_find("d1/d2/f5").unwrap(), None); - } - - #[test] - fn test_depot_links_under() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - let under_f1 = depot.links_under("d1/f1").unwrap().collect::>(); - assert_eq!(under_f1, vec![f1]); - - let under_d1 = depot.links_under("d1").unwrap().collect::>(); - let expected_under_d1 = vec![f1, f2, f3, f4]; - assert!( - under_d1.len() == expected_under_d1.len() - && expected_under_d1.iter().all(|x| under_d1.contains(x)) - ); - - let under_d2 = depot.links_under("d2").unwrap().collect::>(); - assert_eq!(under_d2, vec![]); - - let under_d3 = depot.links_under("d3").unwrap().collect::>(); - assert_eq!(under_d3, vec![d3]); - - let under_root = depot.links_under("").unwrap().collect::>(); - let expected_under_root = vec![f1, f2, f3, f4, d3]; - assert!( - under_root.len() == expected_under_root.len() - && expected_under_root.iter().all(|x| under_root.contains(x)) - ); - } - - #[test] - fn test_depot_has_links_under() { - let mut depot = Depot::default(); - let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - assert!(depot.has_links_under("").unwrap()); - assert!(depot.has_links_under("d1").unwrap()); - assert!(depot.has_links_under("d3").unwrap()); - assert!(depot.has_links_under("d1/f1").unwrap()); - assert!(depot.has_links_under("d1/d2").unwrap()); - assert!(depot.has_links_under("d1/d2/f4").unwrap()); - - assert!(!depot.has_links_under("d2").unwrap()); - assert!(!depot.has_links_under("d4").unwrap()); - assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); - } - - #[test] - fn test_depot_links_verify_install() { - let mut depot = Depot::default(); - let f1 = depot.link_create("nvim", ".config/nvim").unwrap(); - let f2 = depot.link_create("alacritty", ".config/alacritty").unwrap(); - let f3 = depot.link_create("bash/.bashrc", ".bashrc").unwrap(); - let f4 = depot.link_create("bash_laptop/.bashrc", ".bashrc").unwrap(); - - depot - .links_verify_install(vec![f1, f2, f3].into_iter()) - .unwrap(); - depot - .links_verify_install(vec![f1, f2, f3, f4].into_iter()) - .unwrap_err(); - } - - #[test] - fn test_depot_read_dir() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - let read_dir = depot.read_dir("d1").unwrap().collect::>(); - let expected_read_dir = vec![ - DirNode::Link(f1), - DirNode::Link(f2), - DirNode::Link(f3), - DirNode::Directory(PathBuf::from("d1/d2")), - ]; - assert!( - read_dir.len() == expected_read_dir.len() - && expected_read_dir.iter().all(|x| read_dir.contains(x)) - ); - } - - #[test] - fn test_path_verify() { - path_verify(Path::new("")).unwrap(); - path_verify(Path::new("f1")).unwrap(); - path_verify(Path::new("d1/f1")).unwrap(); - path_verify(Path::new("d1/f1.txt")).unwrap(); - path_verify(Path::new("d1/./f1.txt")).unwrap(); - - path_verify(Path::new("/")).unwrap_err(); - path_verify(Path::new("./f1")).unwrap_err(); - path_verify(Path::new("/d1/f1")).unwrap_err(); - path_verify(Path::new("d1/../f1.txt")).unwrap_err(); - path_verify(Path::new("/d1/../f1.txt")).unwrap_err(); - } - - #[test] - fn test_path_verify_link() { - path_verify_link(Path::new("f1")).unwrap(); - path_verify_link(Path::new("d1/f1")).unwrap(); - path_verify_link(Path::new("d1/f1.txt")).unwrap(); - path_verify_link(Path::new("d1/./f1.txt")).unwrap(); - - path_verify_link(Path::new("")).unwrap_err(); - path_verify_link(Path::new("/")).unwrap_err(); - path_verify_link(Path::new("./f1")).unwrap_err(); - path_verify_link(Path::new("/d1/f1")).unwrap_err(); - path_verify_link(Path::new("d1/../f1.txt")).unwrap_err(); - path_verify_link(Path::new("/d1/../f1.txt")).unwrap_err(); - } - - #[test] - fn test_path_iter_comps() { - let path = Path::new("comp1/comp2/./comp3/file.txt"); - let mut iter = path_iter_comps(path); - assert_eq!(iter.next(), Some(OsStr::new("comp1"))); - assert_eq!(iter.next(), Some(OsStr::new("comp2"))); - assert_eq!(iter.next(), Some(OsStr::new("comp3"))); - assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); - assert_eq!(iter.next(), None); - } - } -} - -pub mod dotup { - use std::{ - cmp::Ordering, - collections::HashSet, - path::{Path, PathBuf}, - }; - - use ansi_term::Color; - use anyhow::Context; - - use crate::{ - depot::{self, Depot, DirNode, LinkID}, - utils, - }; - - #[derive(Debug)] - struct CanonicalPair { - origin: PathBuf, - destination: PathBuf, - } - - #[derive(Debug, Clone)] - enum StatusItem { - Link { - origin: PathBuf, - destination: PathBuf, - is_directory: bool, - }, - Directory { - origin: PathBuf, - items: Vec, - }, - Unlinked { - origin: PathBuf, - is_directory: bool, - }, - } - - impl StatusItem { - fn display_ord_cmp(&self, other: &Self) -> Ordering { - match (self, other) { - ( - StatusItem::Link { - origin: l_origin, .. - }, - StatusItem::Link { - origin: r_origin, .. - }, - ) => l_origin.cmp(r_origin), - (StatusItem::Link { .. }, StatusItem::Directory { .. }) => Ordering::Less, - ( - StatusItem::Link { - is_directory: l_is_dir, - .. - }, - StatusItem::Unlinked { - is_directory: u_is_dir, - .. - }, - ) => { - if *u_is_dir && !*l_is_dir { - Ordering::Less - } else { - Ordering::Greater - } - } - (StatusItem::Directory { .. }, StatusItem::Link { .. }) => Ordering::Greater, - ( - StatusItem::Directory { - origin: l_origin, .. - }, - StatusItem::Directory { - origin: r_origin, .. - }, - ) => l_origin.cmp(r_origin), - (StatusItem::Directory { .. }, StatusItem::Unlinked { .. }) => Ordering::Greater, - ( - StatusItem::Unlinked { - is_directory: u_is_dir, - .. - }, - StatusItem::Link { - is_directory: l_is_dir, - .. - }, - ) => { - if *u_is_dir && !*l_is_dir { - Ordering::Greater - } else { - Ordering::Less - } - } - (StatusItem::Unlinked { .. }, StatusItem::Directory { .. }) => Ordering::Less, - ( - StatusItem::Unlinked { - origin: l_origin, .. - }, - StatusItem::Unlinked { - origin: r_origin, .. - }, - ) => l_origin.cmp(r_origin), - } - } - } - - #[derive(Debug)] - pub struct Dotup { - depot: Depot, - depot_dir: PathBuf, - depot_path: PathBuf, - install_base: PathBuf, - } - - impl Dotup { - fn new(depot: Depot, depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { - assert!(depot_path.is_absolute()); - assert!(depot_path.is_file()); - assert!(install_base.is_absolute()); - assert!(install_base.is_dir()); - let depot_dir = { - let mut d = depot_path.clone(); - d.pop(); - d - }; - Ok(Self { - depot, - depot_dir, - depot_path, - install_base, - }) - } - - pub fn link(&mut self, origin: impl AsRef, destination: impl AsRef) { - let link_result: anyhow::Result<()> = try { - let origin = self.prepare_relative_path(origin.as_ref())?; - let destination = destination.as_ref(); - self.depot.link_create(origin, destination)?; - }; - match link_result { - Ok(_) => {} - Err(e) => println!("Failed to create link : {e}"), - } - } - - pub fn unlink(&mut self, paths: impl Iterator>, uninstall: bool) { - for origin in paths { - let unlink_result: anyhow::Result<()> = try { - let origin = self.prepare_relative_path(origin.as_ref())?; - let links_under: Vec<_> = self.depot.links_under(&origin)?.collect(); - for link_id in links_under { - if uninstall && self.symlink_is_installed_by_link_id(link_id)? { - self.symlink_uninstall_by_link_id(link_id)?; - } - self.depot.link_remove(link_id); - } - }; - match unlink_result { - Ok(_) => {} - Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()), - } - } - } - - pub fn install(&self, paths: impl Iterator>) { - let install_result: anyhow::Result<()> = try { - let link_ids = self.link_ids_from_paths_iter(paths)?; - self.depot.links_verify_install(link_ids.iter().copied())?; - - for link_id in link_ids { - self.symlink_install_by_link_id(link_id)?; - } - }; - if let Err(e) = install_result { - println!("error while installing : {e}"); - } - } - - pub fn uninstall(&self, paths: impl Iterator>) { - let uninstall_result: anyhow::Result<()> = try { - let link_ids = self.link_ids_from_paths_iter(paths)?; - for link_id in link_ids { - if self.symlink_is_installed_by_link_id(link_id)? { - self.symlink_uninstall_by_link_id(link_id)?; - } - } - }; - if let Err(e) = uninstall_result { - println!("error while uninstalling {e}",); - } - } - - pub fn mv( - &mut self, - origins: impl Iterator>, - destination: impl AsRef, - ) { - let origins = { - let mut v = Vec::new(); - for origin in origins { - match self.prepare_relative_path(origin.as_ref()) { - Ok(origin) => v.push(origin), - Err(e) => { - println!("invalid link {} : {e}", origin.as_ref().display()); - return; - } - } - } - v - }; - let destination = destination.as_ref(); - - // if we are moving multiple links then the destination must be a directory - if origins.len() > 1 && destination.is_dir() { - println!("destination must be a directory"); - return; - } - - for origin in origins { - if let Err(e) = self.mv_one(&origin, destination) { - println!("error moving link {} : {e}", origin.display()); - } - } - } - - fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - let link_id = match self.depot.link_find(origin)? { - Some(link_id) => link_id, - None => { - return Err(anyhow::anyhow!(format!( - "{} is not a link", - origin.display() - ))) - } - }; - let is_installed = self.symlink_is_installed_by_link_id(link_id)?; - let original_origin = self.depot.link_view(link_id).origin().to_owned(); - self.depot.link_move(link_id, destination)?; - // move the actual file on disk - if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file") { - // unwrap: moving the link back to its origin place has to work - self.depot.link_move(link_id, original_origin).unwrap(); - return Err(e); - } - // reinstall because we just moved the origin - if is_installed { - self.symlink_install_by_link_id(link_id) - .context("failed to reinstall link while moving")?; - } - Ok(()) - } - - pub fn status(&self) { - let status_result: anyhow::Result<()> = try { - let canonical_dir = utils::current_working_directory(); - let item = self.status_path_to_item(&canonical_dir)?; - self.status_print_item(item, 0)?; - }; - if let Err(e) = status_result { - println!("error while displaying status : {e}"); - } - } - fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result { - debug_assert!(canonical_path.is_absolute()); - debug_assert!(canonical_path.exists()); - let relative_path = self.prepare_relative_path(canonical_path)?; - - let item = if canonical_path.is_dir() { - if let Some(link_id) = self.depot.link_find(&relative_path)? { - let destination = self.depot.link_view(link_id).destination().to_owned(); - StatusItem::Link { - origin: relative_path, - destination, - is_directory: true, - } - } else if self.depot.has_links_under(&relative_path)? { - let mut items = Vec::new(); - let mut collected_rel_paths = HashSet::::new(); - let directory_paths = utils::collect_paths_in_dir(&canonical_path)?; - for canonical_item_path in directory_paths { - let item = self.status_path_to_item(&canonical_item_path)?; - match &item { - StatusItem::Link { origin, .. } - | StatusItem::Directory { origin, .. } => { - collected_rel_paths.insert(origin.to_owned()); - } - _ => {} - } - items.push(item); - } - - for dir_node in self.depot.read_dir(&relative_path)? { - match dir_node { - DirNode::Link(link_id) => { - let link_view = self.depot.link_view(link_id); - let link_rel_path = link_view.origin(); - let link_rel_dest = link_view.destination(); - if !collected_rel_paths.contains(link_rel_path) { - items.push(StatusItem::Link { - origin: link_rel_path.to_owned(), - destination: link_rel_dest.to_owned(), - is_directory: false, - }); - } - } - DirNode::Directory(_) => {} - } - } - - StatusItem::Directory { - origin: relative_path, - items, - } - } else { - StatusItem::Unlinked { - origin: relative_path, - is_directory: true, - } - } - } else if let Some(link_id) = self.depot.link_find(&relative_path)? { - let destination = self.depot.link_view(link_id).destination().to_owned(); - StatusItem::Link { - origin: relative_path, - destination, - is_directory: false, - } - } else { - StatusItem::Unlinked { - origin: relative_path, - is_directory: false, - } - }; - Ok(item) - } - fn status_print_item(&self, item: StatusItem, depth: u32) -> anyhow::Result<()> { - fn print_depth(d: u32) { - for _ in 0..d.saturating_sub(1) { - print!(" "); - } - } - fn origin_color(exists: bool, is_installed: bool) -> Color { - if !exists { - Color::Red - } else if is_installed { - Color::Green - } else { - Color::RGB(255, 127, 0) - } - } - - let destination_color = Color::Blue; - - print_depth(depth); - match item { - StatusItem::Link { - origin, - destination, - is_directory, - } => { - let canonical_origin = self.depot_dir.join(&origin); - let canonical_destination = self.install_base.join(&destination); - let file_name = Self::status_get_filename(&canonical_origin); - let is_installed = - self.symlink_is_installed(&canonical_origin, &canonical_destination)?; - let exists = canonical_origin.exists(); - let origin_color = origin_color(exists, is_installed); - let directory_extra = if is_directory { "/" } else { "" }; - println!( - "{}{} -> {}", - origin_color.paint(file_name), - directory_extra, - destination_color.paint(destination.display().to_string()) - ); - } - StatusItem::Directory { origin, mut items } => { - items.sort_by(|a, b| StatusItem::display_ord_cmp(a, b).reverse()); - let directory_name = Self::status_get_filename(&origin); - if depth != 0 { - println!("{}/", directory_name); - } - for item in items { - self.status_print_item(item, depth + 1)?; - } - } - StatusItem::Unlinked { - origin, - is_directory, - } => { - let file_name = Self::status_get_filename(&origin); - let directory_extra = if is_directory { "/" } else { "" }; - println!("{}{}", file_name, directory_extra); - } - } - Ok(()) - } - fn status_get_filename(path: &Path) -> &str { - path.file_name() - .and_then(|s| s.to_str()) - .unwrap_or_default() - } - - fn prepare_relative_path(&self, origin: &Path) -> anyhow::Result { - let canonical = utils::weakly_canonical(origin); - let relative = canonical - .strip_prefix(&self.depot_dir) - .context("Invalid origin path, not under depot directory")?; - Ok(relative.to_owned()) - } - - fn link_ids_from_paths_iter( - &self, - paths: impl Iterator>, - ) -> anyhow::Result> { - let mut link_ids = HashSet::::default(); - for path in paths { - let path = self.prepare_relative_path(path.as_ref())?; - link_ids.extend(self.depot.links_under(&path)?); - } - Ok(Vec::from_iter(link_ids.into_iter())) - } - - fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result { - let canonical_pair = self.canonical_pair_from_link_id(link_id); - self.symlink_is_installed(&canonical_pair.origin, &canonical_pair.destination) - } - - fn symlink_is_installed(&self, origin: &Path, destination: &Path) -> anyhow::Result { - debug_assert!(origin.is_absolute()); - debug_assert!(destination.is_absolute()); - - if destination.is_symlink() { - let symlink_destination = destination.read_link()?; - match symlink_destination.canonicalize() { - Ok(canonicalized) => Ok(origin == canonicalized), - Err(_) => Ok(false), - } - } else { - Ok(false) - } - } - - fn symlink_install_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { - let canonical_pair = self.canonical_pair_from_link_id(link_id); - self.symlink_install(&canonical_pair.origin, &canonical_pair.destination) - } - - fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - debug_assert!(origin.is_absolute()); - debug_assert!(destination.is_absolute()); - log::debug!( - "symlink_install : {} -> {}", - origin.display(), - destination.display() - ); - - let destination_parent = destination - .parent() - .ok_or_else(|| anyhow::anyhow!("destination has no parent component"))?; - std::fs::create_dir_all(destination_parent).context("Failed to create directories")?; - // need to do this beacause if the destination path ends in '/' because the symlink - // functions will treat it as a directory but we want a file with that name. - let destination = destination.with_file_name(destination.file_name().unwrap()); - - let destination_exists = destination.exists(); - let destination_is_symlink = destination.is_symlink(); - - if destination_exists && !destination_is_symlink { - return Err(anyhow::anyhow!("destination already exists")); - } - - if destination_is_symlink { - log::debug!("symlink already exists, removing before recreating"); - std::fs::remove_file(&destination)?; - } - - log::debug!( - "creating filesystem symlink {} -> {}", - origin.display(), - destination.display() - ); - std::os::unix::fs::symlink(origin, destination).context("failed to create symlink")?; - - Ok(()) - } - - fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - debug_assert!(origin.is_absolute()); - debug_assert!(destination.is_absolute()); - let destination = destination.with_file_name(destination.file_name().unwrap()); - - if destination.is_symlink() { - let symlink_destination = destination.read_link()?.canonicalize()?; - if symlink_destination == origin { - std::fs::remove_file(&destination)?; - } - } - - Ok(()) - } - - fn symlink_uninstall_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { - let canonical_pair = self.canonical_pair_from_link_id(link_id); - self.symlink_uninstall(&canonical_pair.origin, &canonical_pair.destination) - } - - fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair { - let link_view = self.depot.link_view(link_id); - let relative_origin = link_view.origin(); - let relative_destination = link_view.destination(); - let canonical_origin = self.depot_dir.join(relative_origin); - let canonical_destination = self.install_base.join(relative_destination); - CanonicalPair { - origin: canonical_origin, - destination: canonical_destination, - } - } - } - - pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { - let depot_path = depot_path - .canonicalize() - .context("Failed to canonicalize depot path")?; - let install_base = install_base - .canonicalize() - .context("Failed to canonicalize install base")?; - if !install_base.is_dir() { - return Err(anyhow::anyhow!("Install base must be a directory")); - } - let depot = depot::read(&depot_path)?; - Dotup::new(depot, depot_path, install_base) - } - - pub fn write(dotup: &Dotup) -> anyhow::Result<()> { - depot::write(&dotup.depot_path, &dotup.depot)?; - Ok(()) - } -} - -mod utils { - use std::{ - collections::VecDeque, - path::{Component, Path, PathBuf}, - }; - - use crate::{ - dotup::{self, Dotup}, - Flags, - }; - - pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; - - /// Returns a list of canonical paths to all the files in `dir`. This includes files in - /// subdirectories. - /// Fails if dir isnt a directory or if there is some other io error. - pub fn collect_files_in_dir_recursive(dir: impl Into) -> anyhow::Result> { - let mut paths = Vec::new(); - let mut dirs = VecDeque::new(); - dirs.push_back(dir.into()); - - while let Some(dir) = dirs.pop_front() { - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let filetype = entry.file_type()?; - if filetype.is_dir() { - dirs.push_back(entry.path()); - } else { - paths.push(entry.path()); - } - } - } - - Ok(paths) - } - - pub fn collect_paths_in_dir(dir: impl AsRef) -> anyhow::Result> { - Ok(std::fs::read_dir(dir)? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .collect()) - } - - pub fn read_dotup(flags: &Flags) -> anyhow::Result { - let depot_path = depot_path_from_flags(flags)?; - let install_base = install_base_from_flags(flags); - dotup::read(depot_path, install_base) - } - - pub fn write_dotup(dotup: &Dotup) -> anyhow::Result<()> { - dotup::write(dotup) - } - - pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result { - match flags.depot { - Some(ref path) => Ok(path.clone()), - None => find_depot_path().ok_or_else(|| anyhow::anyhow!("Failed to find depot path")), - } - } - - pub fn default_depot_path() -> PathBuf { - current_working_directory().join(DEFAULT_DEPOT_FILE_NAME) - } - - pub fn find_depot_path() -> Option { - let mut cwd = current_working_directory(); - loop { - let path = cwd.join(DEFAULT_DEPOT_FILE_NAME); - if path.exists() { - break Some(path); - } - if !cwd.pop() { - break None; - } - } - } - - pub fn install_base_from_flags(flags: &Flags) -> PathBuf { - match flags.install_base { - Some(ref path) => path.clone(), - None => default_install_base(), - } - } - - pub fn default_install_base() -> PathBuf { - PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable")) - } - pub fn weakly_canonical(path: impl AsRef) -> PathBuf { - let cwd = current_working_directory(); - 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 - } - - pub fn current_working_directory() -> PathBuf { - std::env::current_dir().expect("Failed to obtain current working directory") - } - - #[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) - ); - } - } -} +mod depot; +mod dotup; +mod utils; use std::path::PathBuf; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..a7e945a --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,156 @@ +use std::{ + collections::VecDeque, + path::{Component, Path, PathBuf}, +}; + +use crate::{ + dotup::{self, Dotup}, + Flags, +}; + +pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; + +/// Returns a list of canonical paths to all the files in `dir`. This includes files in +/// subdirectories. +/// Fails if dir isnt a directory or if there is some other io error. +pub fn collect_files_in_dir_recursive(dir: impl Into) -> anyhow::Result> { + let mut paths = Vec::new(); + let mut dirs = VecDeque::new(); + dirs.push_back(dir.into()); + + while let Some(dir) = dirs.pop_front() { + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let filetype = entry.file_type()?; + if filetype.is_dir() { + dirs.push_back(entry.path()); + } else { + paths.push(entry.path()); + } + } + } + + Ok(paths) +} + +pub fn collect_paths_in_dir(dir: impl AsRef) -> anyhow::Result> { + Ok(std::fs::read_dir(dir)? + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .collect()) +} + +pub fn read_dotup(flags: &Flags) -> anyhow::Result { + let depot_path = depot_path_from_flags(flags)?; + let install_base = install_base_from_flags(flags); + dotup::read(depot_path, install_base) +} + +pub fn write_dotup(dotup: &Dotup) -> anyhow::Result<()> { + dotup::write(dotup) +} + +pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result { + match flags.depot { + Some(ref path) => Ok(path.clone()), + None => find_depot_path().ok_or_else(|| anyhow::anyhow!("Failed to find depot path")), + } +} + +pub fn default_depot_path() -> PathBuf { + current_working_directory().join(DEFAULT_DEPOT_FILE_NAME) +} + +pub fn find_depot_path() -> Option { + let mut cwd = current_working_directory(); + loop { + let path = cwd.join(DEFAULT_DEPOT_FILE_NAME); + if path.exists() { + break Some(path); + } + if !cwd.pop() { + break None; + } + } +} + +pub fn install_base_from_flags(flags: &Flags) -> PathBuf { + match flags.install_base { + Some(ref path) => path.clone(), + None => default_install_base(), + } +} + +pub fn default_install_base() -> PathBuf { + PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable")) +} +pub fn weakly_canonical(path: impl AsRef) -> PathBuf { + let cwd = current_working_directory(); + 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 +} + +pub fn current_working_directory() -> PathBuf { + std::env::current_dir().expect("Failed to obtain current working directory") +} + +#[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) + ); + } +} -- cgit