From 37d767fbc1b0ec5f9afc41e741c56bf2407e837d Mon Sep 17 00:00:00 2001 From: diogo464 Date: Tue, 8 Feb 2022 08:47:26 +0000 Subject: snapshot --- src/main.rs | 914 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 900 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index f623450..9b17787 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,847 @@ // TODO: rewrite all errors so they start with lower case -pub mod depot { +mod depot { + 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, + #[error("{0}")] + LinkCreateError(#[from] LinkCreateError), + } + + #[derive(Debug, Error)] + enum LinkCreateError { + #[error("an ancestor of this path is already linked")] + AncestorAlreadyLinked, + } + + #[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)] + pub 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) + } + + 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 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(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_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 depot2 { use std::{ collections::HashSet, ffi::{OsStr, OsString}, @@ -933,19 +1773,16 @@ pub mod dotup { } } - pub fn unlink(&mut self, paths: impl Iterator>) { + 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 search_results = self.depot.link_search(&origin)?; - match search_results { - depot::SearchResult::Found(link_id) => { - self.depot.link_remove(link_id); - println!("removed link {}", origin.display()); - } - depot::SearchResult::Ancestor(_) | depot::SearchResult::NotFound => { - println!("{} is not linked", origin.display()) + 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 { @@ -1149,7 +1986,7 @@ pub mod dotup { } else if is_installed { Color::Green } else { - Color::Cyan + Color::RGB(255, 127, 0) } } @@ -1301,6 +2138,11 @@ pub mod dotup { 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(); @@ -1336,7 +2178,10 @@ pub mod dotup { } mod utils { - use std::path::{Component, Path, PathBuf}; + use std::{ + collections::VecDeque, + path::{Component, Path, PathBuf}, + }; use crate::{ dotup::{self, Dotup}, @@ -1345,6 +2190,29 @@ mod utils { 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()) @@ -1522,6 +2390,7 @@ fn main() -> anyhow::Result<()> { 2 => "debug", _ => "trace", }; + let log_level = "trace"; Logger::try_with_env_or_str(log_level)? .format(flexi_logger::colored_default_format) @@ -1565,31 +2434,48 @@ fn command_init(_global_flags: Flags, args: InitArgs) -> anyhow::Result<()> { #[derive(Parser, Debug)] struct LinkArgs { + #[clap(long)] + directory: bool, + origin: PathBuf, + destination: PathBuf, } fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> { let mut dotup = utils::read_dotup(&global_flags)?; - dotup.link(args.origin, args.destination); + let origins = if args.directory { + vec![args.origin] + } else { + utils::collect_files_in_dir_recursive(args.origin)? + }; + for origin in origins { + dotup.link(origin, &args.destination); + } utils::write_dotup(&dotup)?; Ok(()) } #[derive(Parser, Debug)] struct UnlinkArgs { + #[clap(long)] + uninstall: bool, + paths: Vec, } fn command_unlink(global_flags: Flags, args: UnlinkArgs) -> anyhow::Result<()> { let mut dotup = utils::read_dotup(&global_flags)?; - dotup.unlink(args.paths.into_iter()); + dotup.unlink(args.paths.into_iter(), args.uninstall); utils::write_dotup(&dotup)?; Ok(()) } #[derive(Parser, Debug)] struct InstallArgs { + #[clap(long)] + directory: bool, + paths: Vec, } -- cgit