aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs1748
1 files changed, 1748 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..7b38357
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,1748 @@
1#![feature(try_blocks)]
2
3// TODO: rewrite all errors so they start with lower case
4
5mod depot {
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
866pub 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
1403mod 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
1562use std::path::PathBuf;
1563
1564use clap::Parser;
1565use flexi_logger::Logger;
1566use utils::DEFAULT_DEPOT_FILE_NAME;
1567
1568#[derive(Parser, Debug)]
1569pub struct Flags {
1570 #[clap(long)]
1571 depot: Option<PathBuf>,
1572 #[clap(long)]
1573 install_base: Option<PathBuf>,
1574}
1575
1576#[derive(Parser, Debug)]
1577#[clap(author, version, about, long_about = None)]
1578struct Args {
1579 /// A level of verbosity, and can be used multiple times
1580 ///
1581 /// Level 1 - Info
1582 ///
1583 /// Level 2 - Debug
1584 ///
1585 /// Level 3 - Trace
1586 #[clap(short, long, parse(from_occurrences))]
1587 verbose: i32,
1588
1589 #[clap(flatten)]
1590 flags: Flags,
1591
1592 #[clap(subcommand)]
1593 command: SubCommand,
1594}
1595
1596#[derive(Parser, Debug)]
1597enum SubCommand {
1598 Init(InitArgs),
1599 Link(LinkArgs),
1600 Unlink(UnlinkArgs),
1601 Install(InstallArgs),
1602 Uninstall(UninstallArgs),
1603 Mv(MvArgs),
1604 Status(StatusArgs),
1605}
1606
1607fn main() -> anyhow::Result<()> {
1608 let args = Args::parse();
1609
1610 let log_level = match args.verbose {
1611 0 => "warn",
1612 1 => "info",
1613 2 => "debug",
1614 _ => "trace",
1615 };
1616
1617 Logger::try_with_env_or_str(log_level)?
1618 .format(flexi_logger::colored_default_format)
1619 .set_palette("196;208;32;198;15".to_string())
1620 .start()?;
1621
1622 match args.command {
1623 SubCommand::Init(cmd_args) => command_init(args.flags, cmd_args),
1624 SubCommand::Link(cmd_args) => command_link(args.flags, cmd_args),
1625 SubCommand::Unlink(cmd_args) => command_unlink(args.flags, cmd_args),
1626 SubCommand::Install(cmd_args) => command_install(args.flags, cmd_args),
1627 SubCommand::Uninstall(cmd_args) => command_uninstall(args.flags, cmd_args),
1628 SubCommand::Mv(cmd_args) => command_mv(args.flags, cmd_args),
1629 SubCommand::Status(cmd_args) => command_status(args.flags, cmd_args),
1630 }
1631}
1632
1633#[derive(Parser, Debug)]
1634struct InitArgs {
1635 path: Option<PathBuf>,
1636}
1637
1638fn command_init(_global_flags: Flags, args: InitArgs) -> anyhow::Result<()> {
1639 let depot_path = {
1640 let mut path = args.path.unwrap_or_else(utils::default_depot_path);
1641 if path.is_dir() {
1642 path = path.join(DEFAULT_DEPOT_FILE_NAME);
1643 }
1644 path
1645 };
1646
1647 if depot_path.exists() {
1648 println!("Depot at {} already exists", depot_path.display());
1649 } else {
1650 depot::write(&depot_path, &Default::default())?;
1651 println!("Depot initialized at {}", depot_path.display());
1652 }
1653
1654 Ok(())
1655}
1656
1657#[derive(Parser, Debug)]
1658struct LinkArgs {
1659 #[clap(long)]
1660 directory: bool,
1661
1662 origin: PathBuf,
1663
1664 destination: PathBuf,
1665}
1666
1667fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> {
1668 let mut dotup = utils::read_dotup(&global_flags)?;
1669 let origins = if args.directory {
1670 vec![args.origin]
1671 } else if args.origin.is_dir() {
1672 utils::collect_files_in_dir_recursive(args.origin)?
1673 } else {
1674 vec![args.origin]
1675 };
1676 for origin in origins {
1677 dotup.link(origin, &args.destination);
1678 }
1679 utils::write_dotup(&dotup)?;
1680 Ok(())
1681}
1682
1683#[derive(Parser, Debug)]
1684struct UnlinkArgs {
1685 #[clap(long)]
1686 uninstall: bool,
1687
1688 paths: Vec<PathBuf>,
1689}
1690
1691fn command_unlink(global_flags: Flags, args: UnlinkArgs) -> anyhow::Result<()> {
1692 let mut dotup = utils::read_dotup(&global_flags)?;
1693 dotup.unlink(args.paths.into_iter(), args.uninstall);
1694 utils::write_dotup(&dotup)?;
1695 Ok(())
1696}
1697
1698#[derive(Parser, Debug)]
1699struct InstallArgs {
1700 #[clap(long)]
1701 directory: bool,
1702
1703 paths: Vec<PathBuf>,
1704}
1705
1706fn command_install(global_flags: Flags, args: InstallArgs) -> anyhow::Result<()> {
1707 let dotup = utils::read_dotup(&global_flags)?;
1708 dotup.install(args.paths.into_iter());
1709 Ok(())
1710}
1711
1712#[derive(Parser, Debug)]
1713struct UninstallArgs {
1714 paths: Vec<PathBuf>,
1715}
1716
1717fn command_uninstall(global_flags: Flags, args: UninstallArgs) -> anyhow::Result<()> {
1718 let dotup = utils::read_dotup(&global_flags)?;
1719 dotup.uninstall(args.paths.into_iter());
1720 Ok(())
1721}
1722
1723#[derive(Parser, Debug)]
1724struct MvArgs {
1725 paths: Vec<PathBuf>,
1726}
1727
1728fn command_mv(global_flags: Flags, args: MvArgs) -> anyhow::Result<()> {
1729 let mut dotup = utils::read_dotup(&global_flags)?;
1730 let mut paths = args.paths;
1731 if paths.len() < 2 {
1732 return Err(anyhow::anyhow!("mv requires atleast 2 arguments"));
1733 }
1734 let to = paths.pop().unwrap();
1735 let from = paths;
1736 dotup.mv(from.iter(), &to);
1737 utils::write_dotup(&dotup)?;
1738 Ok(())
1739}
1740
1741#[derive(Parser, Debug)]
1742struct StatusArgs {}
1743
1744fn command_status(global_flags: Flags, _args: StatusArgs) -> anyhow::Result<()> {
1745 let dotup = utils::read_dotup(&global_flags)?;
1746 dotup.status();
1747 Ok(())
1748}