aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2022-02-08 09:49:16 +0000
committerdiogo464 <[email protected]>2022-02-08 09:49:16 +0000
commit472cf96ba0b21f3530b62ffc887017a2dabf014b (patch)
tree5968fb62e5ff096465929f1a5af135ecf0dfb4ae
parent52552b1b12ba8632a26a56a926b6767370901b56 (diff)
split code into different files
-rw-r--r--src/depot.rs850
-rw-r--r--src/dotup.rs533
-rw-r--r--src/main.rs1561
-rw-r--r--src/utils.rs156
4 files changed, 1542 insertions, 1558 deletions
diff --git a/src/depot.rs b/src/depot.rs
new file mode 100644
index 0000000..b2d4e3c
--- /dev/null
+++ b/src/depot.rs
@@ -0,0 +1,850 @@
1use anyhow::Context;
2use std::{
3 collections::HashSet,
4 ffi::{OsStr, OsString},
5 ops::Index,
6 path::{Path, PathBuf},
7};
8use thiserror::Error;
9
10use slotmap::{Key, SlotMap};
11
12//pub type Result<T, E = DepotError> = std::result::Result<T, E>;
13pub use anyhow::Result;
14pub use disk::{read, write};
15
16slotmap::new_key_type! {pub struct LinkID;}
17slotmap::new_key_type! {struct NodeID;}
18
19#[derive(Debug, Error)]
20enum DepotError {
21 #[error("path must be relative")]
22 InvalidPath,
23 #[error("path must be relative and not empty")]
24 InvalidLinkPath,
25}
26
27#[derive(Debug, Clone)]
28struct Node {
29 comp: OsString,
30 parent: NodeID,
31 kind: NodeKind,
32}
33
34#[derive(Debug, Clone)]
35enum NodeKind {
36 Link(LinkID),
37 Directory(HashSet<NodeID>),
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41enum NodeSearchResult {
42 Found(NodeID),
43 /// the closest NodeID up the the search point.
44 NotFound(NodeID),
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum DirNode {
49 Link(LinkID),
50 Directory(PathBuf),
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum SearchResult {
55 Found(LinkID),
56 Ancestor(LinkID),
57 NotFound,
58}
59
60#[derive(Debug, Clone)]
61struct Link {
62 origin: PathBuf,
63 destination: PathBuf,
64 origin_id: NodeID,
65}
66
67#[derive(Debug)]
68pub struct LinkView<'a> {
69 link_id: LinkID,
70 depot: &'a Depot,
71}
72
73impl<'a> LinkView<'a> {
74 pub fn origin(&self) -> &Path {
75 &self.depot.links[self.link_id].origin
76 }
77
78 pub fn destination(&self) -> &Path {
79 &self.depot.links[self.link_id].destination
80 }
81}
82
83#[derive(Debug, Clone)]
84struct DepotTree {
85 root: NodeID,
86 nodes: SlotMap<NodeID, Node>,
87}
88
89impl Default for DepotTree {
90 fn default() -> Self {
91 let mut nodes = SlotMap::<NodeID, Node>::default();
92 let root = nodes.insert(Node {
93 comp: Default::default(),
94 parent: Default::default(),
95 kind: NodeKind::Directory(Default::default()),
96 });
97 Self { root, nodes }
98 }
99}
100
101impl Index<NodeID> for DepotTree {
102 type Output = Node;
103
104 fn index(&self, index: NodeID) -> &Self::Output {
105 self.nodes.index(index)
106 }
107}
108
109impl DepotTree {
110 /// create a node of kind [`NodeKind::Link`].
111 pub fn link_create(&mut self, path: &Path, link_id: LinkID) -> Result<NodeID> {
112 debug_assert!(path_verify_link(path).is_ok());
113
114 let path_search_result = self.search(path);
115
116 // handle the error cases
117 match path_search_result {
118 NodeSearchResult::Found(node_id) => {
119 let node = &self.nodes[node_id];
120 match &node.kind {
121 NodeKind::Link(_) => Err(anyhow::anyhow!("link already exists")),
122 NodeKind::Directory(_) => {
123 Err(anyhow::anyhow!("path already has links under it"))
124 }
125 }
126 }
127 NodeSearchResult::NotFound(ancestor_node_id) => {
128 let ancestor_node = &self.nodes[ancestor_node_id];
129 match &ancestor_node.kind {
130 NodeKind::Link(_) => Err(anyhow::anyhow!(
131 "an ancestor of this path is already linked"
132 )),
133 NodeKind::Directory(_) => Ok(()),
134 }
135 }
136 }?;
137
138 // create the node
139 // unwrap: this is a verfied link path, it must have atleast one component
140 let filename = path.file_name().unwrap();
141 let parent_path = path_parent_or_empty(path);
142 let node_id = self.nodes.insert(Node {
143 comp: filename.to_owned(),
144 parent: Default::default(),
145 kind: NodeKind::Link(link_id),
146 });
147 let parent_id = self.directory_get_or_create(parent_path, node_id);
148 self.nodes[node_id].parent = parent_id;
149 Ok(node_id)
150 }
151
152 pub fn link_update_id(&mut self, node_id: NodeID, link_id: LinkID) {
153 let node = &mut self.nodes[node_id];
154 match &mut node.kind {
155 NodeKind::Link(lid) => *lid = link_id,
156 NodeKind::Directory(_) => unreachable!(),
157 }
158 }
159
160 /// attempts to moves a node of kind [`NodeKind::Link`] to `destination`.
161 pub fn link_move(&mut self, node_id: NodeID, destination: &Path) -> Result<()> {
162 let parent_id = self.nodes[node_id].parent;
163 let parent = &mut self.nodes[parent_id];
164
165 // remove the node from its parent temporarily so that the search never returns this
166 // link and that way any link will find means an error.
167 // if an error does happen then we re-add this node to its parent to keep the data
168 // consistent.
169 match &mut parent.kind {
170 NodeKind::Link(_) => unreachable!(),
171 NodeKind::Directory(children) => children.remove(&node_id),
172 };
173
174 let search_result = self.search(destination);
175 // handle the error cases
176 match search_result {
177 NodeSearchResult::Found(found_id) => {
178 assert!(found_id != node_id);
179 self.directory_add_child(parent_id, node_id);
180 return Err(anyhow::anyhow!("link already exists at that path"));
181 }
182 NodeSearchResult::NotFound(ancestor_id) => {
183 let ancestor = &self.nodes[ancestor_id];
184 match &ancestor.kind {
185 NodeKind::Link(_) => {
186 self.directory_add_child(parent_id, node_id);
187 return Err(anyhow::anyhow!("ancestor path is already linked"));
188 }
189 NodeKind::Directory(_) => {}
190 }
191 }
192 };
193
194 let destination_parent = path_parent_or_empty(destination);
195 let new_parent_id = self.directory_get_or_create(destination_parent, node_id);
196 if new_parent_id != parent_id {
197 self.nodes[node_id].parent = new_parent_id;
198
199 // we have to re-add and call the remove function because it could lead to the removal
200 // of several directories if they become empty after this remove.
201 self.directory_add_child(parent_id, node_id);
202 self.directory_remove_child(parent_id, node_id);
203 }
204
205 // unwrap: destination is a verified link path so it has atleast 1 component
206 let comp = destination.file_name().unwrap();
207 let node = &mut self.nodes[node_id];
208 if node.comp != comp {
209 node.comp = comp.to_owned();
210 }
211
212 Ok(())
213 }
214
215 pub fn link_search(&self, path: &Path) -> SearchResult {
216 match self.search(path) {
217 NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind {
218 NodeKind::Link(link_id) => SearchResult::Found(*link_id),
219 NodeKind::Directory(_) => SearchResult::NotFound,
220 },
221 NodeSearchResult::NotFound(node_id) => match &self.nodes[node_id].kind {
222 NodeKind::Link(link_id) => SearchResult::Ancestor(*link_id),
223 NodeKind::Directory(_) => SearchResult::NotFound,
224 },
225 }
226 }
227
228 /// remove a node of kind [`NodeKind::Link`].
229 pub fn link_remove(&mut self, node_id: NodeID) {
230 let node = &self.nodes[node_id];
231 assert!(std::matches!(node.kind, NodeKind::Link(_)));
232 let parent_id = node.parent;
233 self.nodes.remove(node_id);
234 self.directory_remove_child(parent_id, node_id);
235 }
236
237 pub fn links_under(&self, path: &Path) -> impl Iterator<Item = LinkID> + '_ {
238 let links = match self.search(path) {
239 NodeSearchResult::Found(node_id) => {
240 let node = &self.nodes[node_id];
241 match &node.kind {
242 NodeKind::Link(link_id) => vec![*link_id],
243 NodeKind::Directory(children) => {
244 let mut links = Vec::new();
245 let mut node_ids = Vec::from_iter(children.iter().copied());
246 while let Some(child_id) = node_ids.pop() {
247 let child = &self.nodes[child_id];
248 match &child.kind {
249 NodeKind::Link(link_id) => links.push(*link_id),
250 NodeKind::Directory(extra_children) => {
251 node_ids.extend(extra_children.iter().copied())
252 }
253 }
254 }
255 links
256 }
257 }
258 }
259 NodeSearchResult::NotFound(_) => vec![],
260 };
261 links.into_iter()
262 }
263
264 pub fn has_links_under(&self, path: &Path) -> bool {
265 // it does not matter what type of node is found. if a directory exists then there
266 // must be atleast one link under it.
267 match self.search(path) {
268 NodeSearchResult::Found(_) => true,
269 NodeSearchResult::NotFound(_) => false,
270 }
271 }
272
273 pub fn read_dir(&self, path: &Path) -> Result<impl Iterator<Item = DirNode> + '_> {
274 match self.search(path) {
275 NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind {
276 NodeKind::Link(_) => Err(anyhow::anyhow!("read dir called on a link")),
277 NodeKind::Directory(children) => Ok(children.iter().map(|child_id| {
278 let child = &self.nodes[*child_id];
279 match &child.kind {
280 NodeKind::Link(link_id) => DirNode::Link(*link_id),
281 NodeKind::Directory(_) => DirNode::Directory(self.build_path(*child_id)),
282 }
283 })),
284 },
285 NodeSearchResult::NotFound(_) => Err(anyhow::anyhow!("directory not found")),
286 }
287 }
288
289 pub fn build_path(&self, node_id: NodeID) -> PathBuf {
290 fn recursive_helper(nodes: &SlotMap<NodeID, Node>, nid: NodeID, pbuf: &mut PathBuf) {
291 if nid.is_null() {
292 return;
293 }
294 let parent_id = nodes[nid].parent;
295 recursive_helper(nodes, parent_id, pbuf);
296 pbuf.push(&nodes[nid].comp);
297 }
298
299 let mut node_path = PathBuf::default();
300 recursive_helper(&self.nodes, node_id, &mut node_path);
301 node_path
302 }
303
304 fn search(&self, path: &Path) -> NodeSearchResult {
305 debug_assert!(path_verify(path).is_ok());
306
307 let mut curr_node_id = self.root;
308 let mut comp_iter = path_iter_comps(path).peekable();
309 while let Some(comp) = comp_iter.next() {
310 if let Some(child_id) = self.directory_search_children(curr_node_id, comp) {
311 let child = &self.nodes[child_id];
312 match &child.kind {
313 NodeKind::Link(_) => {
314 if comp_iter.peek().is_some() {
315 return NodeSearchResult::NotFound(child_id);
316 } else {
317 return NodeSearchResult::Found(child_id);
318 }
319 }
320 NodeKind::Directory(_) => curr_node_id = child_id,
321 }
322 } else {
323 return NodeSearchResult::NotFound(curr_node_id);
324 }
325 }
326 NodeSearchResult::Found(curr_node_id)
327 }
328
329 // creates directories all the way up to and including path.
330 // there cannot be any links up to `path`.
331 fn directory_get_or_create(&mut self, path: &Path, initial_child: NodeID) -> NodeID {
332 // TODO: this could be replaced if the search function also returned the depth of the
333 // node and we skip those components and just start creating directories up to the
334 // path.
335 let mut curr_node_id = self.root;
336 for comp in path_iter_comps(path) {
337 if let Some(child_id) = self.directory_search_children(curr_node_id, comp) {
338 debug_assert!(std::matches!(
339 self.nodes[child_id].kind,
340 NodeKind::Directory(_)
341 ));
342 curr_node_id = child_id;
343 } else {
344 let new_node_id = self.nodes.insert(Node {
345 comp: comp.to_owned(),
346 parent: curr_node_id,
347 kind: NodeKind::Directory(Default::default()),
348 });
349 self.directory_add_child(curr_node_id, new_node_id);
350 curr_node_id = new_node_id;
351 }
352 }
353 self.directory_add_child(curr_node_id, initial_child);
354 curr_node_id
355 }
356
357 fn directory_search_children(&self, node_id: NodeID, comp: &OsStr) -> Option<NodeID> {
358 let node = &self.nodes[node_id];
359 match &node.kind {
360 NodeKind::Link(_) => unreachable!(),
361 NodeKind::Directory(children) => {
362 for &child_id in children {
363 let child = &self.nodes[child_id];
364 if child.comp == comp {
365 return Some(child_id);
366 }
367 }
368 }
369 }
370 None
371 }
372
373 fn directory_add_child(&mut self, node_id: NodeID, child_id: NodeID) {
374 let node = &mut self.nodes[node_id];
375 match &mut node.kind {
376 NodeKind::Link(_) => unreachable!(),
377 NodeKind::Directory(children) => children.insert(child_id),
378 };
379 }
380
381 fn directory_remove_child(&mut self, node_id: NodeID, child_id: NodeID) {
382 let node = &mut self.nodes[node_id];
383 match &mut node.kind {
384 NodeKind::Link(_) => unreachable!(),
385 NodeKind::Directory(children) => {
386 children.remove(&child_id);
387 if children.is_empty() && !node.parent.is_null() {
388 let parent_id = node.parent;
389 self.directory_remove_child(parent_id, node_id);
390 }
391 }
392 }
393 }
394}
395
396#[derive(Debug, Default, Clone)]
397pub struct Depot {
398 links: SlotMap<LinkID, Link>,
399 origin: DepotTree,
400}
401
402impl Depot {
403 pub fn link_create(
404 &mut self,
405 origin: impl AsRef<Path>,
406 destination: impl AsRef<Path>,
407 ) -> Result<LinkID> {
408 let origin = origin.as_ref();
409 let destination = destination.as_ref();
410 path_verify_link(origin)?;
411 path_verify_link(destination)?;
412 self.link_create_unchecked(origin, destination)
413 }
414
415 pub fn link_remove(&mut self, link_id: LinkID) {
416 let node_id = self.links[link_id].origin_id;
417 self.links.remove(link_id);
418 self.origin.link_remove(node_id);
419 }
420
421 /// moves the link specified by `link_id` to the path at `destination`.
422 /// if the link is already at the destination nothing is done.
423 /// if the destination is another link that that link is removed.
424 /// if the destination is under another link then an error is returned.
425 /// `destination` will be the link's new origin.
426 pub fn link_move(&mut self, link_id: LinkID, destination: impl AsRef<Path>) -> Result<()> {
427 let destination = destination.as_ref();
428 path_verify_link(destination)?;
429 self.link_move_unchecked(link_id, destination)
430 }
431
432 #[allow(unused)]
433 pub fn link_search(&self, path: impl AsRef<Path>) -> Result<SearchResult> {
434 let path = path.as_ref();
435 path_verify(path)?;
436 Ok(self.link_search_unchecked(path))
437 }
438
439 pub fn link_find(&self, path: impl AsRef<Path>) -> Result<Option<LinkID>> {
440 let path = path.as_ref();
441 path_verify(path)?;
442 Ok(self.link_find_unchecked(path))
443 }
444
445 pub fn links_under(&self, path: impl AsRef<Path>) -> Result<impl Iterator<Item = LinkID> + '_> {
446 let path = path.as_ref();
447 path_verify(path)?;
448 Ok(self.links_under_unchecked(path))
449 }
450
451 pub fn has_links_under(&self, path: impl AsRef<Path>) -> Result<bool> {
452 let path = path.as_ref();
453 path_verify(path)?;
454 Ok(self.has_links_under_unchecked(path))
455 }
456
457 pub fn links_verify_install(&self, link_ids: impl Iterator<Item = LinkID>) -> Result<()> {
458 let mut destination = DepotTree::default();
459 for link_id in link_ids {
460 let link = &self.links[link_id];
461 destination
462 .link_create(&link.destination, link_id)
463 .context("link destinations overlap")?;
464 }
465 Ok(())
466 }
467
468 pub fn link_view(&self, link_id: LinkID) -> LinkView {
469 LinkView {
470 link_id,
471 depot: self,
472 }
473 }
474
475 pub fn read_dir(&self, path: impl AsRef<Path>) -> Result<impl Iterator<Item = DirNode> + '_> {
476 let path = path.as_ref();
477 path_verify(path)?;
478 self.read_dir_unchecked(path)
479 }
480
481 fn link_create_unchecked(&mut self, origin: &Path, destination: &Path) -> Result<LinkID> {
482 let node_id = self.origin.link_create(origin, Default::default())?;
483 let link_id = self.links.insert(Link {
484 origin: origin.to_owned(),
485 destination: destination.to_owned(),
486 origin_id: node_id,
487 });
488 self.origin.link_update_id(node_id, link_id);
489 Ok(link_id)
490 }
491
492 fn link_move_unchecked(&mut self, link_id: LinkID, destination: &Path) -> Result<()> {
493 let link = &self.links[link_id];
494 if link.origin == destination {
495 return Ok(());
496 }
497 let node_id = link.origin_id;
498 self.origin.link_move(node_id, destination)?;
499 self.links[link_id].origin = destination.to_owned();
500 Ok(())
501 }
502
503 fn link_search_unchecked(&self, path: &Path) -> SearchResult {
504 self.origin.link_search(path)
505 }
506
507 fn link_find_unchecked(&self, path: &Path) -> Option<LinkID> {
508 match self.link_search_unchecked(path) {
509 SearchResult::Found(link_id) => Some(link_id),
510 _ => None,
511 }
512 }
513
514 fn links_under_unchecked(&self, path: &Path) -> impl Iterator<Item = LinkID> + '_ {
515 self.origin.links_under(path)
516 }
517
518 fn has_links_under_unchecked(&self, path: &Path) -> bool {
519 self.origin.has_links_under(path)
520 }
521
522 fn read_dir_unchecked(&self, path: &Path) -> Result<impl Iterator<Item = DirNode> + '_> {
523 self.origin.read_dir(path)
524 }
525}
526
527/// a verified link path is a path that:
528/// + is not empty
529/// + is relative
530/// + does not contain Prefix/RootDir/ParentDir
531fn path_verify_link(path: &Path) -> Result<()> {
532 // make sure the path is not empty
533 if path.components().next().is_none() {
534 return Err(DepotError::InvalidLinkPath.into());
535 }
536 path_verify(path).map_err(|_| DepotError::InvalidLinkPath.into())
537}
538
539/// a verified path is a path that:
540/// + is not empty
541/// + is relative
542/// + does not contain Prefix/RootDir/ParentDir
543fn path_verify(path: &Path) -> Result<()> {
544 // make sure the path is relative
545 // make sure the path does not contain '.' or '..'
546 for component in path.components() {
547 match component {
548 std::path::Component::Prefix(_)
549 | std::path::Component::RootDir
550 | std::path::Component::CurDir
551 | std::path::Component::ParentDir => return Err(DepotError::InvalidPath.into()),
552 std::path::Component::Normal(_) => {}
553 }
554 }
555 Ok(())
556}
557
558fn path_parent_or_empty(path: &Path) -> &Path {
559 path.parent().unwrap_or_else(|| Path::new(""))
560}
561
562/// Iterate over the components of a path.
563/// # Pre
564/// The path can only have "Normal" components.
565fn path_iter_comps(path: &Path) -> impl Iterator<Item = &OsStr> {
566 debug_assert!(path_verify(path).is_ok());
567 path.components().map(|component| match component {
568 std::path::Component::Normal(comp) => comp,
569 _ => unreachable!(),
570 })
571}
572
573mod disk {
574 use std::path::{Path, PathBuf};
575
576 use anyhow::Context;
577 use serde::{Deserialize, Serialize};
578
579 use super::Depot;
580
581 #[derive(Debug, Serialize, Deserialize)]
582 struct DiskLink {
583 origin: PathBuf,
584 destination: PathBuf,
585 }
586
587 #[derive(Debug, Serialize, Deserialize)]
588 struct DiskLinks {
589 links: Vec<DiskLink>,
590 }
591
592 pub fn read(path: &Path) -> anyhow::Result<Depot> {
593 let contents = std::fs::read_to_string(path).context("Failed to read depot file")?;
594 let disk_links = toml::from_str::<DiskLinks>(&contents)
595 .context("Failed to parse depot file")?
596 .links;
597 let mut depot = Depot::default();
598 for disk_link in disk_links {
599 depot
600 .link_create(disk_link.origin, disk_link.destination)
601 .context("Failed to build depot from file. File is in an invalid state")?;
602 }
603 Ok(depot)
604 }
605
606 pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> {
607 let mut links = Vec::with_capacity(depot.links.len());
608 for (_, link) in depot.links.iter() {
609 links.push(DiskLink {
610 origin: link.origin.clone(),
611 destination: link.destination.clone(),
612 });
613 }
614 let contents =
615 toml::to_string_pretty(&DiskLinks { links }).context("Failed to serialize depot")?;
616 std::fs::write(path, contents).context("Failed to write depot to file")?;
617 Ok(())
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn test_depot_link_create() {
627 let mut depot = Depot::default();
628 let f1 = depot.link_create("f1", "f1").unwrap();
629 let f2 = depot.link_create("f2", "f2").unwrap();
630 let f3 = depot.link_create("d1/f3", "d1/f3").unwrap();
631 let f4 = depot.link_create("d1/d2/f4", "d1/d2/d4").unwrap();
632
633 assert_eq!(depot.link_find("f1").unwrap(), Some(f1));
634 assert_eq!(depot.link_find("f2").unwrap(), Some(f2));
635 assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3));
636 assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4));
637
638 depot.link_create("f2", "").unwrap_err();
639 depot.link_create("", "d4").unwrap_err();
640 depot.link_create("f1/f3", "f3").unwrap_err();
641 }
642
643 #[test]
644 fn test_depot_link_remove() {
645 let mut depot = Depot::default();
646 let f1 = depot.link_create("d1/f1", "d1/f1").unwrap();
647 let f2 = depot.link_create("d1/f2", "d1/f2").unwrap();
648 let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap();
649 let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap();
650 let d3 = depot.link_create("d3", "d3").unwrap();
651
652 depot.link_remove(f2);
653 assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1));
654 assert_eq!(depot.link_find("d1/f2").unwrap(), None);
655 depot.link_remove(f4);
656 assert_eq!(depot.link_find("d1/d2/f4").unwrap(), None);
657 depot.link_remove(d3);
658 assert_eq!(depot.link_find("d3").unwrap(), None);
659 }
660
661 #[test]
662 fn test_depot_link_move() {
663 let mut depot = Depot::default();
664 let f1 = depot.link_create("d1/f1", "d1/f1").unwrap();
665 let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap();
666
667 depot.link_move(f1, "").unwrap_err();
668 depot.link_move(f1, "d1/f2/f1").unwrap_err();
669 depot.link_move(f1, "d1/f2").unwrap_err();
670
671 depot.link_move(f1, "f1").unwrap();
672 assert_eq!(depot.link_view(f1).origin(), Path::new("f1"));
673 depot.link_move(f1, "f2").unwrap();
674 assert_eq!(depot.link_view(f1).origin(), Path::new("f2"));
675 assert_eq!(depot.link_find("f2").unwrap(), Some(f1));
676 }
677
678 #[test]
679 fn test_depot_link_search() {
680 let mut depot = Depot::default();
681 let f1 = depot.link_create("d1/f1", "d1/f1").unwrap();
682 let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap();
683 let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap();
684 let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap();
685 let _d3 = depot.link_create("d3", "d3").unwrap();
686
687 assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1));
688 assert_eq!(
689 depot.link_search("d1/f1/f5").unwrap(),
690 SearchResult::Ancestor(f1)
691 );
692 assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound);
693 assert_eq!(
694 depot.link_search("d1/d2/f5").unwrap(),
695 SearchResult::NotFound
696 );
697 }
698
699 #[test]
700 fn test_depot_link_find() {
701 let mut depot = Depot::default();
702 let f1 = depot.link_create("d1/f1", "d1/f1").unwrap();
703 let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap();
704 let f3 = depot.link_create("d1/f3", "d1/f3").unwrap();
705 let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap();
706 let d3 = depot.link_create("d3", "d3").unwrap();
707
708 assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1));
709 assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3));
710 assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4));
711 assert_eq!(depot.link_find("d3").unwrap(), Some(d3));
712
713 assert_eq!(depot.link_find("d5").unwrap(), None);
714 assert_eq!(depot.link_find("d3/d5").unwrap(), None);
715 assert_eq!(depot.link_find("d1/d2/f5").unwrap(), None);
716 }
717
718 #[test]
719 fn test_depot_links_under() {
720 let mut depot = Depot::default();
721 let f1 = depot.link_create("d1/f1", "d1/f1").unwrap();
722 let f2 = depot.link_create("d1/f2", "d1/f2").unwrap();
723 let f3 = depot.link_create("d1/f3", "d1/f3").unwrap();
724 let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap();
725 let d3 = depot.link_create("d3", "d3").unwrap();
726
727 let under_f1 = depot.links_under("d1/f1").unwrap().collect::<Vec<_>>();
728 assert_eq!(under_f1, vec![f1]);
729
730 let under_d1 = depot.links_under("d1").unwrap().collect::<Vec<_>>();
731 let expected_under_d1 = vec![f1, f2, f3, f4];
732 assert!(
733 under_d1.len() == expected_under_d1.len()
734 && expected_under_d1.iter().all(|x| under_d1.contains(x))
735 );
736
737 let under_d2 = depot.links_under("d2").unwrap().collect::<Vec<_>>();
738 assert_eq!(under_d2, vec![]);
739
740 let under_d3 = depot.links_under("d3").unwrap().collect::<Vec<_>>();
741 assert_eq!(under_d3, vec![d3]);
742
743 let under_root = depot.links_under("").unwrap().collect::<Vec<_>>();
744 let expected_under_root = vec![f1, f2, f3, f4, d3];
745 assert!(
746 under_root.len() == expected_under_root.len()
747 && expected_under_root.iter().all(|x| under_root.contains(x))
748 );
749 }
750
751 #[test]
752 fn test_depot_has_links_under() {
753 let mut depot = Depot::default();
754 let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap();
755 let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap();
756 let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap();
757 let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap();
758 let _d3 = depot.link_create("d3", "d3").unwrap();
759
760 assert!(depot.has_links_under("").unwrap());
761 assert!(depot.has_links_under("d1").unwrap());
762 assert!(depot.has_links_under("d3").unwrap());
763 assert!(depot.has_links_under("d1/f1").unwrap());
764 assert!(depot.has_links_under("d1/d2").unwrap());
765 assert!(depot.has_links_under("d1/d2/f4").unwrap());
766
767 assert!(!depot.has_links_under("d2").unwrap());
768 assert!(!depot.has_links_under("d4").unwrap());
769 assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap());
770 }
771
772 #[test]
773 fn test_depot_links_verify_install() {
774 let mut depot = Depot::default();
775 let f1 = depot.link_create("nvim", ".config/nvim").unwrap();
776 let f2 = depot.link_create("alacritty", ".config/alacritty").unwrap();
777 let f3 = depot.link_create("bash/.bashrc", ".bashrc").unwrap();
778 let f4 = depot.link_create("bash_laptop/.bashrc", ".bashrc").unwrap();
779
780 depot
781 .links_verify_install(vec![f1, f2, f3].into_iter())
782 .unwrap();
783 depot
784 .links_verify_install(vec![f1, f2, f3, f4].into_iter())
785 .unwrap_err();
786 }
787
788 #[test]
789 fn test_depot_read_dir() {
790 let mut depot = Depot::default();
791 let f1 = depot.link_create("d1/f1", "d1/f1").unwrap();
792 let f2 = depot.link_create("d1/f2", "d1/f2").unwrap();
793 let f3 = depot.link_create("d1/f3", "d1/f3").unwrap();
794 let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap();
795 let _d3 = depot.link_create("d3", "d3").unwrap();
796
797 let read_dir = depot.read_dir("d1").unwrap().collect::<Vec<_>>();
798 let expected_read_dir = vec![
799 DirNode::Link(f1),
800 DirNode::Link(f2),
801 DirNode::Link(f3),
802 DirNode::Directory(PathBuf::from("d1/d2")),
803 ];
804 assert!(
805 read_dir.len() == expected_read_dir.len()
806 && expected_read_dir.iter().all(|x| read_dir.contains(x))
807 );
808 }
809
810 #[test]
811 fn test_path_verify() {
812 path_verify(Path::new("")).unwrap();
813 path_verify(Path::new("f1")).unwrap();
814 path_verify(Path::new("d1/f1")).unwrap();
815 path_verify(Path::new("d1/f1.txt")).unwrap();
816 path_verify(Path::new("d1/./f1.txt")).unwrap();
817
818 path_verify(Path::new("/")).unwrap_err();
819 path_verify(Path::new("./f1")).unwrap_err();
820 path_verify(Path::new("/d1/f1")).unwrap_err();
821 path_verify(Path::new("d1/../f1.txt")).unwrap_err();
822 path_verify(Path::new("/d1/../f1.txt")).unwrap_err();
823 }
824
825 #[test]
826 fn test_path_verify_link() {
827 path_verify_link(Path::new("f1")).unwrap();
828 path_verify_link(Path::new("d1/f1")).unwrap();
829 path_verify_link(Path::new("d1/f1.txt")).unwrap();
830 path_verify_link(Path::new("d1/./f1.txt")).unwrap();
831
832 path_verify_link(Path::new("")).unwrap_err();
833 path_verify_link(Path::new("/")).unwrap_err();
834 path_verify_link(Path::new("./f1")).unwrap_err();
835 path_verify_link(Path::new("/d1/f1")).unwrap_err();
836 path_verify_link(Path::new("d1/../f1.txt")).unwrap_err();
837 path_verify_link(Path::new("/d1/../f1.txt")).unwrap_err();
838 }
839
840 #[test]
841 fn test_path_iter_comps() {
842 let path = Path::new("comp1/comp2/./comp3/file.txt");
843 let mut iter = path_iter_comps(path);
844 assert_eq!(iter.next(), Some(OsStr::new("comp1")));
845 assert_eq!(iter.next(), Some(OsStr::new("comp2")));
846 assert_eq!(iter.next(), Some(OsStr::new("comp3")));
847 assert_eq!(iter.next(), Some(OsStr::new("file.txt")));
848 assert_eq!(iter.next(), None);
849 }
850}
diff --git a/src/dotup.rs b/src/dotup.rs
new file mode 100644
index 0000000..882d245
--- /dev/null
+++ b/src/dotup.rs
@@ -0,0 +1,533 @@
1use std::{
2 cmp::Ordering,
3 collections::HashSet,
4 path::{Path, PathBuf},
5};
6
7use ansi_term::Color;
8use anyhow::Context;
9
10use crate::{
11 depot::{self, Depot, DirNode, LinkID},
12 utils,
13};
14
15#[derive(Debug)]
16struct CanonicalPair {
17 origin: PathBuf,
18 destination: PathBuf,
19}
20
21#[derive(Debug, Clone)]
22enum StatusItem {
23 Link {
24 origin: PathBuf,
25 destination: PathBuf,
26 is_directory: bool,
27 },
28 Directory {
29 origin: PathBuf,
30 items: Vec<StatusItem>,
31 },
32 Unlinked {
33 origin: PathBuf,
34 is_directory: bool,
35 },
36}
37
38impl StatusItem {
39 fn display_ord_cmp(&self, other: &Self) -> Ordering {
40 match (self, other) {
41 (
42 StatusItem::Link {
43 origin: l_origin, ..
44 },
45 StatusItem::Link {
46 origin: r_origin, ..
47 },
48 ) => l_origin.cmp(r_origin),
49 (StatusItem::Link { .. }, StatusItem::Directory { .. }) => Ordering::Less,
50 (
51 StatusItem::Link {
52 is_directory: l_is_dir,
53 ..
54 },
55 StatusItem::Unlinked {
56 is_directory: u_is_dir,
57 ..
58 },
59 ) => {
60 if *u_is_dir && !*l_is_dir {
61 Ordering::Less
62 } else {
63 Ordering::Greater
64 }
65 }
66 (StatusItem::Directory { .. }, StatusItem::Link { .. }) => Ordering::Greater,
67 (
68 StatusItem::Directory {
69 origin: l_origin, ..
70 },
71 StatusItem::Directory {
72 origin: r_origin, ..
73 },
74 ) => l_origin.cmp(r_origin),
75 (StatusItem::Directory { .. }, StatusItem::Unlinked { .. }) => Ordering::Greater,
76 (
77 StatusItem::Unlinked {
78 is_directory: u_is_dir,
79 ..
80 },
81 StatusItem::Link {
82 is_directory: l_is_dir,
83 ..
84 },
85 ) => {
86 if *u_is_dir && !*l_is_dir {
87 Ordering::Greater
88 } else {
89 Ordering::Less
90 }
91 }
92 (StatusItem::Unlinked { .. }, StatusItem::Directory { .. }) => Ordering::Less,
93 (
94 StatusItem::Unlinked {
95 origin: l_origin, ..
96 },
97 StatusItem::Unlinked {
98 origin: r_origin, ..
99 },
100 ) => l_origin.cmp(r_origin),
101 }
102 }
103}
104
105#[derive(Debug)]
106pub struct Dotup {
107 depot: Depot,
108 depot_dir: PathBuf,
109 depot_path: PathBuf,
110 install_base: PathBuf,
111}
112
113impl Dotup {
114 fn new(depot: Depot, depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result<Self> {
115 assert!(depot_path.is_absolute());
116 assert!(depot_path.is_file());
117 assert!(install_base.is_absolute());
118 assert!(install_base.is_dir());
119 let depot_dir = {
120 let mut d = depot_path.clone();
121 d.pop();
122 d
123 };
124 Ok(Self {
125 depot,
126 depot_dir,
127 depot_path,
128 install_base,
129 })
130 }
131
132 pub fn link(&mut self, origin: impl AsRef<Path>, destination: impl AsRef<Path>) {
133 let link_result: anyhow::Result<()> = try {
134 let origin = self.prepare_relative_path(origin.as_ref())?;
135 let destination = destination.as_ref();
136 self.depot.link_create(origin, destination)?;
137 };
138 match link_result {
139 Ok(_) => {}
140 Err(e) => println!("Failed to create link : {e}"),
141 }
142 }
143
144 pub fn unlink(&mut self, paths: impl Iterator<Item = impl AsRef<Path>>, uninstall: bool) {
145 for origin in paths {
146 let unlink_result: anyhow::Result<()> = try {
147 let origin = self.prepare_relative_path(origin.as_ref())?;
148 let links_under: Vec<_> = self.depot.links_under(&origin)?.collect();
149 for link_id in links_under {
150 if uninstall && self.symlink_is_installed_by_link_id(link_id)? {
151 self.symlink_uninstall_by_link_id(link_id)?;
152 }
153 self.depot.link_remove(link_id);
154 }
155 };
156 match unlink_result {
157 Ok(_) => {}
158 Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()),
159 }
160 }
161 }
162
163 pub fn install(&self, paths: impl Iterator<Item = impl AsRef<Path>>) {
164 let install_result: anyhow::Result<()> = try {
165 let link_ids = self.link_ids_from_paths_iter(paths)?;
166 self.depot.links_verify_install(link_ids.iter().copied())?;
167
168 for link_id in link_ids {
169 self.symlink_install_by_link_id(link_id)?;
170 }
171 };
172 if let Err(e) = install_result {
173 println!("error while installing : {e}");
174 }
175 }
176
177 pub fn uninstall(&self, paths: impl Iterator<Item = impl AsRef<Path>>) {
178 let uninstall_result: anyhow::Result<()> = try {
179 let link_ids = self.link_ids_from_paths_iter(paths)?;
180 for link_id in link_ids {
181 if self.symlink_is_installed_by_link_id(link_id)? {
182 self.symlink_uninstall_by_link_id(link_id)?;
183 }
184 }
185 };
186 if let Err(e) = uninstall_result {
187 println!("error while uninstalling {e}",);
188 }
189 }
190
191 pub fn mv(
192 &mut self,
193 origins: impl Iterator<Item = impl AsRef<Path>>,
194 destination: impl AsRef<Path>,
195 ) {
196 let origins = {
197 let mut v = Vec::new();
198 for origin in origins {
199 match self.prepare_relative_path(origin.as_ref()) {
200 Ok(origin) => v.push(origin),
201 Err(e) => {
202 println!("invalid link {} : {e}", origin.as_ref().display());
203 return;
204 }
205 }
206 }
207 v
208 };
209 let destination = destination.as_ref();
210
211 // if we are moving multiple links then the destination must be a directory
212 if origins.len() > 1 && destination.is_dir() {
213 println!("destination must be a directory");
214 return;
215 }
216
217 for origin in origins {
218 if let Err(e) = self.mv_one(&origin, destination) {
219 println!("error moving link {} : {e}", origin.display());
220 }
221 }
222 }
223
224 fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> {
225 let link_id = match self.depot.link_find(origin)? {
226 Some(link_id) => link_id,
227 None => {
228 return Err(anyhow::anyhow!(format!(
229 "{} is not a link",
230 origin.display()
231 )))
232 }
233 };
234 let is_installed = self.symlink_is_installed_by_link_id(link_id)?;
235 let original_origin = self.depot.link_view(link_id).origin().to_owned();
236 self.depot.link_move(link_id, destination)?;
237 // move the actual file on disk
238 if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file") {
239 // unwrap: moving the link back to its origin place has to work
240 self.depot.link_move(link_id, original_origin).unwrap();
241 return Err(e);
242 }
243 // reinstall because we just moved the origin
244 if is_installed {
245 self.symlink_install_by_link_id(link_id)
246 .context("failed to reinstall link while moving")?;
247 }
248 Ok(())
249 }
250
251 pub fn status(&self) {
252 let status_result: anyhow::Result<()> = try {
253 let canonical_dir = utils::current_working_directory();
254 let item = self.status_path_to_item(&canonical_dir)?;
255 self.status_print_item(item, 0)?;
256 };
257 if let Err(e) = status_result {
258 println!("error while displaying status : {e}");
259 }
260 }
261 fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result<StatusItem> {
262 debug_assert!(canonical_path.is_absolute());
263 debug_assert!(canonical_path.exists());
264 let relative_path = self.prepare_relative_path(canonical_path)?;
265
266 let item = if canonical_path.is_dir() {
267 if let Some(link_id) = self.depot.link_find(&relative_path)? {
268 let destination = self.depot.link_view(link_id).destination().to_owned();
269 StatusItem::Link {
270 origin: relative_path,
271 destination,
272 is_directory: true,
273 }
274 } else if self.depot.has_links_under(&relative_path)? {
275 let mut items = Vec::new();
276 let mut collected_rel_paths = HashSet::<PathBuf>::new();
277 let directory_paths = utils::collect_paths_in_dir(&canonical_path)?;
278 for canonical_item_path in directory_paths {
279 let item = self.status_path_to_item(&canonical_item_path)?;
280 match &item {
281 StatusItem::Link { origin, .. } | StatusItem::Directory { origin, .. } => {
282 collected_rel_paths.insert(origin.to_owned());
283 }
284 _ => {}
285 }
286 items.push(item);
287 }
288
289 for dir_node in self.depot.read_dir(&relative_path)? {
290 match dir_node {
291 DirNode::Link(link_id) => {
292 let link_view = self.depot.link_view(link_id);
293 let link_rel_path = link_view.origin();
294 let link_rel_dest = link_view.destination();
295 if !collected_rel_paths.contains(link_rel_path) {
296 items.push(StatusItem::Link {
297 origin: link_rel_path.to_owned(),
298 destination: link_rel_dest.to_owned(),
299 is_directory: false,
300 });
301 }
302 }
303 DirNode::Directory(_) => {}
304 }
305 }
306
307 StatusItem::Directory {
308 origin: relative_path,
309 items,
310 }
311 } else {
312 StatusItem::Unlinked {
313 origin: relative_path,
314 is_directory: true,
315 }
316 }
317 } else if let Some(link_id) = self.depot.link_find(&relative_path)? {
318 let destination = self.depot.link_view(link_id).destination().to_owned();
319 StatusItem::Link {
320 origin: relative_path,
321 destination,
322 is_directory: false,
323 }
324 } else {
325 StatusItem::Unlinked {
326 origin: relative_path,
327 is_directory: false,
328 }
329 };
330 Ok(item)
331 }
332 fn status_print_item(&self, item: StatusItem, depth: u32) -> anyhow::Result<()> {
333 fn print_depth(d: u32) {
334 for _ in 0..d.saturating_sub(1) {
335 print!(" ");
336 }
337 }
338 fn origin_color(exists: bool, is_installed: bool) -> Color {
339 if !exists {
340 Color::Red
341 } else if is_installed {
342 Color::Green
343 } else {
344 Color::RGB(255, 127, 0)
345 }
346 }
347
348 let destination_color = Color::Blue;
349
350 print_depth(depth);
351 match item {
352 StatusItem::Link {
353 origin,
354 destination,
355 is_directory,
356 } => {
357 let canonical_origin = self.depot_dir.join(&origin);
358 let canonical_destination = self.install_base.join(&destination);
359 let file_name = Self::status_get_filename(&canonical_origin);
360 let is_installed =
361 self.symlink_is_installed(&canonical_origin, &canonical_destination)?;
362 let exists = canonical_origin.exists();
363 let origin_color = origin_color(exists, is_installed);
364 let directory_extra = if is_directory { "/" } else { "" };
365 println!(
366 "{}{} -> {}",
367 origin_color.paint(file_name),
368 directory_extra,
369 destination_color.paint(destination.display().to_string())
370 );
371 }
372 StatusItem::Directory { origin, mut items } => {
373 items.sort_by(|a, b| StatusItem::display_ord_cmp(a, b).reverse());
374 let directory_name = Self::status_get_filename(&origin);
375 if depth != 0 {
376 println!("{}/", directory_name);
377 }
378 for item in items {
379 self.status_print_item(item, depth + 1)?;
380 }
381 }
382 StatusItem::Unlinked {
383 origin,
384 is_directory,
385 } => {
386 let file_name = Self::status_get_filename(&origin);
387 let directory_extra = if is_directory { "/" } else { "" };
388 println!("{}{}", file_name, directory_extra);
389 }
390 }
391 Ok(())
392 }
393 fn status_get_filename(path: &Path) -> &str {
394 path.file_name()
395 .and_then(|s| s.to_str())
396 .unwrap_or_default()
397 }
398
399 fn prepare_relative_path(&self, origin: &Path) -> anyhow::Result<PathBuf> {
400 let canonical = utils::weakly_canonical(origin);
401 let relative = canonical
402 .strip_prefix(&self.depot_dir)
403 .context("Invalid origin path, not under depot directory")?;
404 Ok(relative.to_owned())
405 }
406
407 fn link_ids_from_paths_iter(
408 &self,
409 paths: impl Iterator<Item = impl AsRef<Path>>,
410 ) -> anyhow::Result<Vec<LinkID>> {
411 let mut link_ids = HashSet::<LinkID>::default();
412 for path in paths {
413 let path = self.prepare_relative_path(path.as_ref())?;
414 link_ids.extend(self.depot.links_under(&path)?);
415 }
416 Ok(Vec::from_iter(link_ids.into_iter()))
417 }
418
419 fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result<bool> {
420 let canonical_pair = self.canonical_pair_from_link_id(link_id);
421 self.symlink_is_installed(&canonical_pair.origin, &canonical_pair.destination)
422 }
423
424 fn symlink_is_installed(&self, origin: &Path, destination: &Path) -> anyhow::Result<bool> {
425 debug_assert!(origin.is_absolute());
426 debug_assert!(destination.is_absolute());
427
428 if destination.is_symlink() {
429 let symlink_destination = destination.read_link()?;
430 match symlink_destination.canonicalize() {
431 Ok(canonicalized) => Ok(origin == canonicalized),
432 Err(_) => Ok(false),
433 }
434 } else {
435 Ok(false)
436 }
437 }
438
439 fn symlink_install_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> {
440 let canonical_pair = self.canonical_pair_from_link_id(link_id);
441 self.symlink_install(&canonical_pair.origin, &canonical_pair.destination)
442 }
443
444 fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> {
445 debug_assert!(origin.is_absolute());
446 debug_assert!(destination.is_absolute());
447 log::debug!(
448 "symlink_install : {} -> {}",
449 origin.display(),
450 destination.display()
451 );
452
453 let destination_parent = destination
454 .parent()
455 .ok_or_else(|| anyhow::anyhow!("destination has no parent component"))?;
456 std::fs::create_dir_all(destination_parent).context("Failed to create directories")?;
457 // need to do this beacause if the destination path ends in '/' because the symlink
458 // functions will treat it as a directory but we want a file with that name.
459 let destination = destination.with_file_name(destination.file_name().unwrap());
460
461 let destination_exists = destination.exists();
462 let destination_is_symlink = destination.is_symlink();
463
464 if destination_exists && !destination_is_symlink {
465 return Err(anyhow::anyhow!("destination already exists"));
466 }
467
468 if destination_is_symlink {
469 log::debug!("symlink already exists, removing before recreating");
470 std::fs::remove_file(&destination)?;
471 }
472
473 log::debug!(
474 "creating filesystem symlink {} -> {}",
475 origin.display(),
476 destination.display()
477 );
478 std::os::unix::fs::symlink(origin, destination).context("failed to create symlink")?;
479
480 Ok(())
481 }
482
483 fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> {
484 debug_assert!(origin.is_absolute());
485 debug_assert!(destination.is_absolute());
486 let destination = destination.with_file_name(destination.file_name().unwrap());
487
488 if destination.is_symlink() {
489 let symlink_destination = destination.read_link()?.canonicalize()?;
490 if symlink_destination == origin {
491 std::fs::remove_file(&destination)?;
492 }
493 }
494
495 Ok(())
496 }
497
498 fn symlink_uninstall_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> {
499 let canonical_pair = self.canonical_pair_from_link_id(link_id);
500 self.symlink_uninstall(&canonical_pair.origin, &canonical_pair.destination)
501 }
502
503 fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair {
504 let link_view = self.depot.link_view(link_id);
505 let relative_origin = link_view.origin();
506 let relative_destination = link_view.destination();
507 let canonical_origin = self.depot_dir.join(relative_origin);
508 let canonical_destination = self.install_base.join(relative_destination);
509 CanonicalPair {
510 origin: canonical_origin,
511 destination: canonical_destination,
512 }
513 }
514}
515
516pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result<Dotup> {
517 let depot_path = depot_path
518 .canonicalize()
519 .context("Failed to canonicalize depot path")?;
520 let install_base = install_base
521 .canonicalize()
522 .context("Failed to canonicalize install base")?;
523 if !install_base.is_dir() {
524 return Err(anyhow::anyhow!("Install base must be a directory"));
525 }
526 let depot = depot::read(&depot_path)?;
527 Dotup::new(depot, depot_path, install_base)
528}
529
530pub fn write(dotup: &Dotup) -> anyhow::Result<()> {
531 depot::write(&dotup.depot_path, &dotup.depot)?;
532 Ok(())
533}
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 3mod depot;
4 4mod dotup;
5mod depot { 5mod 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
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 6
1562use std::path::PathBuf; 7use std::path::PathBuf;
1563 8
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644
index 0000000..a7e945a
--- /dev/null
+++ b/src/utils.rs
@@ -0,0 +1,156 @@
1use std::{
2 collections::VecDeque,
3 path::{Component, Path, PathBuf},
4};
5
6use crate::{
7 dotup::{self, Dotup},
8 Flags,
9};
10
11pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot";
12
13/// Returns a list of canonical paths to all the files in `dir`. This includes files in
14/// subdirectories.
15/// Fails if dir isnt a directory or if there is some other io error.
16pub fn collect_files_in_dir_recursive(dir: impl Into<PathBuf>) -> anyhow::Result<Vec<PathBuf>> {
17 let mut paths = Vec::new();
18 let mut dirs = VecDeque::new();
19 dirs.push_back(dir.into());
20
21 while let Some(dir) = dirs.pop_front() {
22 for entry in std::fs::read_dir(dir)? {
23 let entry = entry?;
24 let filetype = entry.file_type()?;
25 if filetype.is_dir() {
26 dirs.push_back(entry.path());
27 } else {
28 paths.push(entry.path());
29 }
30 }
31 }
32
33 Ok(paths)
34}
35
36pub fn collect_paths_in_dir(dir: impl AsRef<Path>) -> anyhow::Result<Vec<PathBuf>> {
37 Ok(std::fs::read_dir(dir)?
38 .filter_map(|e| e.ok())
39 .map(|e| e.path())
40 .collect())
41}
42
43pub fn read_dotup(flags: &Flags) -> anyhow::Result<Dotup> {
44 let depot_path = depot_path_from_flags(flags)?;
45 let install_base = install_base_from_flags(flags);
46 dotup::read(depot_path, install_base)
47}
48
49pub fn write_dotup(dotup: &Dotup) -> anyhow::Result<()> {
50 dotup::write(dotup)
51}
52
53pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result<PathBuf> {
54 match flags.depot {
55 Some(ref path) => Ok(path.clone()),
56 None => find_depot_path().ok_or_else(|| anyhow::anyhow!("Failed to find depot path")),
57 }
58}
59
60pub fn default_depot_path() -> PathBuf {
61 current_working_directory().join(DEFAULT_DEPOT_FILE_NAME)
62}
63
64pub fn find_depot_path() -> Option<PathBuf> {
65 let mut cwd = current_working_directory();
66 loop {
67 let path = cwd.join(DEFAULT_DEPOT_FILE_NAME);
68 if path.exists() {
69 break Some(path);
70 }
71 if !cwd.pop() {
72 break None;
73 }
74 }
75}
76
77pub fn install_base_from_flags(flags: &Flags) -> PathBuf {
78 match flags.install_base {
79 Some(ref path) => path.clone(),
80 None => default_install_base(),
81 }
82}
83
84pub fn default_install_base() -> PathBuf {
85 PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable"))
86}
87pub fn weakly_canonical(path: impl AsRef<Path>) -> PathBuf {
88 let cwd = current_working_directory();
89 weakly_canonical_cwd(path, cwd)
90}
91
92fn weakly_canonical_cwd(path: impl AsRef<Path>, cwd: PathBuf) -> PathBuf {
93 // Adapated from
94 // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
95 let path = path.as_ref();
96
97 let mut components = path.components().peekable();
98 let mut canonical = cwd;
99 let prefix = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
100 components.next();
101 PathBuf::from(c.as_os_str())
102 } else {
103 PathBuf::new()
104 };
105
106 for component in components {
107 match component {
108 Component::Prefix(_) => unreachable!(),
109 Component::RootDir => {
110 canonical = prefix.clone();
111 canonical.push(component.as_os_str())
112 }
113 Component::CurDir => {}
114 Component::ParentDir => {
115 canonical.pop();
116 }
117 Component::Normal(p) => canonical.push(p),
118 };
119 }
120
121 canonical
122}
123
124pub fn current_working_directory() -> PathBuf {
125 std::env::current_dir().expect("Failed to obtain current working directory")
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn weak_canonical_test() {
134 let cwd = PathBuf::from("/home/user");
135 assert_eq!(
136 PathBuf::from("/home/dest"),
137 weakly_canonical_cwd("../dest", cwd.clone())
138 );
139 assert_eq!(
140 PathBuf::from("/home/dest/configs/init.vim"),
141 weakly_canonical_cwd("../dest/configs/init.vim", cwd.clone())
142 );
143 assert_eq!(
144 PathBuf::from("/dest/configs/init.vim"),
145 weakly_canonical_cwd("/dest/configs/init.vim", cwd.clone())
146 );
147 assert_eq!(
148 PathBuf::from("/home/user/configs/nvim/lua/setup.lua"),
149 weakly_canonical_cwd("./configs/nvim/lua/setup.lua", cwd.clone())
150 );
151 assert_eq!(
152 PathBuf::from("/home/user/configs/nvim/lua/setup.lua"),
153 weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd)
154 );
155 }
156}