diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 1561 |
1 files changed, 3 insertions, 1558 deletions
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 @@ | |||
| 1 | #![feature(try_blocks)] | 1 | #![feature(try_blocks)] |
| 2 | 2 | ||
| 3 | // TODO: rewrite all errors so they start with lower case | 3 | mod depot; |
| 4 | 4 | mod dotup; | |
| 5 | mod depot { | 5 | mod utils; |
| 6 | use anyhow::Context; | ||
| 7 | use std::{ | ||
| 8 | collections::HashSet, | ||
| 9 | ffi::{OsStr, OsString}, | ||
| 10 | ops::Index, | ||
| 11 | path::{Path, PathBuf}, | ||
| 12 | }; | ||
| 13 | use thiserror::Error; | ||
| 14 | |||
| 15 | use slotmap::{Key, SlotMap}; | ||
| 16 | |||
| 17 | //pub type Result<T, E = DepotError> = std::result::Result<T, E>; | ||
| 18 | pub use anyhow::Result; | ||
| 19 | pub use disk::{read, write}; | ||
| 20 | |||
| 21 | slotmap::new_key_type! {pub struct LinkID;} | ||
| 22 | slotmap::new_key_type! {struct NodeID;} | ||
| 23 | |||
| 24 | #[derive(Debug, Error)] | ||
| 25 | enum DepotError { | ||
| 26 | #[error("path must be relative")] | ||
| 27 | InvalidPath, | ||
| 28 | #[error("path must be relative and not empty")] | ||
| 29 | InvalidLinkPath, | ||
| 30 | } | ||
| 31 | |||
| 32 | #[derive(Debug, Clone)] | ||
| 33 | struct Node { | ||
| 34 | comp: OsString, | ||
| 35 | parent: NodeID, | ||
| 36 | kind: NodeKind, | ||
| 37 | } | ||
| 38 | |||
| 39 | #[derive(Debug, Clone)] | ||
| 40 | enum NodeKind { | ||
| 41 | Link(LinkID), | ||
| 42 | Directory(HashSet<NodeID>), | ||
| 43 | } | ||
| 44 | |||
| 45 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 46 | enum NodeSearchResult { | ||
| 47 | Found(NodeID), | ||
| 48 | /// the closest NodeID up the the search point. | ||
| 49 | NotFound(NodeID), | ||
| 50 | } | ||
| 51 | |||
| 52 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 53 | pub enum DirNode { | ||
| 54 | Link(LinkID), | ||
| 55 | Directory(PathBuf), | ||
| 56 | } | ||
| 57 | |||
| 58 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 59 | pub enum SearchResult { | ||
| 60 | Found(LinkID), | ||
| 61 | Ancestor(LinkID), | ||
| 62 | NotFound, | ||
| 63 | } | ||
| 64 | |||
| 65 | #[derive(Debug, Clone)] | ||
| 66 | struct Link { | ||
| 67 | origin: PathBuf, | ||
| 68 | destination: PathBuf, | ||
| 69 | origin_id: NodeID, | ||
| 70 | } | ||
| 71 | |||
| 72 | #[derive(Debug)] | ||
| 73 | pub struct LinkView<'a> { | ||
| 74 | link_id: LinkID, | ||
| 75 | depot: &'a Depot, | ||
| 76 | } | ||
| 77 | |||
| 78 | impl<'a> LinkView<'a> { | ||
| 79 | pub fn origin(&self) -> &Path { | ||
| 80 | &self.depot.links[self.link_id].origin | ||
| 81 | } | ||
| 82 | |||
| 83 | pub fn destination(&self) -> &Path { | ||
| 84 | &self.depot.links[self.link_id].destination | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | #[derive(Debug, Clone)] | ||
| 89 | struct DepotTree { | ||
| 90 | root: NodeID, | ||
| 91 | nodes: SlotMap<NodeID, Node>, | ||
| 92 | } | ||
| 93 | |||
| 94 | impl Default for DepotTree { | ||
| 95 | fn default() -> Self { | ||
| 96 | let mut nodes = SlotMap::<NodeID, Node>::default(); | ||
| 97 | let root = nodes.insert(Node { | ||
| 98 | comp: Default::default(), | ||
| 99 | parent: Default::default(), | ||
| 100 | kind: NodeKind::Directory(Default::default()), | ||
| 101 | }); | ||
| 102 | Self { root, nodes } | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | impl Index<NodeID> for DepotTree { | ||
| 107 | type Output = Node; | ||
| 108 | |||
| 109 | fn index(&self, index: NodeID) -> &Self::Output { | ||
| 110 | self.nodes.index(index) | ||
| 111 | } | ||
| 112 | } | ||
| 113 | |||
| 114 | impl DepotTree { | ||
| 115 | /// create a node of kind [`NodeKind::Link`]. | ||
| 116 | pub fn link_create(&mut self, path: &Path, link_id: LinkID) -> Result<NodeID> { | ||
| 117 | debug_assert!(path_verify_link(path).is_ok()); | ||
| 118 | |||
| 119 | let path_search_result = self.search(path); | ||
| 120 | |||
| 121 | // handle the error cases | ||
| 122 | match path_search_result { | ||
| 123 | NodeSearchResult::Found(node_id) => { | ||
| 124 | let node = &self.nodes[node_id]; | ||
| 125 | match &node.kind { | ||
| 126 | NodeKind::Link(_) => Err(anyhow::anyhow!("link already exists")), | ||
| 127 | NodeKind::Directory(_) => { | ||
| 128 | Err(anyhow::anyhow!("path already has links under it")) | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | ||
| 132 | NodeSearchResult::NotFound(ancestor_node_id) => { | ||
| 133 | let ancestor_node = &self.nodes[ancestor_node_id]; | ||
| 134 | match &ancestor_node.kind { | ||
| 135 | NodeKind::Link(_) => Err(anyhow::anyhow!( | ||
| 136 | "an ancestor of this path is already linked" | ||
| 137 | )), | ||
| 138 | NodeKind::Directory(_) => Ok(()), | ||
| 139 | } | ||
| 140 | } | ||
| 141 | }?; | ||
| 142 | |||
| 143 | // create the node | ||
| 144 | // unwrap: this is a verfied link path, it must have atleast one component | ||
| 145 | let filename = path.file_name().unwrap(); | ||
| 146 | let parent_path = path_parent_or_empty(path); | ||
| 147 | let node_id = self.nodes.insert(Node { | ||
| 148 | comp: filename.to_owned(), | ||
| 149 | parent: Default::default(), | ||
| 150 | kind: NodeKind::Link(link_id), | ||
| 151 | }); | ||
| 152 | let parent_id = self.directory_get_or_create(parent_path, node_id); | ||
| 153 | self.nodes[node_id].parent = parent_id; | ||
| 154 | Ok(node_id) | ||
| 155 | } | ||
| 156 | |||
| 157 | pub fn link_update_id(&mut self, node_id: NodeID, link_id: LinkID) { | ||
| 158 | let node = &mut self.nodes[node_id]; | ||
| 159 | match &mut node.kind { | ||
| 160 | NodeKind::Link(lid) => *lid = link_id, | ||
| 161 | NodeKind::Directory(_) => unreachable!(), | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | /// attempts to moves a node of kind [`NodeKind::Link`] to `destination`. | ||
| 166 | pub fn link_move(&mut self, node_id: NodeID, destination: &Path) -> Result<()> { | ||
| 167 | let parent_id = self.nodes[node_id].parent; | ||
| 168 | let parent = &mut self.nodes[parent_id]; | ||
| 169 | |||
| 170 | // remove the node from its parent temporarily so that the search never returns this | ||
| 171 | // link and that way any link will find means an error. | ||
| 172 | // if an error does happen then we re-add this node to its parent to keep the data | ||
| 173 | // consistent. | ||
| 174 | match &mut parent.kind { | ||
| 175 | NodeKind::Link(_) => unreachable!(), | ||
| 176 | NodeKind::Directory(children) => children.remove(&node_id), | ||
| 177 | }; | ||
| 178 | |||
| 179 | let search_result = self.search(destination); | ||
| 180 | // handle the error cases | ||
| 181 | match search_result { | ||
| 182 | NodeSearchResult::Found(found_id) => { | ||
| 183 | assert!(found_id != node_id); | ||
| 184 | self.directory_add_child(parent_id, node_id); | ||
| 185 | return Err(anyhow::anyhow!("link already exists at that path")); | ||
| 186 | } | ||
| 187 | NodeSearchResult::NotFound(ancestor_id) => { | ||
| 188 | let ancestor = &self.nodes[ancestor_id]; | ||
| 189 | match &ancestor.kind { | ||
| 190 | NodeKind::Link(_) => { | ||
| 191 | self.directory_add_child(parent_id, node_id); | ||
| 192 | return Err(anyhow::anyhow!("ancestor path is already linked")); | ||
| 193 | } | ||
| 194 | NodeKind::Directory(_) => {} | ||
| 195 | } | ||
| 196 | } | ||
| 197 | }; | ||
| 198 | |||
| 199 | let destination_parent = path_parent_or_empty(destination); | ||
| 200 | let new_parent_id = self.directory_get_or_create(destination_parent, node_id); | ||
| 201 | if new_parent_id != parent_id { | ||
| 202 | self.nodes[node_id].parent = new_parent_id; | ||
| 203 | |||
| 204 | // we have to re-add and call the remove function because it could lead to the removal | ||
| 205 | // of several directories if they become empty after this remove. | ||
| 206 | self.directory_add_child(parent_id, node_id); | ||
| 207 | self.directory_remove_child(parent_id, node_id); | ||
| 208 | } | ||
| 209 | |||
| 210 | // unwrap: destination is a verified link path so it has atleast 1 component | ||
| 211 | let comp = destination.file_name().unwrap(); | ||
| 212 | let node = &mut self.nodes[node_id]; | ||
| 213 | if node.comp != comp { | ||
| 214 | node.comp = comp.to_owned(); | ||
| 215 | } | ||
| 216 | |||
| 217 | Ok(()) | ||
| 218 | } | ||
| 219 | |||
| 220 | pub fn link_search(&self, path: &Path) -> SearchResult { | ||
| 221 | match self.search(path) { | ||
| 222 | NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { | ||
| 223 | NodeKind::Link(link_id) => SearchResult::Found(*link_id), | ||
| 224 | NodeKind::Directory(_) => SearchResult::NotFound, | ||
| 225 | }, | ||
| 226 | NodeSearchResult::NotFound(node_id) => match &self.nodes[node_id].kind { | ||
| 227 | NodeKind::Link(link_id) => SearchResult::Ancestor(*link_id), | ||
| 228 | NodeKind::Directory(_) => SearchResult::NotFound, | ||
| 229 | }, | ||
| 230 | } | ||
| 231 | } | ||
| 232 | |||
| 233 | /// remove a node of kind [`NodeKind::Link`]. | ||
| 234 | pub fn link_remove(&mut self, node_id: NodeID) { | ||
| 235 | let node = &self.nodes[node_id]; | ||
| 236 | assert!(std::matches!(node.kind, NodeKind::Link(_))); | ||
| 237 | let parent_id = node.parent; | ||
| 238 | self.nodes.remove(node_id); | ||
| 239 | self.directory_remove_child(parent_id, node_id); | ||
| 240 | } | ||
| 241 | |||
| 242 | pub fn links_under(&self, path: &Path) -> impl Iterator<Item = LinkID> + '_ { | ||
| 243 | let links = match self.search(path) { | ||
| 244 | NodeSearchResult::Found(node_id) => { | ||
| 245 | let node = &self.nodes[node_id]; | ||
| 246 | match &node.kind { | ||
| 247 | NodeKind::Link(link_id) => vec![*link_id], | ||
| 248 | NodeKind::Directory(children) => { | ||
| 249 | let mut links = Vec::new(); | ||
| 250 | let mut node_ids = Vec::from_iter(children.iter().copied()); | ||
| 251 | while let Some(child_id) = node_ids.pop() { | ||
| 252 | let child = &self.nodes[child_id]; | ||
| 253 | match &child.kind { | ||
| 254 | NodeKind::Link(link_id) => links.push(*link_id), | ||
| 255 | NodeKind::Directory(extra_children) => { | ||
| 256 | node_ids.extend(extra_children.iter().copied()) | ||
| 257 | } | ||
| 258 | } | ||
| 259 | } | ||
| 260 | links | ||
| 261 | } | ||
| 262 | } | ||
| 263 | } | ||
| 264 | NodeSearchResult::NotFound(_) => vec![], | ||
| 265 | }; | ||
| 266 | links.into_iter() | ||
| 267 | } | ||
| 268 | |||
| 269 | pub fn has_links_under(&self, path: &Path) -> bool { | ||
| 270 | // it does not matter what type of node is found. if a directory exists then there | ||
| 271 | // must be atleast one link under it. | ||
| 272 | match self.search(path) { | ||
| 273 | NodeSearchResult::Found(_) => true, | ||
| 274 | NodeSearchResult::NotFound(_) => false, | ||
| 275 | } | ||
| 276 | } | ||
| 277 | |||
| 278 | pub fn read_dir(&self, path: &Path) -> Result<impl Iterator<Item = DirNode> + '_> { | ||
| 279 | match self.search(path) { | ||
| 280 | NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { | ||
| 281 | NodeKind::Link(_) => Err(anyhow::anyhow!("read dir called on a link")), | ||
| 282 | NodeKind::Directory(children) => Ok(children.iter().map(|child_id| { | ||
| 283 | let child = &self.nodes[*child_id]; | ||
| 284 | match &child.kind { | ||
| 285 | NodeKind::Link(link_id) => DirNode::Link(*link_id), | ||
| 286 | NodeKind::Directory(_) => { | ||
| 287 | DirNode::Directory(self.build_path(*child_id)) | ||
| 288 | } | ||
| 289 | } | ||
| 290 | })), | ||
| 291 | }, | ||
| 292 | NodeSearchResult::NotFound(_) => Err(anyhow::anyhow!("directory not found")), | ||
| 293 | } | ||
| 294 | } | ||
| 295 | |||
| 296 | pub fn build_path(&self, node_id: NodeID) -> PathBuf { | ||
| 297 | fn recursive_helper(nodes: &SlotMap<NodeID, Node>, nid: NodeID, pbuf: &mut PathBuf) { | ||
| 298 | if nid.is_null() { | ||
| 299 | return; | ||
| 300 | } | ||
| 301 | let parent_id = nodes[nid].parent; | ||
| 302 | recursive_helper(nodes, parent_id, pbuf); | ||
| 303 | pbuf.push(&nodes[nid].comp); | ||
| 304 | } | ||
| 305 | |||
| 306 | let mut node_path = PathBuf::default(); | ||
| 307 | recursive_helper(&self.nodes, node_id, &mut node_path); | ||
| 308 | node_path | ||
| 309 | } | ||
| 310 | |||
| 311 | fn search(&self, path: &Path) -> NodeSearchResult { | ||
| 312 | debug_assert!(path_verify(path).is_ok()); | ||
| 313 | |||
| 314 | let mut curr_node_id = self.root; | ||
| 315 | let mut comp_iter = path_iter_comps(path).peekable(); | ||
| 316 | while let Some(comp) = comp_iter.next() { | ||
| 317 | if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { | ||
| 318 | let child = &self.nodes[child_id]; | ||
| 319 | match &child.kind { | ||
| 320 | NodeKind::Link(_) => { | ||
| 321 | if comp_iter.peek().is_some() { | ||
| 322 | return NodeSearchResult::NotFound(child_id); | ||
| 323 | } else { | ||
| 324 | return NodeSearchResult::Found(child_id); | ||
| 325 | } | ||
| 326 | } | ||
| 327 | NodeKind::Directory(_) => curr_node_id = child_id, | ||
| 328 | } | ||
| 329 | } else { | ||
| 330 | return NodeSearchResult::NotFound(curr_node_id); | ||
| 331 | } | ||
| 332 | } | ||
| 333 | NodeSearchResult::Found(curr_node_id) | ||
| 334 | } | ||
| 335 | |||
| 336 | // creates directories all the way up to and including path. | ||
| 337 | // there cannot be any links up to `path`. | ||
| 338 | fn directory_get_or_create(&mut self, path: &Path, initial_child: NodeID) -> NodeID { | ||
| 339 | // TODO: this could be replaced if the search function also returned the depth of the | ||
| 340 | // node and we skip those components and just start creating directories up to the | ||
| 341 | // path. | ||
| 342 | let mut curr_node_id = self.root; | ||
| 343 | for comp in path_iter_comps(path) { | ||
| 344 | if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { | ||
| 345 | debug_assert!(std::matches!( | ||
| 346 | self.nodes[child_id].kind, | ||
| 347 | NodeKind::Directory(_) | ||
| 348 | )); | ||
| 349 | curr_node_id = child_id; | ||
| 350 | } else { | ||
| 351 | let new_node_id = self.nodes.insert(Node { | ||
| 352 | comp: comp.to_owned(), | ||
| 353 | parent: curr_node_id, | ||
| 354 | kind: NodeKind::Directory(Default::default()), | ||
| 355 | }); | ||
| 356 | self.directory_add_child(curr_node_id, new_node_id); | ||
| 357 | curr_node_id = new_node_id; | ||
| 358 | } | ||
| 359 | } | ||
| 360 | self.directory_add_child(curr_node_id, initial_child); | ||
| 361 | curr_node_id | ||
| 362 | } | ||
| 363 | |||
| 364 | fn directory_search_children(&self, node_id: NodeID, comp: &OsStr) -> Option<NodeID> { | ||
| 365 | let node = &self.nodes[node_id]; | ||
| 366 | match &node.kind { | ||
| 367 | NodeKind::Link(_) => unreachable!(), | ||
| 368 | NodeKind::Directory(children) => { | ||
| 369 | for &child_id in children { | ||
| 370 | let child = &self.nodes[child_id]; | ||
| 371 | if child.comp == comp { | ||
| 372 | return Some(child_id); | ||
| 373 | } | ||
| 374 | } | ||
| 375 | } | ||
| 376 | } | ||
| 377 | None | ||
| 378 | } | ||
| 379 | |||
| 380 | fn directory_add_child(&mut self, node_id: NodeID, child_id: NodeID) { | ||
| 381 | let node = &mut self.nodes[node_id]; | ||
| 382 | match &mut node.kind { | ||
| 383 | NodeKind::Link(_) => unreachable!(), | ||
| 384 | NodeKind::Directory(children) => children.insert(child_id), | ||
| 385 | }; | ||
| 386 | } | ||
| 387 | |||
| 388 | fn directory_remove_child(&mut self, node_id: NodeID, child_id: NodeID) { | ||
| 389 | let node = &mut self.nodes[node_id]; | ||
| 390 | match &mut node.kind { | ||
| 391 | NodeKind::Link(_) => unreachable!(), | ||
| 392 | NodeKind::Directory(children) => { | ||
| 393 | children.remove(&child_id); | ||
| 394 | if children.is_empty() && !node.parent.is_null() { | ||
| 395 | let parent_id = node.parent; | ||
| 396 | self.directory_remove_child(parent_id, node_id); | ||
| 397 | } | ||
| 398 | } | ||
| 399 | } | ||
| 400 | } | ||
| 401 | } | ||
| 402 | |||
| 403 | #[derive(Debug, Default, Clone)] | ||
| 404 | pub struct Depot { | ||
| 405 | links: SlotMap<LinkID, Link>, | ||
| 406 | origin: DepotTree, | ||
| 407 | } | ||
| 408 | |||
| 409 | impl Depot { | ||
| 410 | pub fn link_create( | ||
| 411 | &mut self, | ||
| 412 | origin: impl AsRef<Path>, | ||
| 413 | destination: impl AsRef<Path>, | ||
| 414 | ) -> Result<LinkID> { | ||
| 415 | let origin = origin.as_ref(); | ||
| 416 | let destination = destination.as_ref(); | ||
| 417 | path_verify_link(origin)?; | ||
| 418 | path_verify_link(destination)?; | ||
| 419 | self.link_create_unchecked(origin, destination) | ||
| 420 | } | ||
| 421 | |||
| 422 | pub fn link_remove(&mut self, link_id: LinkID) { | ||
| 423 | let node_id = self.links[link_id].origin_id; | ||
| 424 | self.links.remove(link_id); | ||
| 425 | self.origin.link_remove(node_id); | ||
| 426 | } | ||
| 427 | |||
| 428 | /// moves the link specified by `link_id` to the path at `destination`. | ||
| 429 | /// if the link is already at the destination nothing is done. | ||
| 430 | /// if the destination is another link that that link is removed. | ||
| 431 | /// if the destination is under another link then an error is returned. | ||
| 432 | /// `destination` will be the link's new origin. | ||
| 433 | pub fn link_move(&mut self, link_id: LinkID, destination: impl AsRef<Path>) -> Result<()> { | ||
| 434 | let destination = destination.as_ref(); | ||
| 435 | path_verify_link(destination)?; | ||
| 436 | self.link_move_unchecked(link_id, destination) | ||
| 437 | } | ||
| 438 | |||
| 439 | #[allow(unused)] | ||
| 440 | pub fn link_search(&self, path: impl AsRef<Path>) -> Result<SearchResult> { | ||
| 441 | let path = path.as_ref(); | ||
| 442 | path_verify(path)?; | ||
| 443 | Ok(self.link_search_unchecked(path)) | ||
| 444 | } | ||
| 445 | |||
| 446 | pub fn link_find(&self, path: impl AsRef<Path>) -> Result<Option<LinkID>> { | ||
| 447 | let path = path.as_ref(); | ||
| 448 | path_verify(path)?; | ||
| 449 | Ok(self.link_find_unchecked(path)) | ||
| 450 | } | ||
| 451 | |||
| 452 | pub fn links_under( | ||
| 453 | &self, | ||
| 454 | path: impl AsRef<Path>, | ||
| 455 | ) -> Result<impl Iterator<Item = LinkID> + '_> { | ||
| 456 | let path = path.as_ref(); | ||
| 457 | path_verify(path)?; | ||
| 458 | Ok(self.links_under_unchecked(path)) | ||
| 459 | } | ||
| 460 | |||
| 461 | pub fn has_links_under(&self, path: impl AsRef<Path>) -> Result<bool> { | ||
| 462 | let path = path.as_ref(); | ||
| 463 | path_verify(path)?; | ||
| 464 | Ok(self.has_links_under_unchecked(path)) | ||
| 465 | } | ||
| 466 | |||
| 467 | pub fn links_verify_install(&self, link_ids: impl Iterator<Item = LinkID>) -> Result<()> { | ||
| 468 | let mut destination = DepotTree::default(); | ||
| 469 | for link_id in link_ids { | ||
| 470 | let link = &self.links[link_id]; | ||
| 471 | destination | ||
| 472 | .link_create(&link.destination, link_id) | ||
| 473 | .context("link destinations overlap")?; | ||
| 474 | } | ||
| 475 | Ok(()) | ||
| 476 | } | ||
| 477 | |||
| 478 | pub fn link_view(&self, link_id: LinkID) -> LinkView { | ||
| 479 | LinkView { | ||
| 480 | link_id, | ||
| 481 | depot: self, | ||
| 482 | } | ||
| 483 | } | ||
| 484 | |||
| 485 | pub fn read_dir( | ||
| 486 | &self, | ||
| 487 | path: impl AsRef<Path>, | ||
| 488 | ) -> Result<impl Iterator<Item = DirNode> + '_> { | ||
| 489 | let path = path.as_ref(); | ||
| 490 | path_verify(path)?; | ||
| 491 | self.read_dir_unchecked(path) | ||
| 492 | } | ||
| 493 | |||
| 494 | fn link_create_unchecked(&mut self, origin: &Path, destination: &Path) -> Result<LinkID> { | ||
| 495 | let node_id = self.origin.link_create(origin, Default::default())?; | ||
| 496 | let link_id = self.links.insert(Link { | ||
| 497 | origin: origin.to_owned(), | ||
| 498 | destination: destination.to_owned(), | ||
| 499 | origin_id: node_id, | ||
| 500 | }); | ||
| 501 | self.origin.link_update_id(node_id, link_id); | ||
| 502 | Ok(link_id) | ||
| 503 | } | ||
| 504 | |||
| 505 | fn link_move_unchecked(&mut self, link_id: LinkID, destination: &Path) -> Result<()> { | ||
| 506 | let link = &self.links[link_id]; | ||
| 507 | if link.origin == destination { | ||
| 508 | return Ok(()); | ||
| 509 | } | ||
| 510 | let node_id = link.origin_id; | ||
| 511 | self.origin.link_move(node_id, destination)?; | ||
| 512 | self.links[link_id].origin = destination.to_owned(); | ||
| 513 | Ok(()) | ||
| 514 | } | ||
| 515 | |||
| 516 | fn link_search_unchecked(&self, path: &Path) -> SearchResult { | ||
| 517 | self.origin.link_search(path) | ||
| 518 | } | ||
| 519 | |||
| 520 | fn link_find_unchecked(&self, path: &Path) -> Option<LinkID> { | ||
| 521 | match self.link_search_unchecked(path) { | ||
| 522 | SearchResult::Found(link_id) => Some(link_id), | ||
| 523 | _ => None, | ||
| 524 | } | ||
| 525 | } | ||
| 526 | |||
| 527 | fn links_under_unchecked(&self, path: &Path) -> impl Iterator<Item = LinkID> + '_ { | ||
| 528 | self.origin.links_under(path) | ||
| 529 | } | ||
| 530 | |||
| 531 | fn has_links_under_unchecked(&self, path: &Path) -> bool { | ||
| 532 | self.origin.has_links_under(path) | ||
| 533 | } | ||
| 534 | |||
| 535 | fn read_dir_unchecked(&self, path: &Path) -> Result<impl Iterator<Item = DirNode> + '_> { | ||
| 536 | self.origin.read_dir(path) | ||
| 537 | } | ||
| 538 | } | ||
| 539 | |||
| 540 | /// a verified link path is a path that: | ||
| 541 | /// + is not empty | ||
| 542 | /// + is relative | ||
| 543 | /// + does not contain Prefix/RootDir/ParentDir | ||
| 544 | fn path_verify_link(path: &Path) -> Result<()> { | ||
| 545 | // make sure the path is not empty | ||
| 546 | if path.components().next().is_none() { | ||
| 547 | return Err(DepotError::InvalidLinkPath.into()); | ||
| 548 | } | ||
| 549 | path_verify(path).map_err(|_| DepotError::InvalidLinkPath.into()) | ||
| 550 | } | ||
| 551 | |||
| 552 | /// a verified path is a path that: | ||
| 553 | /// + is not empty | ||
| 554 | /// + is relative | ||
| 555 | /// + does not contain Prefix/RootDir/ParentDir | ||
| 556 | fn path_verify(path: &Path) -> Result<()> { | ||
| 557 | // make sure the path is relative | ||
| 558 | // make sure the path does not contain '.' or '..' | ||
| 559 | for component in path.components() { | ||
| 560 | match component { | ||
| 561 | std::path::Component::Prefix(_) | ||
| 562 | | std::path::Component::RootDir | ||
| 563 | | std::path::Component::CurDir | ||
| 564 | | std::path::Component::ParentDir => return Err(DepotError::InvalidPath.into()), | ||
| 565 | std::path::Component::Normal(_) => {} | ||
| 566 | } | ||
| 567 | } | ||
| 568 | Ok(()) | ||
| 569 | } | ||
| 570 | |||
| 571 | fn path_parent_or_empty(path: &Path) -> &Path { | ||
| 572 | path.parent().unwrap_or_else(|| Path::new("")) | ||
| 573 | } | ||
| 574 | |||
| 575 | /// Iterate over the components of a path. | ||
| 576 | /// # Pre | ||
| 577 | /// The path can only have "Normal" components. | ||
| 578 | fn path_iter_comps(path: &Path) -> impl Iterator<Item = &OsStr> { | ||
| 579 | debug_assert!(path_verify(path).is_ok()); | ||
| 580 | path.components().map(|component| match component { | ||
| 581 | std::path::Component::Normal(comp) => comp, | ||
| 582 | _ => unreachable!(), | ||
| 583 | }) | ||
| 584 | } | ||
| 585 | |||
| 586 | mod disk { | ||
| 587 | use std::path::{Path, PathBuf}; | ||
| 588 | |||
| 589 | use anyhow::Context; | ||
| 590 | use serde::{Deserialize, Serialize}; | ||
| 591 | |||
| 592 | use super::Depot; | ||
| 593 | |||
| 594 | #[derive(Debug, Serialize, Deserialize)] | ||
| 595 | struct DiskLink { | ||
| 596 | origin: PathBuf, | ||
| 597 | destination: PathBuf, | ||
| 598 | } | ||
| 599 | |||
| 600 | #[derive(Debug, Serialize, Deserialize)] | ||
| 601 | struct DiskLinks { | ||
| 602 | links: Vec<DiskLink>, | ||
| 603 | } | ||
| 604 | |||
| 605 | pub fn read(path: &Path) -> anyhow::Result<Depot> { | ||
| 606 | let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; | ||
| 607 | let disk_links = toml::from_str::<DiskLinks>(&contents) | ||
| 608 | .context("Failed to parse depot file")? | ||
| 609 | .links; | ||
| 610 | let mut depot = Depot::default(); | ||
| 611 | for disk_link in disk_links { | ||
| 612 | depot | ||
| 613 | .link_create(disk_link.origin, disk_link.destination) | ||
| 614 | .context("Failed to build depot from file. File is in an invalid state")?; | ||
| 615 | } | ||
| 616 | Ok(depot) | ||
| 617 | } | ||
| 618 | |||
| 619 | pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { | ||
| 620 | let mut links = Vec::with_capacity(depot.links.len()); | ||
| 621 | for (_, link) in depot.links.iter() { | ||
| 622 | links.push(DiskLink { | ||
| 623 | origin: link.origin.clone(), | ||
| 624 | destination: link.destination.clone(), | ||
| 625 | }); | ||
| 626 | } | ||
| 627 | let contents = toml::to_string_pretty(&DiskLinks { links }) | ||
| 628 | .context("Failed to serialize depot")?; | ||
| 629 | std::fs::write(path, contents).context("Failed to write depot to file")?; | ||
| 630 | Ok(()) | ||
| 631 | } | ||
| 632 | } | ||
| 633 | |||
| 634 | #[cfg(test)] | ||
| 635 | mod tests { | ||
| 636 | use super::*; | ||
| 637 | |||
| 638 | #[test] | ||
| 639 | fn test_depot_link_create() { | ||
| 640 | let mut depot = Depot::default(); | ||
| 641 | let f1 = depot.link_create("f1", "f1").unwrap(); | ||
| 642 | let f2 = depot.link_create("f2", "f2").unwrap(); | ||
| 643 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 644 | let f4 = depot.link_create("d1/d2/f4", "d1/d2/d4").unwrap(); | ||
| 645 | |||
| 646 | assert_eq!(depot.link_find("f1").unwrap(), Some(f1)); | ||
| 647 | assert_eq!(depot.link_find("f2").unwrap(), Some(f2)); | ||
| 648 | assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); | ||
| 649 | assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); | ||
| 650 | |||
| 651 | depot.link_create("f2", "").unwrap_err(); | ||
| 652 | depot.link_create("", "d4").unwrap_err(); | ||
| 653 | depot.link_create("f1/f3", "f3").unwrap_err(); | ||
| 654 | } | ||
| 655 | |||
| 656 | #[test] | ||
| 657 | fn test_depot_link_remove() { | ||
| 658 | let mut depot = Depot::default(); | ||
| 659 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 660 | let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 661 | let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 662 | let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 663 | let d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 664 | |||
| 665 | depot.link_remove(f2); | ||
| 666 | assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); | ||
| 667 | assert_eq!(depot.link_find("d1/f2").unwrap(), None); | ||
| 668 | depot.link_remove(f4); | ||
| 669 | assert_eq!(depot.link_find("d1/d2/f4").unwrap(), None); | ||
| 670 | depot.link_remove(d3); | ||
| 671 | assert_eq!(depot.link_find("d3").unwrap(), None); | ||
| 672 | } | ||
| 673 | |||
| 674 | #[test] | ||
| 675 | fn test_depot_link_move() { | ||
| 676 | let mut depot = Depot::default(); | ||
| 677 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 678 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 679 | |||
| 680 | depot.link_move(f1, "").unwrap_err(); | ||
| 681 | depot.link_move(f1, "d1/f2/f1").unwrap_err(); | ||
| 682 | depot.link_move(f1, "d1/f2").unwrap_err(); | ||
| 683 | |||
| 684 | depot.link_move(f1, "f1").unwrap(); | ||
| 685 | assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); | ||
| 686 | depot.link_move(f1, "f2").unwrap(); | ||
| 687 | assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); | ||
| 688 | assert_eq!(depot.link_find("f2").unwrap(), Some(f1)); | ||
| 689 | } | ||
| 690 | |||
| 691 | #[test] | ||
| 692 | fn test_depot_link_search() { | ||
| 693 | let mut depot = Depot::default(); | ||
| 694 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 695 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 696 | let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 697 | let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 698 | let _d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 699 | |||
| 700 | assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); | ||
| 701 | assert_eq!( | ||
| 702 | depot.link_search("d1/f1/f5").unwrap(), | ||
| 703 | SearchResult::Ancestor(f1) | ||
| 704 | ); | ||
| 705 | assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound); | ||
| 706 | assert_eq!( | ||
| 707 | depot.link_search("d1/d2/f5").unwrap(), | ||
| 708 | SearchResult::NotFound | ||
| 709 | ); | ||
| 710 | } | ||
| 711 | |||
| 712 | #[test] | ||
| 713 | fn test_depot_link_find() { | ||
| 714 | let mut depot = Depot::default(); | ||
| 715 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 716 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 717 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 718 | let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 719 | let d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 720 | |||
| 721 | assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); | ||
| 722 | assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); | ||
| 723 | assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); | ||
| 724 | assert_eq!(depot.link_find("d3").unwrap(), Some(d3)); | ||
| 725 | |||
| 726 | assert_eq!(depot.link_find("d5").unwrap(), None); | ||
| 727 | assert_eq!(depot.link_find("d3/d5").unwrap(), None); | ||
| 728 | assert_eq!(depot.link_find("d1/d2/f5").unwrap(), None); | ||
| 729 | } | ||
| 730 | |||
| 731 | #[test] | ||
| 732 | fn test_depot_links_under() { | ||
| 733 | let mut depot = Depot::default(); | ||
| 734 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 735 | let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 736 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 737 | let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 738 | let d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 739 | |||
| 740 | let under_f1 = depot.links_under("d1/f1").unwrap().collect::<Vec<_>>(); | ||
| 741 | assert_eq!(under_f1, vec![f1]); | ||
| 742 | |||
| 743 | let under_d1 = depot.links_under("d1").unwrap().collect::<Vec<_>>(); | ||
| 744 | let expected_under_d1 = vec![f1, f2, f3, f4]; | ||
| 745 | assert!( | ||
| 746 | under_d1.len() == expected_under_d1.len() | ||
| 747 | && expected_under_d1.iter().all(|x| under_d1.contains(x)) | ||
| 748 | ); | ||
| 749 | |||
| 750 | let under_d2 = depot.links_under("d2").unwrap().collect::<Vec<_>>(); | ||
| 751 | assert_eq!(under_d2, vec![]); | ||
| 752 | |||
| 753 | let under_d3 = depot.links_under("d3").unwrap().collect::<Vec<_>>(); | ||
| 754 | assert_eq!(under_d3, vec![d3]); | ||
| 755 | |||
| 756 | let under_root = depot.links_under("").unwrap().collect::<Vec<_>>(); | ||
| 757 | let expected_under_root = vec![f1, f2, f3, f4, d3]; | ||
| 758 | assert!( | ||
| 759 | under_root.len() == expected_under_root.len() | ||
| 760 | && expected_under_root.iter().all(|x| under_root.contains(x)) | ||
| 761 | ); | ||
| 762 | } | ||
| 763 | |||
| 764 | #[test] | ||
| 765 | fn test_depot_has_links_under() { | ||
| 766 | let mut depot = Depot::default(); | ||
| 767 | let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 768 | let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 769 | let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 770 | let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 771 | let _d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 772 | |||
| 773 | assert!(depot.has_links_under("").unwrap()); | ||
| 774 | assert!(depot.has_links_under("d1").unwrap()); | ||
| 775 | assert!(depot.has_links_under("d3").unwrap()); | ||
| 776 | assert!(depot.has_links_under("d1/f1").unwrap()); | ||
| 777 | assert!(depot.has_links_under("d1/d2").unwrap()); | ||
| 778 | assert!(depot.has_links_under("d1/d2/f4").unwrap()); | ||
| 779 | |||
| 780 | assert!(!depot.has_links_under("d2").unwrap()); | ||
| 781 | assert!(!depot.has_links_under("d4").unwrap()); | ||
| 782 | assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); | ||
| 783 | } | ||
| 784 | |||
| 785 | #[test] | ||
| 786 | fn test_depot_links_verify_install() { | ||
| 787 | let mut depot = Depot::default(); | ||
| 788 | let f1 = depot.link_create("nvim", ".config/nvim").unwrap(); | ||
| 789 | let f2 = depot.link_create("alacritty", ".config/alacritty").unwrap(); | ||
| 790 | let f3 = depot.link_create("bash/.bashrc", ".bashrc").unwrap(); | ||
| 791 | let f4 = depot.link_create("bash_laptop/.bashrc", ".bashrc").unwrap(); | ||
| 792 | |||
| 793 | depot | ||
| 794 | .links_verify_install(vec![f1, f2, f3].into_iter()) | ||
| 795 | .unwrap(); | ||
| 796 | depot | ||
| 797 | .links_verify_install(vec![f1, f2, f3, f4].into_iter()) | ||
| 798 | .unwrap_err(); | ||
| 799 | } | ||
| 800 | |||
| 801 | #[test] | ||
| 802 | fn test_depot_read_dir() { | ||
| 803 | let mut depot = Depot::default(); | ||
| 804 | let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); | ||
| 805 | let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); | ||
| 806 | let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); | ||
| 807 | let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); | ||
| 808 | let _d3 = depot.link_create("d3", "d3").unwrap(); | ||
| 809 | |||
| 810 | let read_dir = depot.read_dir("d1").unwrap().collect::<Vec<_>>(); | ||
| 811 | let expected_read_dir = vec![ | ||
| 812 | DirNode::Link(f1), | ||
| 813 | DirNode::Link(f2), | ||
| 814 | DirNode::Link(f3), | ||
| 815 | DirNode::Directory(PathBuf::from("d1/d2")), | ||
| 816 | ]; | ||
| 817 | assert!( | ||
| 818 | read_dir.len() == expected_read_dir.len() | ||
| 819 | && expected_read_dir.iter().all(|x| read_dir.contains(x)) | ||
| 820 | ); | ||
| 821 | } | ||
| 822 | |||
| 823 | #[test] | ||
| 824 | fn test_path_verify() { | ||
| 825 | path_verify(Path::new("")).unwrap(); | ||
| 826 | path_verify(Path::new("f1")).unwrap(); | ||
| 827 | path_verify(Path::new("d1/f1")).unwrap(); | ||
| 828 | path_verify(Path::new("d1/f1.txt")).unwrap(); | ||
| 829 | path_verify(Path::new("d1/./f1.txt")).unwrap(); | ||
| 830 | |||
| 831 | path_verify(Path::new("/")).unwrap_err(); | ||
| 832 | path_verify(Path::new("./f1")).unwrap_err(); | ||
| 833 | path_verify(Path::new("/d1/f1")).unwrap_err(); | ||
| 834 | path_verify(Path::new("d1/../f1.txt")).unwrap_err(); | ||
| 835 | path_verify(Path::new("/d1/../f1.txt")).unwrap_err(); | ||
| 836 | } | ||
| 837 | |||
| 838 | #[test] | ||
| 839 | fn test_path_verify_link() { | ||
| 840 | path_verify_link(Path::new("f1")).unwrap(); | ||
| 841 | path_verify_link(Path::new("d1/f1")).unwrap(); | ||
| 842 | path_verify_link(Path::new("d1/f1.txt")).unwrap(); | ||
| 843 | path_verify_link(Path::new("d1/./f1.txt")).unwrap(); | ||
| 844 | |||
| 845 | path_verify_link(Path::new("")).unwrap_err(); | ||
| 846 | path_verify_link(Path::new("/")).unwrap_err(); | ||
| 847 | path_verify_link(Path::new("./f1")).unwrap_err(); | ||
| 848 | path_verify_link(Path::new("/d1/f1")).unwrap_err(); | ||
| 849 | path_verify_link(Path::new("d1/../f1.txt")).unwrap_err(); | ||
| 850 | path_verify_link(Path::new("/d1/../f1.txt")).unwrap_err(); | ||
| 851 | } | ||
| 852 | |||
| 853 | #[test] | ||
| 854 | fn test_path_iter_comps() { | ||
| 855 | let path = Path::new("comp1/comp2/./comp3/file.txt"); | ||
| 856 | let mut iter = path_iter_comps(path); | ||
| 857 | assert_eq!(iter.next(), Some(OsStr::new("comp1"))); | ||
| 858 | assert_eq!(iter.next(), Some(OsStr::new("comp2"))); | ||
| 859 | assert_eq!(iter.next(), Some(OsStr::new("comp3"))); | ||
| 860 | assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); | ||
| 861 | assert_eq!(iter.next(), None); | ||
| 862 | } | ||
| 863 | } | ||
| 864 | } | ||
| 865 | |||
| 866 | pub mod dotup { | ||
| 867 | use std::{ | ||
| 868 | cmp::Ordering, | ||
| 869 | collections::HashSet, | ||
| 870 | path::{Path, PathBuf}, | ||
| 871 | }; | ||
| 872 | |||
| 873 | use ansi_term::Color; | ||
| 874 | use anyhow::Context; | ||
| 875 | |||
| 876 | use crate::{ | ||
| 877 | depot::{self, Depot, DirNode, LinkID}, | ||
| 878 | utils, | ||
| 879 | }; | ||
| 880 | |||
| 881 | #[derive(Debug)] | ||
| 882 | struct CanonicalPair { | ||
| 883 | origin: PathBuf, | ||
| 884 | destination: PathBuf, | ||
| 885 | } | ||
| 886 | |||
| 887 | #[derive(Debug, Clone)] | ||
| 888 | enum StatusItem { | ||
| 889 | Link { | ||
| 890 | origin: PathBuf, | ||
| 891 | destination: PathBuf, | ||
| 892 | is_directory: bool, | ||
| 893 | }, | ||
| 894 | Directory { | ||
| 895 | origin: PathBuf, | ||
| 896 | items: Vec<StatusItem>, | ||
| 897 | }, | ||
| 898 | Unlinked { | ||
| 899 | origin: PathBuf, | ||
| 900 | is_directory: bool, | ||
| 901 | }, | ||
| 902 | } | ||
| 903 | |||
| 904 | impl StatusItem { | ||
| 905 | fn display_ord_cmp(&self, other: &Self) -> Ordering { | ||
| 906 | match (self, other) { | ||
| 907 | ( | ||
| 908 | StatusItem::Link { | ||
| 909 | origin: l_origin, .. | ||
| 910 | }, | ||
| 911 | StatusItem::Link { | ||
| 912 | origin: r_origin, .. | ||
| 913 | }, | ||
| 914 | ) => l_origin.cmp(r_origin), | ||
| 915 | (StatusItem::Link { .. }, StatusItem::Directory { .. }) => Ordering::Less, | ||
| 916 | ( | ||
| 917 | StatusItem::Link { | ||
| 918 | is_directory: l_is_dir, | ||
| 919 | .. | ||
| 920 | }, | ||
| 921 | StatusItem::Unlinked { | ||
| 922 | is_directory: u_is_dir, | ||
| 923 | .. | ||
| 924 | }, | ||
| 925 | ) => { | ||
| 926 | if *u_is_dir && !*l_is_dir { | ||
| 927 | Ordering::Less | ||
| 928 | } else { | ||
| 929 | Ordering::Greater | ||
| 930 | } | ||
| 931 | } | ||
| 932 | (StatusItem::Directory { .. }, StatusItem::Link { .. }) => Ordering::Greater, | ||
| 933 | ( | ||
| 934 | StatusItem::Directory { | ||
| 935 | origin: l_origin, .. | ||
| 936 | }, | ||
| 937 | StatusItem::Directory { | ||
| 938 | origin: r_origin, .. | ||
| 939 | }, | ||
| 940 | ) => l_origin.cmp(r_origin), | ||
| 941 | (StatusItem::Directory { .. }, StatusItem::Unlinked { .. }) => Ordering::Greater, | ||
| 942 | ( | ||
| 943 | StatusItem::Unlinked { | ||
| 944 | is_directory: u_is_dir, | ||
| 945 | .. | ||
| 946 | }, | ||
| 947 | StatusItem::Link { | ||
| 948 | is_directory: l_is_dir, | ||
| 949 | .. | ||
| 950 | }, | ||
| 951 | ) => { | ||
| 952 | if *u_is_dir && !*l_is_dir { | ||
| 953 | Ordering::Greater | ||
| 954 | } else { | ||
| 955 | Ordering::Less | ||
| 956 | } | ||
| 957 | } | ||
| 958 | (StatusItem::Unlinked { .. }, StatusItem::Directory { .. }) => Ordering::Less, | ||
| 959 | ( | ||
| 960 | StatusItem::Unlinked { | ||
| 961 | origin: l_origin, .. | ||
| 962 | }, | ||
| 963 | StatusItem::Unlinked { | ||
| 964 | origin: r_origin, .. | ||
| 965 | }, | ||
| 966 | ) => l_origin.cmp(r_origin), | ||
| 967 | } | ||
| 968 | } | ||
| 969 | } | ||
| 970 | |||
| 971 | #[derive(Debug)] | ||
| 972 | pub struct Dotup { | ||
| 973 | depot: Depot, | ||
| 974 | depot_dir: PathBuf, | ||
| 975 | depot_path: PathBuf, | ||
| 976 | install_base: PathBuf, | ||
| 977 | } | ||
| 978 | |||
| 979 | impl Dotup { | ||
| 980 | fn new(depot: Depot, depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result<Self> { | ||
| 981 | assert!(depot_path.is_absolute()); | ||
| 982 | assert!(depot_path.is_file()); | ||
| 983 | assert!(install_base.is_absolute()); | ||
| 984 | assert!(install_base.is_dir()); | ||
| 985 | let depot_dir = { | ||
| 986 | let mut d = depot_path.clone(); | ||
| 987 | d.pop(); | ||
| 988 | d | ||
| 989 | }; | ||
| 990 | Ok(Self { | ||
| 991 | depot, | ||
| 992 | depot_dir, | ||
| 993 | depot_path, | ||
| 994 | install_base, | ||
| 995 | }) | ||
| 996 | } | ||
| 997 | |||
| 998 | pub fn link(&mut self, origin: impl AsRef<Path>, destination: impl AsRef<Path>) { | ||
| 999 | let link_result: anyhow::Result<()> = try { | ||
| 1000 | let origin = self.prepare_relative_path(origin.as_ref())?; | ||
| 1001 | let destination = destination.as_ref(); | ||
| 1002 | self.depot.link_create(origin, destination)?; | ||
| 1003 | }; | ||
| 1004 | match link_result { | ||
| 1005 | Ok(_) => {} | ||
| 1006 | Err(e) => println!("Failed to create link : {e}"), | ||
| 1007 | } | ||
| 1008 | } | ||
| 1009 | |||
| 1010 | pub fn unlink(&mut self, paths: impl Iterator<Item = impl AsRef<Path>>, uninstall: bool) { | ||
| 1011 | for origin in paths { | ||
| 1012 | let unlink_result: anyhow::Result<()> = try { | ||
| 1013 | let origin = self.prepare_relative_path(origin.as_ref())?; | ||
| 1014 | let links_under: Vec<_> = self.depot.links_under(&origin)?.collect(); | ||
| 1015 | for link_id in links_under { | ||
| 1016 | if uninstall && self.symlink_is_installed_by_link_id(link_id)? { | ||
| 1017 | self.symlink_uninstall_by_link_id(link_id)?; | ||
| 1018 | } | ||
| 1019 | self.depot.link_remove(link_id); | ||
| 1020 | } | ||
| 1021 | }; | ||
| 1022 | match unlink_result { | ||
| 1023 | Ok(_) => {} | ||
| 1024 | Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()), | ||
| 1025 | } | ||
| 1026 | } | ||
| 1027 | } | ||
| 1028 | |||
| 1029 | pub fn install(&self, paths: impl Iterator<Item = impl AsRef<Path>>) { | ||
| 1030 | let install_result: anyhow::Result<()> = try { | ||
| 1031 | let link_ids = self.link_ids_from_paths_iter(paths)?; | ||
| 1032 | self.depot.links_verify_install(link_ids.iter().copied())?; | ||
| 1033 | |||
| 1034 | for link_id in link_ids { | ||
| 1035 | self.symlink_install_by_link_id(link_id)?; | ||
| 1036 | } | ||
| 1037 | }; | ||
| 1038 | if let Err(e) = install_result { | ||
| 1039 | println!("error while installing : {e}"); | ||
| 1040 | } | ||
| 1041 | } | ||
| 1042 | |||
| 1043 | pub fn uninstall(&self, paths: impl Iterator<Item = impl AsRef<Path>>) { | ||
| 1044 | let uninstall_result: anyhow::Result<()> = try { | ||
| 1045 | let link_ids = self.link_ids_from_paths_iter(paths)?; | ||
| 1046 | for link_id in link_ids { | ||
| 1047 | if self.symlink_is_installed_by_link_id(link_id)? { | ||
| 1048 | self.symlink_uninstall_by_link_id(link_id)?; | ||
| 1049 | } | ||
| 1050 | } | ||
| 1051 | }; | ||
| 1052 | if let Err(e) = uninstall_result { | ||
| 1053 | println!("error while uninstalling {e}",); | ||
| 1054 | } | ||
| 1055 | } | ||
| 1056 | |||
| 1057 | pub fn mv( | ||
| 1058 | &mut self, | ||
| 1059 | origins: impl Iterator<Item = impl AsRef<Path>>, | ||
| 1060 | destination: impl AsRef<Path>, | ||
| 1061 | ) { | ||
| 1062 | let origins = { | ||
| 1063 | let mut v = Vec::new(); | ||
| 1064 | for origin in origins { | ||
| 1065 | match self.prepare_relative_path(origin.as_ref()) { | ||
| 1066 | Ok(origin) => v.push(origin), | ||
| 1067 | Err(e) => { | ||
| 1068 | println!("invalid link {} : {e}", origin.as_ref().display()); | ||
| 1069 | return; | ||
| 1070 | } | ||
| 1071 | } | ||
| 1072 | } | ||
| 1073 | v | ||
| 1074 | }; | ||
| 1075 | let destination = destination.as_ref(); | ||
| 1076 | |||
| 1077 | // if we are moving multiple links then the destination must be a directory | ||
| 1078 | if origins.len() > 1 && destination.is_dir() { | ||
| 1079 | println!("destination must be a directory"); | ||
| 1080 | return; | ||
| 1081 | } | ||
| 1082 | |||
| 1083 | for origin in origins { | ||
| 1084 | if let Err(e) = self.mv_one(&origin, destination) { | ||
| 1085 | println!("error moving link {} : {e}", origin.display()); | ||
| 1086 | } | ||
| 1087 | } | ||
| 1088 | } | ||
| 1089 | |||
| 1090 | fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> { | ||
| 1091 | let link_id = match self.depot.link_find(origin)? { | ||
| 1092 | Some(link_id) => link_id, | ||
| 1093 | None => { | ||
| 1094 | return Err(anyhow::anyhow!(format!( | ||
| 1095 | "{} is not a link", | ||
| 1096 | origin.display() | ||
| 1097 | ))) | ||
| 1098 | } | ||
| 1099 | }; | ||
| 1100 | let is_installed = self.symlink_is_installed_by_link_id(link_id)?; | ||
| 1101 | let original_origin = self.depot.link_view(link_id).origin().to_owned(); | ||
| 1102 | self.depot.link_move(link_id, destination)?; | ||
| 1103 | // move the actual file on disk | ||
| 1104 | if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file") { | ||
| 1105 | // unwrap: moving the link back to its origin place has to work | ||
| 1106 | self.depot.link_move(link_id, original_origin).unwrap(); | ||
| 1107 | return Err(e); | ||
| 1108 | } | ||
| 1109 | // reinstall because we just moved the origin | ||
| 1110 | if is_installed { | ||
| 1111 | self.symlink_install_by_link_id(link_id) | ||
| 1112 | .context("failed to reinstall link while moving")?; | ||
| 1113 | } | ||
| 1114 | Ok(()) | ||
| 1115 | } | ||
| 1116 | |||
| 1117 | pub fn status(&self) { | ||
| 1118 | let status_result: anyhow::Result<()> = try { | ||
| 1119 | let canonical_dir = utils::current_working_directory(); | ||
| 1120 | let item = self.status_path_to_item(&canonical_dir)?; | ||
| 1121 | self.status_print_item(item, 0)?; | ||
| 1122 | }; | ||
| 1123 | if let Err(e) = status_result { | ||
| 1124 | println!("error while displaying status : {e}"); | ||
| 1125 | } | ||
| 1126 | } | ||
| 1127 | fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result<StatusItem> { | ||
| 1128 | debug_assert!(canonical_path.is_absolute()); | ||
| 1129 | debug_assert!(canonical_path.exists()); | ||
| 1130 | let relative_path = self.prepare_relative_path(canonical_path)?; | ||
| 1131 | |||
| 1132 | let item = if canonical_path.is_dir() { | ||
| 1133 | if let Some(link_id) = self.depot.link_find(&relative_path)? { | ||
| 1134 | let destination = self.depot.link_view(link_id).destination().to_owned(); | ||
| 1135 | StatusItem::Link { | ||
| 1136 | origin: relative_path, | ||
| 1137 | destination, | ||
| 1138 | is_directory: true, | ||
| 1139 | } | ||
| 1140 | } else if self.depot.has_links_under(&relative_path)? { | ||
| 1141 | let mut items = Vec::new(); | ||
| 1142 | let mut collected_rel_paths = HashSet::<PathBuf>::new(); | ||
| 1143 | let directory_paths = utils::collect_paths_in_dir(&canonical_path)?; | ||
| 1144 | for canonical_item_path in directory_paths { | ||
| 1145 | let item = self.status_path_to_item(&canonical_item_path)?; | ||
| 1146 | match &item { | ||
| 1147 | StatusItem::Link { origin, .. } | ||
| 1148 | | StatusItem::Directory { origin, .. } => { | ||
| 1149 | collected_rel_paths.insert(origin.to_owned()); | ||
| 1150 | } | ||
| 1151 | _ => {} | ||
| 1152 | } | ||
| 1153 | items.push(item); | ||
| 1154 | } | ||
| 1155 | |||
| 1156 | for dir_node in self.depot.read_dir(&relative_path)? { | ||
| 1157 | match dir_node { | ||
| 1158 | DirNode::Link(link_id) => { | ||
| 1159 | let link_view = self.depot.link_view(link_id); | ||
| 1160 | let link_rel_path = link_view.origin(); | ||
| 1161 | let link_rel_dest = link_view.destination(); | ||
| 1162 | if !collected_rel_paths.contains(link_rel_path) { | ||
| 1163 | items.push(StatusItem::Link { | ||
| 1164 | origin: link_rel_path.to_owned(), | ||
| 1165 | destination: link_rel_dest.to_owned(), | ||
| 1166 | is_directory: false, | ||
| 1167 | }); | ||
| 1168 | } | ||
| 1169 | } | ||
| 1170 | DirNode::Directory(_) => {} | ||
| 1171 | } | ||
| 1172 | } | ||
| 1173 | |||
| 1174 | StatusItem::Directory { | ||
| 1175 | origin: relative_path, | ||
| 1176 | items, | ||
| 1177 | } | ||
| 1178 | } else { | ||
| 1179 | StatusItem::Unlinked { | ||
| 1180 | origin: relative_path, | ||
| 1181 | is_directory: true, | ||
| 1182 | } | ||
| 1183 | } | ||
| 1184 | } else if let Some(link_id) = self.depot.link_find(&relative_path)? { | ||
| 1185 | let destination = self.depot.link_view(link_id).destination().to_owned(); | ||
| 1186 | StatusItem::Link { | ||
| 1187 | origin: relative_path, | ||
| 1188 | destination, | ||
| 1189 | is_directory: false, | ||
| 1190 | } | ||
| 1191 | } else { | ||
| 1192 | StatusItem::Unlinked { | ||
| 1193 | origin: relative_path, | ||
| 1194 | is_directory: false, | ||
| 1195 | } | ||
| 1196 | }; | ||
| 1197 | Ok(item) | ||
| 1198 | } | ||
| 1199 | fn status_print_item(&self, item: StatusItem, depth: u32) -> anyhow::Result<()> { | ||
| 1200 | fn print_depth(d: u32) { | ||
| 1201 | for _ in 0..d.saturating_sub(1) { | ||
| 1202 | print!(" "); | ||
| 1203 | } | ||
| 1204 | } | ||
| 1205 | fn origin_color(exists: bool, is_installed: bool) -> Color { | ||
| 1206 | if !exists { | ||
| 1207 | Color::Red | ||
| 1208 | } else if is_installed { | ||
| 1209 | Color::Green | ||
| 1210 | } else { | ||
| 1211 | Color::RGB(255, 127, 0) | ||
| 1212 | } | ||
| 1213 | } | ||
| 1214 | |||
| 1215 | let destination_color = Color::Blue; | ||
| 1216 | |||
| 1217 | print_depth(depth); | ||
| 1218 | match item { | ||
| 1219 | StatusItem::Link { | ||
| 1220 | origin, | ||
| 1221 | destination, | ||
| 1222 | is_directory, | ||
| 1223 | } => { | ||
| 1224 | let canonical_origin = self.depot_dir.join(&origin); | ||
| 1225 | let canonical_destination = self.install_base.join(&destination); | ||
| 1226 | let file_name = Self::status_get_filename(&canonical_origin); | ||
| 1227 | let is_installed = | ||
| 1228 | self.symlink_is_installed(&canonical_origin, &canonical_destination)?; | ||
| 1229 | let exists = canonical_origin.exists(); | ||
| 1230 | let origin_color = origin_color(exists, is_installed); | ||
| 1231 | let directory_extra = if is_directory { "/" } else { "" }; | ||
| 1232 | println!( | ||
| 1233 | "{}{} -> {}", | ||
| 1234 | origin_color.paint(file_name), | ||
| 1235 | directory_extra, | ||
| 1236 | destination_color.paint(destination.display().to_string()) | ||
| 1237 | ); | ||
| 1238 | } | ||
| 1239 | StatusItem::Directory { origin, mut items } => { | ||
| 1240 | items.sort_by(|a, b| StatusItem::display_ord_cmp(a, b).reverse()); | ||
| 1241 | let directory_name = Self::status_get_filename(&origin); | ||
| 1242 | if depth != 0 { | ||
| 1243 | println!("{}/", directory_name); | ||
| 1244 | } | ||
| 1245 | for item in items { | ||
| 1246 | self.status_print_item(item, depth + 1)?; | ||
| 1247 | } | ||
| 1248 | } | ||
| 1249 | StatusItem::Unlinked { | ||
| 1250 | origin, | ||
| 1251 | is_directory, | ||
| 1252 | } => { | ||
| 1253 | let file_name = Self::status_get_filename(&origin); | ||
| 1254 | let directory_extra = if is_directory { "/" } else { "" }; | ||
| 1255 | println!("{}{}", file_name, directory_extra); | ||
| 1256 | } | ||
| 1257 | } | ||
| 1258 | Ok(()) | ||
| 1259 | } | ||
| 1260 | fn status_get_filename(path: &Path) -> &str { | ||
| 1261 | path.file_name() | ||
| 1262 | .and_then(|s| s.to_str()) | ||
| 1263 | .unwrap_or_default() | ||
| 1264 | } | ||
| 1265 | |||
| 1266 | fn prepare_relative_path(&self, origin: &Path) -> anyhow::Result<PathBuf> { | ||
| 1267 | let canonical = utils::weakly_canonical(origin); | ||
| 1268 | let relative = canonical | ||
| 1269 | .strip_prefix(&self.depot_dir) | ||
| 1270 | .context("Invalid origin path, not under depot directory")?; | ||
| 1271 | Ok(relative.to_owned()) | ||
| 1272 | } | ||
| 1273 | |||
| 1274 | fn link_ids_from_paths_iter( | ||
| 1275 | &self, | ||
| 1276 | paths: impl Iterator<Item = impl AsRef<Path>>, | ||
| 1277 | ) -> anyhow::Result<Vec<LinkID>> { | ||
| 1278 | let mut link_ids = HashSet::<LinkID>::default(); | ||
| 1279 | for path in paths { | ||
| 1280 | let path = self.prepare_relative_path(path.as_ref())?; | ||
| 1281 | link_ids.extend(self.depot.links_under(&path)?); | ||
| 1282 | } | ||
| 1283 | Ok(Vec::from_iter(link_ids.into_iter())) | ||
| 1284 | } | ||
| 1285 | |||
| 1286 | fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result<bool> { | ||
| 1287 | let canonical_pair = self.canonical_pair_from_link_id(link_id); | ||
| 1288 | self.symlink_is_installed(&canonical_pair.origin, &canonical_pair.destination) | ||
| 1289 | } | ||
| 1290 | |||
| 1291 | fn symlink_is_installed(&self, origin: &Path, destination: &Path) -> anyhow::Result<bool> { | ||
| 1292 | debug_assert!(origin.is_absolute()); | ||
| 1293 | debug_assert!(destination.is_absolute()); | ||
| 1294 | |||
| 1295 | if destination.is_symlink() { | ||
| 1296 | let symlink_destination = destination.read_link()?; | ||
| 1297 | match symlink_destination.canonicalize() { | ||
| 1298 | Ok(canonicalized) => Ok(origin == canonicalized), | ||
| 1299 | Err(_) => Ok(false), | ||
| 1300 | } | ||
| 1301 | } else { | ||
| 1302 | Ok(false) | ||
| 1303 | } | ||
| 1304 | } | ||
| 1305 | |||
| 1306 | fn symlink_install_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { | ||
| 1307 | let canonical_pair = self.canonical_pair_from_link_id(link_id); | ||
| 1308 | self.symlink_install(&canonical_pair.origin, &canonical_pair.destination) | ||
| 1309 | } | ||
| 1310 | |||
| 1311 | fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { | ||
| 1312 | debug_assert!(origin.is_absolute()); | ||
| 1313 | debug_assert!(destination.is_absolute()); | ||
| 1314 | log::debug!( | ||
| 1315 | "symlink_install : {} -> {}", | ||
| 1316 | origin.display(), | ||
| 1317 | destination.display() | ||
| 1318 | ); | ||
| 1319 | |||
| 1320 | let destination_parent = destination | ||
| 1321 | .parent() | ||
| 1322 | .ok_or_else(|| anyhow::anyhow!("destination has no parent component"))?; | ||
| 1323 | std::fs::create_dir_all(destination_parent).context("Failed to create directories")?; | ||
| 1324 | // need to do this beacause if the destination path ends in '/' because the symlink | ||
| 1325 | // functions will treat it as a directory but we want a file with that name. | ||
| 1326 | let destination = destination.with_file_name(destination.file_name().unwrap()); | ||
| 1327 | |||
| 1328 | let destination_exists = destination.exists(); | ||
| 1329 | let destination_is_symlink = destination.is_symlink(); | ||
| 1330 | |||
| 1331 | if destination_exists && !destination_is_symlink { | ||
| 1332 | return Err(anyhow::anyhow!("destination already exists")); | ||
| 1333 | } | ||
| 1334 | |||
| 1335 | if destination_is_symlink { | ||
| 1336 | log::debug!("symlink already exists, removing before recreating"); | ||
| 1337 | std::fs::remove_file(&destination)?; | ||
| 1338 | } | ||
| 1339 | |||
| 1340 | log::debug!( | ||
| 1341 | "creating filesystem symlink {} -> {}", | ||
| 1342 | origin.display(), | ||
| 1343 | destination.display() | ||
| 1344 | ); | ||
| 1345 | std::os::unix::fs::symlink(origin, destination).context("failed to create symlink")?; | ||
| 1346 | |||
| 1347 | Ok(()) | ||
| 1348 | } | ||
| 1349 | |||
| 1350 | fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { | ||
| 1351 | debug_assert!(origin.is_absolute()); | ||
| 1352 | debug_assert!(destination.is_absolute()); | ||
| 1353 | let destination = destination.with_file_name(destination.file_name().unwrap()); | ||
| 1354 | |||
| 1355 | if destination.is_symlink() { | ||
| 1356 | let symlink_destination = destination.read_link()?.canonicalize()?; | ||
| 1357 | if symlink_destination == origin { | ||
| 1358 | std::fs::remove_file(&destination)?; | ||
| 1359 | } | ||
| 1360 | } | ||
| 1361 | |||
| 1362 | Ok(()) | ||
| 1363 | } | ||
| 1364 | |||
| 1365 | fn symlink_uninstall_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { | ||
| 1366 | let canonical_pair = self.canonical_pair_from_link_id(link_id); | ||
| 1367 | self.symlink_uninstall(&canonical_pair.origin, &canonical_pair.destination) | ||
| 1368 | } | ||
| 1369 | |||
| 1370 | fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair { | ||
| 1371 | let link_view = self.depot.link_view(link_id); | ||
| 1372 | let relative_origin = link_view.origin(); | ||
| 1373 | let relative_destination = link_view.destination(); | ||
| 1374 | let canonical_origin = self.depot_dir.join(relative_origin); | ||
| 1375 | let canonical_destination = self.install_base.join(relative_destination); | ||
| 1376 | CanonicalPair { | ||
| 1377 | origin: canonical_origin, | ||
| 1378 | destination: canonical_destination, | ||
| 1379 | } | ||
| 1380 | } | ||
| 1381 | } | ||
| 1382 | |||
| 1383 | pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result<Dotup> { | ||
| 1384 | let depot_path = depot_path | ||
| 1385 | .canonicalize() | ||
| 1386 | .context("Failed to canonicalize depot path")?; | ||
| 1387 | let install_base = install_base | ||
| 1388 | .canonicalize() | ||
| 1389 | .context("Failed to canonicalize install base")?; | ||
| 1390 | if !install_base.is_dir() { | ||
| 1391 | return Err(anyhow::anyhow!("Install base must be a directory")); | ||
| 1392 | } | ||
| 1393 | let depot = depot::read(&depot_path)?; | ||
| 1394 | Dotup::new(depot, depot_path, install_base) | ||
| 1395 | } | ||
| 1396 | |||
| 1397 | pub fn write(dotup: &Dotup) -> anyhow::Result<()> { | ||
| 1398 | depot::write(&dotup.depot_path, &dotup.depot)?; | ||
| 1399 | Ok(()) | ||
| 1400 | } | ||
| 1401 | } | ||
| 1402 | |||
| 1403 | mod utils { | ||
| 1404 | use std::{ | ||
| 1405 | collections::VecDeque, | ||
| 1406 | path::{Component, Path, PathBuf}, | ||
| 1407 | }; | ||
| 1408 | |||
| 1409 | use crate::{ | ||
| 1410 | dotup::{self, Dotup}, | ||
| 1411 | Flags, | ||
| 1412 | }; | ||
| 1413 | |||
| 1414 | pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; | ||
| 1415 | |||
| 1416 | /// Returns a list of canonical paths to all the files in `dir`. This includes files in | ||
| 1417 | /// subdirectories. | ||
| 1418 | /// Fails if dir isnt a directory or if there is some other io error. | ||
| 1419 | pub fn collect_files_in_dir_recursive(dir: impl Into<PathBuf>) -> anyhow::Result<Vec<PathBuf>> { | ||
| 1420 | let mut paths = Vec::new(); | ||
| 1421 | let mut dirs = VecDeque::new(); | ||
| 1422 | dirs.push_back(dir.into()); | ||
| 1423 | |||
| 1424 | while let Some(dir) = dirs.pop_front() { | ||
| 1425 | for entry in std::fs::read_dir(dir)? { | ||
| 1426 | let entry = entry?; | ||
| 1427 | let filetype = entry.file_type()?; | ||
| 1428 | if filetype.is_dir() { | ||
| 1429 | dirs.push_back(entry.path()); | ||
| 1430 | } else { | ||
| 1431 | paths.push(entry.path()); | ||
| 1432 | } | ||
| 1433 | } | ||
| 1434 | } | ||
| 1435 | |||
| 1436 | Ok(paths) | ||
| 1437 | } | ||
| 1438 | |||
| 1439 | pub fn collect_paths_in_dir(dir: impl AsRef<Path>) -> anyhow::Result<Vec<PathBuf>> { | ||
| 1440 | Ok(std::fs::read_dir(dir)? | ||
| 1441 | .filter_map(|e| e.ok()) | ||
| 1442 | .map(|e| e.path()) | ||
| 1443 | .collect()) | ||
| 1444 | } | ||
| 1445 | |||
| 1446 | pub fn read_dotup(flags: &Flags) -> anyhow::Result<Dotup> { | ||
| 1447 | let depot_path = depot_path_from_flags(flags)?; | ||
| 1448 | let install_base = install_base_from_flags(flags); | ||
| 1449 | dotup::read(depot_path, install_base) | ||
| 1450 | } | ||
| 1451 | |||
| 1452 | pub fn write_dotup(dotup: &Dotup) -> anyhow::Result<()> { | ||
| 1453 | dotup::write(dotup) | ||
| 1454 | } | ||
| 1455 | |||
| 1456 | pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result<PathBuf> { | ||
| 1457 | match flags.depot { | ||
| 1458 | Some(ref path) => Ok(path.clone()), | ||
| 1459 | None => find_depot_path().ok_or_else(|| anyhow::anyhow!("Failed to find depot path")), | ||
| 1460 | } | ||
| 1461 | } | ||
| 1462 | |||
| 1463 | pub fn default_depot_path() -> PathBuf { | ||
| 1464 | current_working_directory().join(DEFAULT_DEPOT_FILE_NAME) | ||
| 1465 | } | ||
| 1466 | |||
| 1467 | pub fn find_depot_path() -> Option<PathBuf> { | ||
| 1468 | let mut cwd = current_working_directory(); | ||
| 1469 | loop { | ||
| 1470 | let path = cwd.join(DEFAULT_DEPOT_FILE_NAME); | ||
| 1471 | if path.exists() { | ||
| 1472 | break Some(path); | ||
| 1473 | } | ||
| 1474 | if !cwd.pop() { | ||
| 1475 | break None; | ||
| 1476 | } | ||
| 1477 | } | ||
| 1478 | } | ||
| 1479 | |||
| 1480 | pub fn install_base_from_flags(flags: &Flags) -> PathBuf { | ||
| 1481 | match flags.install_base { | ||
| 1482 | Some(ref path) => path.clone(), | ||
| 1483 | None => default_install_base(), | ||
| 1484 | } | ||
| 1485 | } | ||
| 1486 | |||
| 1487 | pub fn default_install_base() -> PathBuf { | ||
| 1488 | PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable")) | ||
| 1489 | } | ||
| 1490 | pub fn weakly_canonical(path: impl AsRef<Path>) -> PathBuf { | ||
| 1491 | let cwd = current_working_directory(); | ||
| 1492 | weakly_canonical_cwd(path, cwd) | ||
| 1493 | } | ||
| 1494 | |||
| 1495 | fn weakly_canonical_cwd(path: impl AsRef<Path>, cwd: PathBuf) -> PathBuf { | ||
| 1496 | // Adapated from | ||
| 1497 | // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 | ||
| 1498 | let path = path.as_ref(); | ||
| 1499 | |||
| 1500 | let mut components = path.components().peekable(); | ||
| 1501 | let mut canonical = cwd; | ||
| 1502 | let prefix = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { | ||
| 1503 | components.next(); | ||
| 1504 | PathBuf::from(c.as_os_str()) | ||
| 1505 | } else { | ||
| 1506 | PathBuf::new() | ||
| 1507 | }; | ||
| 1508 | |||
| 1509 | for component in components { | ||
| 1510 | match component { | ||
| 1511 | Component::Prefix(_) => unreachable!(), | ||
| 1512 | Component::RootDir => { | ||
| 1513 | canonical = prefix.clone(); | ||
| 1514 | canonical.push(component.as_os_str()) | ||
| 1515 | } | ||
| 1516 | Component::CurDir => {} | ||
| 1517 | Component::ParentDir => { | ||
| 1518 | canonical.pop(); | ||
| 1519 | } | ||
| 1520 | Component::Normal(p) => canonical.push(p), | ||
| 1521 | }; | ||
| 1522 | } | ||
| 1523 | |||
| 1524 | canonical | ||
| 1525 | } | ||
| 1526 | |||
| 1527 | pub fn current_working_directory() -> PathBuf { | ||
| 1528 | std::env::current_dir().expect("Failed to obtain current working directory") | ||
| 1529 | } | ||
| 1530 | |||
| 1531 | #[cfg(test)] | ||
| 1532 | mod tests { | ||
| 1533 | use super::*; | ||
| 1534 | |||
| 1535 | #[test] | ||
| 1536 | fn weak_canonical_test() { | ||
| 1537 | let cwd = PathBuf::from("/home/user"); | ||
| 1538 | assert_eq!( | ||
| 1539 | PathBuf::from("/home/dest"), | ||
| 1540 | weakly_canonical_cwd("../dest", cwd.clone()) | ||
| 1541 | ); | ||
| 1542 | assert_eq!( | ||
| 1543 | PathBuf::from("/home/dest/configs/init.vim"), | ||
| 1544 | weakly_canonical_cwd("../dest/configs/init.vim", cwd.clone()) | ||
| 1545 | ); | ||
| 1546 | assert_eq!( | ||
| 1547 | PathBuf::from("/dest/configs/init.vim"), | ||
| 1548 | weakly_canonical_cwd("/dest/configs/init.vim", cwd.clone()) | ||
| 1549 | ); | ||
| 1550 | assert_eq!( | ||
| 1551 | PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), | ||
| 1552 | weakly_canonical_cwd("./configs/nvim/lua/setup.lua", cwd.clone()) | ||
| 1553 | ); | ||
| 1554 | assert_eq!( | ||
| 1555 | PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), | ||
| 1556 | weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd) | ||
| 1557 | ); | ||
| 1558 | } | ||
| 1559 | } | ||
| 1560 | } | ||
| 1561 | 6 | ||
| 1562 | use std::path::PathBuf; | 7 | use std::path::PathBuf; |
| 1563 | 8 | ||
