aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2022-02-06 18:32:54 +0000
committerdiogo464 <[email protected]>2022-02-06 18:32:54 +0000
commit2462119defef2d7f28cd5b15b09917c9a46e20b6 (patch)
treeddbdec5698fb95bb171c5d76bd9c8a25d7222713 /src
snapshot
Diffstat (limited to 'src')
-rw-r--r--src/main.rs997
1 files changed, 997 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..e2a659b
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,997 @@
1#![feature(try_blocks)]
2pub mod depot {
3 use std::{
4 collections::HashSet,
5 ffi::{OsStr, OsString},
6 ops::Deref,
7 path::{Path, PathBuf},
8 };
9
10 use slotmap::SlotMap;
11
12 pub use disk::{read, write};
13
14 slotmap::new_key_type! {pub struct LinkID;}
15 slotmap::new_key_type! {struct NodeID;}
16
17 #[derive(Debug, Clone, PartialEq, Eq)]
18 pub enum SearchResult {
19 Found(LinkID),
20 Ancestor(LinkID),
21 NotFound,
22 }
23
24 #[derive(Debug)]
25 pub struct LinkView<'a> {
26 link_id: LinkID,
27 depot: &'a Depot,
28 }
29
30 impl<'a> LinkView<'a> {
31 pub fn origin(&self) -> &Path {
32 &self.depot.links[self.link_id].origin
33 }
34
35 pub fn destination(&self) -> &Path {
36 &self.depot.links[self.link_id].destination
37 }
38 }
39
40 // wrapper for a path under the depot
41 // this path is relative and does not contain `..` or similar
42 // Deref(Path)
43 struct DepotPath(PathBuf);
44 impl Deref for DepotPath {
45 type Target = Path;
46
47 fn deref(&self) -> &Self::Target {
48 self.0.deref()
49 }
50 }
51
52 #[derive(Debug, Clone)]
53 struct Node {
54 comp: OsString,
55 parent: NodeID,
56 kind: NodeKind,
57 }
58
59 #[derive(Debug, Clone)]
60 enum NodeKind {
61 Link(LinkID),
62 Directory(HashSet<NodeID>),
63 }
64
65 #[derive(Debug, Clone)]
66 struct Link {
67 origin: PathBuf,
68 destination: PathBuf,
69 node_id: NodeID,
70 }
71
72 #[derive(Debug, Clone)]
73 pub struct Depot {
74 links: SlotMap<LinkID, Link>,
75 nodes: SlotMap<NodeID, Node>,
76 root: NodeID,
77 }
78
79 impl Default for Depot {
80 fn default() -> Self {
81 let mut nodes = SlotMap::default();
82 let root = nodes.insert(Node {
83 comp: Default::default(),
84 parent: Default::default(),
85 kind: NodeKind::Directory(Default::default()),
86 });
87 Self {
88 links: Default::default(),
89 nodes,
90 root,
91 }
92 }
93 }
94
95 impl Depot {
96 pub fn create(
97 &mut self,
98 origin: impl AsRef<Path>,
99 destination: impl AsRef<Path>,
100 ) -> anyhow::Result<LinkID> {
101 let origin = origin.as_ref();
102 let destination = destination.as_ref();
103 verify_path(origin)?;
104 verify_path(destination)?;
105
106 // example
107 // origin = fish/config.fish
108 // destination = .config/fish/config.fish
109
110 // search
111 // if ancestor - return error
112 // if found - update destination
113 // if not found - create
114
115 match self.search_unchecked(&origin) {
116 SearchResult::Found(link_id) => {
117 let link = &mut self.links[link_id];
118 link.destination = destination.to_owned();
119 Ok(link_id)
120 }
121 SearchResult::Ancestor(_) => Err(anyhow::anyhow!(
122 "An ancestor of this path is already linked"
123 )),
124 SearchResult::NotFound => {
125 let link_id = self.links.insert(Link {
126 origin: origin.to_owned(),
127 destination: destination.to_owned(),
128 node_id: Default::default(),
129 });
130 let node_id = self.node_create_link(origin, link_id);
131 self.links[link_id].node_id = node_id;
132 Ok(link_id)
133 }
134 }
135 }
136
137 pub fn move_link(
138 &mut self,
139 link_id: LinkID,
140 destination: impl AsRef<Path>,
141 ) -> anyhow::Result<()> {
142 let destination = destination.as_ref();
143 verify_path(destination)?;
144
145 let link_node_id = self.links[link_id].node_id;
146 let link_parent_node_id = self.nodes[link_node_id].parent;
147 let (node_id, found) = self.node_search(destination);
148
149 // the link is already at the destination
150 if found && node_id == link_node_id {
151 return Ok(());
152 }
153
154 if found {
155 let node = &self.nodes[node_id];
156 match &node.kind {
157 NodeKind::Link(node_link_id) => {
158 let node_parent_id = node.parent;
159 let node_link_id = *node_link_id;
160 assert_ne!(link_id, node_link_id);
161 self.remove(node_link_id);
162 self.node_child_remove(link_parent_node_id, link_node_id);
163 self.node_child_add(node_parent_id, link_node_id);
164 self.node_set_parent(link_node_id, node_parent_id);
165 Ok(())
166 }
167 NodeKind::Directory(..) => Err(anyhow::anyhow!(
168 "Cannot move link, other links exist under the destination"
169 )),
170 }
171 } else {
172 let node = &self.nodes[node_id];
173 match &node.kind {
174 NodeKind::Link(..) => Err(anyhow::anyhow!(
175 "Cannot move link, an ancestor is already linked"
176 )),
177 NodeKind::Directory(_) => {
178 let new_node_id = self.node_create_link(destination, link_id);
179 self.node_remove(link_node_id);
180 self.links[link_id].node_id = new_node_id;
181 Ok(())
182 }
183 }
184 }
185 }
186
187 pub fn remove(&mut self, link_id: LinkID) {
188 let node_id = self.links[link_id].node_id;
189 self.node_remove(node_id);
190 self.links.remove(link_id);
191 }
192
193 pub fn link_view(&self, link_id: LinkID) -> LinkView {
194 LinkView {
195 link_id,
196 depot: self,
197 }
198 }
199
200 /// searchs for the link at `origin`.
201 /// returns SearchResult::Found(..) if there is a link at `origin`.
202 /// returns SearchResult::Ancestor(..) if an ancestor of `origin` is linked.
203 /// returns SearchResult::NotFound otherwise.
204 pub fn search(&self, origin: impl AsRef<Path>) -> anyhow::Result<SearchResult> {
205 let origin = origin.as_ref();
206 verify_path(origin)?;
207 Ok(self.search_unchecked(&origin))
208 }
209
210 /// returns an iterator for all the links at or under the given path.
211 pub fn links_under(
212 &self,
213 path: impl AsRef<Path>,
214 ) -> anyhow::Result<impl Iterator<Item = LinkID> + '_> {
215 let path = path.as_ref();
216 verify_path(path)?;
217
218 let mut link_ids = Vec::new();
219 if let Some(node_id) = self.node_find(path) {
220 let mut node_ids = vec![node_id];
221 while let Some(node_id) = node_ids.pop() {
222 let node = &self.nodes[node_id];
223 match &node.kind {
224 NodeKind::Link(link_id) => link_ids.push(*link_id),
225 NodeKind::Directory(children) => node_ids.extend(children.iter().copied()),
226 }
227 }
228 }
229 Ok(link_ids.into_iter())
230 }
231
232 /// returns true if the `path` is a link or contains an ancestor that is linked.
233 /// returns false otherwise.
234 pub fn is_linked(&self, path: impl AsRef<Path>) -> bool {
235 match self.search(path) {
236 Ok(SearchResult::Found(..)) | Ok(SearchResult::Ancestor(..)) => true,
237 _ => false,
238 }
239 }
240
241 fn search_unchecked(&self, origin: &Path) -> SearchResult {
242 debug_assert!(verify_path(origin).is_ok());
243
244 let mut origin_comps = iter_path_comps(&origin);
245 let mut curr_node = self.root;
246 'outer: loop {
247 let node = &self.nodes[curr_node];
248 let curr_comp = origin_comps.next();
249 match &node.kind {
250 NodeKind::Link(link_id) => match curr_comp {
251 Some(_) => break SearchResult::Ancestor(*link_id),
252 None => break SearchResult::Found(*link_id),
253 },
254 NodeKind::Directory(children) => match curr_comp {
255 Some(curr_comp) => {
256 for &child_id in children.iter() {
257 let child = &self.nodes[child_id];
258 if &child.comp == curr_comp {
259 curr_node = child_id;
260 continue 'outer;
261 }
262 }
263 break SearchResult::NotFound;
264 }
265 None => break SearchResult::NotFound,
266 },
267 }
268 }
269 }
270
271 /// creates a new directory node with no children.
272 /// the node specified by `parent` must be a directory node.
273 fn node_create_dir_empty(&mut self, parent: NodeID, comp: OsString) -> NodeID {
274 let node_id = self.nodes.insert(Node {
275 comp,
276 parent,
277 kind: NodeKind::Directory(Default::default()),
278 });
279 self.node_child_add(parent, node_id);
280 node_id
281 }
282
283 /// all the nodes up to the node to be created have to be directory nodes.
284 /// `path` must be a verified path.
285 fn node_create_link(&mut self, path: &Path, link_id: LinkID) -> NodeID {
286 assert!(verify_path(path).is_ok());
287 let mut curr_node_id = self.root;
288 let mut path_comps = iter_path_comps(path).peekable();
289 // unwrap: a verified path has atleast 1 component
290 let mut curr_path_comp = path_comps.next().unwrap();
291
292 while path_comps.peek().is_some() {
293 let next_node = match self.node_children_search(curr_node_id, curr_path_comp) {
294 Some(child_id) => child_id,
295 None => self.node_create_dir_empty(curr_node_id, curr_path_comp.to_owned()),
296 };
297 curr_node_id = next_node;
298 // unwrap: we known next is Some beacause of this loop's condition
299 curr_path_comp = path_comps.next().unwrap();
300 }
301
302 let new_node = self.nodes.insert(Node {
303 comp: curr_path_comp.to_owned(),
304 parent: curr_node_id,
305 kind: NodeKind::Link(link_id),
306 });
307 self.node_child_add(curr_node_id, new_node);
308 new_node
309 }
310
311 /// finds the node at the given path.
312 /// `path` must be a verified path.
313 fn node_find(&self, path: &Path) -> Option<NodeID> {
314 match self.node_search(path) {
315 (node_id, true) => Some(node_id),
316 _ => None,
317 }
318 }
319
320 /// searches for the node at `path`. if that node does not exists then it returns the
321 /// closest node. return (closest_node, found)
322 fn node_search(&self, path: &Path) -> (NodeID, bool) {
323 debug_assert!(verify_path(path).is_ok());
324
325 let mut origin_comps = iter_path_comps(&path).peekable();
326 let mut curr_node = self.root;
327 'outer: loop {
328 let node = &self.nodes[curr_node];
329 match origin_comps.next() {
330 Some(curr_comp) => match &node.kind {
331 NodeKind::Link(..) => break (curr_node, false),
332 NodeKind::Directory(children) => {
333 for &child_id in children.iter() {
334 let child = &self.nodes[child_id];
335 if &child.comp == curr_comp {
336 curr_node = child_id;
337 continue 'outer;
338 }
339 }
340 break (curr_node, false);
341 }
342 },
343 None => break (curr_node, true),
344 }
345 }
346 }
347
348 /// adds `new_child` to `node_id`'s children.
349 /// the node specified by `node_id` must be a directory node.
350 fn node_child_add(&mut self, node_id: NodeID, new_child: NodeID) {
351 let node = &mut self.nodes[node_id];
352 match node.kind {
353 NodeKind::Directory(ref mut children) => {
354 children.insert(new_child);
355 }
356 _ => unreachable!(),
357 }
358 }
359
360 /// searchs for a child with the given comp and returns its id.
361 /// the node specified by `node_id` must be a directory node.
362 fn node_children_search(&self, node_id: NodeID, search_comp: &OsStr) -> Option<NodeID> {
363 let child_ids = match &self.nodes[node_id].kind {
364 NodeKind::Directory(c) => c,
365 _ => unreachable!(),
366 };
367 for &child_id in child_ids {
368 let child = &self.nodes[child_id];
369 if child.comp == search_comp {
370 return Some(child_id);
371 }
372 }
373 None
374 }
375
376 /// removes `child` from `node_id`'s children.
377 /// the node specified by `node_id` must be a directory node and it must contain the node
378 /// `child`.
379 fn node_child_remove(&mut self, node_id: NodeID, child: NodeID) {
380 let node = &mut self.nodes[node_id];
381 let remove_node = match &mut node.kind {
382 NodeKind::Directory(children) => {
383 let contained = children.remove(&child);
384 assert!(contained);
385 children.is_empty()
386 }
387 _ => unreachable!(),
388 };
389 if remove_node && node_id != self.root {
390 self.node_remove(node_id);
391 }
392 }
393
394 fn node_set_parent(&mut self, node_id: NodeID, parent: NodeID) {
395 self.nodes[node_id].parent = parent;
396 }
397
398 fn node_remove(&mut self, node_id: NodeID) {
399 debug_assert!(node_id != self.root);
400 debug_assert!(self.nodes.contains_key(node_id));
401
402 let node = self.nodes.remove(node_id).unwrap();
403 match node.kind {
404 NodeKind::Link(..) => {}
405 NodeKind::Directory(children) => {
406 // Right now directory nodes are only removed from inside this function and
407 // we do not remove directories with children
408 assert!(children.is_empty());
409 }
410 }
411 let parent_id = node.parent;
412 self.node_child_remove(parent_id, node_id);
413 }
414 }
415
416 mod disk {
417 use std::path::{Path, PathBuf};
418
419 use anyhow::Context;
420 use serde::{Deserialize, Serialize};
421
422 use super::Depot;
423
424 #[derive(Debug, Serialize, Deserialize)]
425 struct DiskLink {
426 origin: PathBuf,
427 destination: PathBuf,
428 }
429
430 #[derive(Debug, Serialize, Deserialize)]
431 struct DiskLinks {
432 links: Vec<DiskLink>,
433 }
434
435 pub fn read(path: &Path) -> anyhow::Result<Depot> {
436 let contents = std::fs::read_to_string(path).context("Failed to read depot file")?;
437 let disk_links = toml::from_str::<DiskLinks>(&contents)
438 .context("Failed to parse depot file")?
439 .links;
440 let mut depot = Depot::default();
441 for disk_link in disk_links {
442 depot
443 .create(disk_link.origin, disk_link.destination)
444 .context("Failed to build depot from file. File is in an invalid state")?;
445 }
446 Ok(depot)
447 }
448
449 pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> {
450 let mut links = Vec::with_capacity(depot.links.len());
451 for (_, link) in depot.links.iter() {
452 links.push(DiskLink {
453 origin: link.origin.clone(),
454 destination: link.destination.clone(),
455 });
456 }
457 let contents = toml::to_string_pretty(&DiskLinks { links })
458 .context("Failed to serialize depot")?;
459 std::fs::write(path, contents).context("Failed to write depot to file")?;
460 Ok(())
461 }
462 }
463
464 fn verify_path(path: &Path) -> anyhow::Result<()> {
465 // make sure the path is not empty
466 // make sure the path is relative
467 // make sure the path does not contain '.' or '..'
468 if path.components().next().is_none() {
469 return Err(anyhow::anyhow!("Path cannot be empty"));
470 }
471 for component in path.components() {
472 match component {
473 std::path::Component::Prefix(_) => {
474 return Err(anyhow::anyhow!("Path cannot have prefix"))
475 }
476 std::path::Component::RootDir => {
477 return Err(anyhow::anyhow!("Path must be relative"))
478 }
479 std::path::Component::CurDir | std::path::Component::ParentDir => {
480 return Err(anyhow::anyhow!("Path cannot contain '.' or '..'"))
481 }
482 std::path::Component::Normal(_) => {}
483 }
484 }
485 Ok(())
486 }
487
488 /// Iterate over the components of a path.
489 /// # Pre
490 /// The path can only have "Normal" components.
491 fn iter_path_comps(path: &Path) -> impl Iterator<Item = &OsStr> {
492 debug_assert!(verify_path(path).is_ok());
493 path.components().map(|component| match component {
494 std::path::Component::Normal(comp) => comp,
495 _ => unreachable!(),
496 })
497 }
498
499 #[cfg(test)]
500 mod tests {
501 use super::*;
502
503 #[test]
504 fn test_depot_create() {
505 let mut depot = Depot::default();
506 depot.create("", "dest1.txt").unwrap_err();
507 depot.create("comp1.txt", "").unwrap_err();
508 depot.create("", "").unwrap_err();
509
510 depot.create("comp1.txt", "dest1.txt").unwrap();
511 depot.create("comp1.txt", "dest1_updated.txt").unwrap();
512 depot
513 .create("./comp1.txt", "dest1_updated.txt")
514 .unwrap_err();
515 depot.create("/comp1.txt", "dest1.txt").unwrap_err();
516 depot.create("dir1/", "destdir1/").unwrap();
517 depot.create("dir1/file1.txt", "destfile1.txt").unwrap_err();
518 }
519
520 #[test]
521 fn test_depot_move_link() {
522 let mut depot = Depot::default();
523 let f1 = depot.create("d1/f1", "d1/f1").unwrap();
524 let _f2 = depot.create("d1/f2", "d1/f2").unwrap();
525
526 depot.move_link(f1, "d1/f2/f1").unwrap_err();
527 depot.move_link(f1, "d1").unwrap_err();
528
529 depot.move_link(f1, "").unwrap();
530 depot.move_link(f1, "d2/f1").unwrap();
531 }
532
533 #[test]
534 fn test_depot_remove() {
535 let mut depot = Depot::default();
536 let f1 = depot.create("d1/f1", "d1/f1").unwrap();
537 assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::Found(f1));
538 depot.remove(f1);
539 assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::NotFound);
540 }
541
542 #[test]
543 fn test_depot_search() {
544 let mut depot = Depot::default();
545 let f1 = depot.create("d1/f1", "d1/f1").unwrap();
546 let f2 = depot.create("d1/f2", "d1/f2").unwrap();
547 let f3 = depot.create("d1/f3", "d1/f3").unwrap();
548 let f4 = depot.create("d1/d2/f4", "d2/f4").unwrap();
549 let d3 = depot.create("d3", "d3").unwrap();
550
551 assert_eq!(depot.search("d1").unwrap(), SearchResult::NotFound,);
552 assert_eq!(depot.search("d1/f1").unwrap(), SearchResult::Found(f1),);
553 assert_eq!(depot.search("d1/f2").unwrap(), SearchResult::Found(f2),);
554 assert_eq!(depot.search("d1/f3").unwrap(), SearchResult::Found(f3),);
555 assert_eq!(depot.search("d1/d2/f4").unwrap(), SearchResult::Found(f4),);
556 assert_eq!(depot.search("d1/d2/f5").unwrap(), SearchResult::NotFound,);
557 assert_eq!(depot.search("d3/f6").unwrap(), SearchResult::Ancestor(d3),);
558 }
559
560 #[test]
561 fn test_iter_path_comps() {
562 let path = Path::new("comp1/comp2/./comp3/file.txt");
563 let mut iter = iter_path_comps(path);
564 assert_eq!(iter.next(), Some(OsStr::new("comp1")));
565 assert_eq!(iter.next(), Some(OsStr::new("comp2")));
566 assert_eq!(iter.next(), Some(OsStr::new("comp3")));
567 assert_eq!(iter.next(), Some(OsStr::new("file.txt")));
568 assert_eq!(iter.next(), None);
569 }
570 }
571}
572
573mod dotup {
574 use std::{
575 collections::HashSet,
576 path::{Path, PathBuf},
577 };
578
579 use anyhow::Context;
580
581 use crate::depot::{self, Depot, LinkID};
582
583 #[derive(Debug)]
584 struct CanonicalPair {
585 link_id: LinkID,
586 origin: PathBuf,
587 destination: PathBuf,
588 }
589
590 #[derive(Debug)]
591 pub struct Dotup {
592 depot: Depot,
593 depot_dir: PathBuf,
594 depot_path: PathBuf,
595 install_base: PathBuf,
596 }
597
598 impl Dotup {
599 fn new(depot: Depot, depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result<Self> {
600 assert!(depot_path.is_absolute());
601 assert!(depot_path.is_file());
602 assert!(install_base.is_absolute());
603 assert!(install_base.is_dir());
604 let depot_dir = {
605 let mut d = depot_path.clone();
606 d.pop();
607 d
608 };
609 Ok(Self {
610 depot,
611 depot_dir,
612 depot_path,
613 install_base,
614 })
615 }
616
617 pub fn link(&mut self, origin: impl AsRef<Path>, destination: impl AsRef<Path>) {
618 let link_result: anyhow::Result<()> = try {
619 let origin = self.prepare_origin_path(origin.as_ref())?;
620 let destination = destination.as_ref();
621 self.depot.create(origin, destination)?;
622 };
623 match link_result {
624 Ok(_) => {}
625 Err(e) => println!("Failed to create link : {e}"),
626 }
627 }
628
629 pub fn unlink(&mut self, paths: impl Iterator<Item = impl AsRef<Path>>) {
630 for origin in paths {
631 let unlink_result: anyhow::Result<()> = try {
632 let origin = self.prepare_origin_path(origin.as_ref())?;
633 let search_results = self.depot.search(&origin)?;
634 match search_results {
635 depot::SearchResult::Found(link_id) => {
636 self.depot.remove(link_id);
637 println!("removed link {}", origin.display());
638 }
639 depot::SearchResult::Ancestor(_) | depot::SearchResult::NotFound => {
640 println!("{} is not linked", origin.display())
641 }
642 }
643 };
644 match unlink_result {
645 Ok(_) => {}
646 Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()),
647 }
648 }
649 }
650
651 pub fn install(&self, paths: impl Iterator<Item = impl AsRef<Path>>) {
652 let mut already_linked: HashSet<LinkID> = Default::default();
653 for origin in paths {
654 let install_result: anyhow::Result<()> = try {
655 let origin = self.prepare_origin_path(origin.as_ref())?;
656 let canonical_pairs = self.canonical_pairs_under(&origin)?;
657 for pair in canonical_pairs {
658 if already_linked.contains(&pair.link_id) {
659 continue;
660 }
661 self.install_symlink(&pair.origin, &pair.destination)?;
662 already_linked.insert(pair.link_id);
663 }
664 };
665 if let Err(e) = install_result {
666 println!("error while installing {} : {e}", origin.as_ref().display());
667 }
668 }
669 }
670
671 pub fn uninstall(&self, paths: impl Iterator<Item = impl AsRef<Path>>) {
672 for origin in paths {
673 let uninstall_result: anyhow::Result<()> = try {
674 let origin = self.prepare_origin_path(origin.as_ref())?;
675 let canonical_pairs = self.canonical_pairs_under(&origin)?;
676 for pair in canonical_pairs {
677 self.uninstall_symlink(&pair.origin, &pair.destination)?;
678 }
679 };
680 if let Err(e) = uninstall_result {
681 println!(
682 "error while uninstalling {} : {e}",
683 origin.as_ref().display()
684 );
685 }
686 }
687 }
688
689 pub fn mv(&mut self, from: impl Iterator<Item = impl AsRef<Path>>, to: impl AsRef<Path>) {
690 let to = to.as_ref();
691 let from: Vec<_> = from.map(|p| p.as_ref().to_owned()).collect();
692 match from.as_slice() {
693 [] => unreachable!(),
694 [from] => self.mv_one(from, to),
695 [from @ ..] => self.mv_many(from, to),
696 }
697 }
698
699 fn mv_one(&mut self, from: &Path, to: &Path) {}
700
701 fn mv_many(&mut self, from: &[PathBuf], to: &Path) {}
702
703 fn prepare_origin_path(&self, origin: &Path) -> anyhow::Result<PathBuf> {
704 let canonical = origin
705 .canonicalize()
706 .context("Failed to canonicalize origin path")?;
707 let relative = canonical
708 .strip_prefix(&self.depot_dir)
709 .context("Invalid origin path, not under depot directory")?;
710 Ok(relative.to_owned())
711 }
712
713 // returns the canonical pairs (origin, destination) for all links under `path`.
714 fn canonical_pairs_under(&self, path: &Path) -> anyhow::Result<Vec<CanonicalPair>> {
715 let origin = self.prepare_origin_path(path)?;
716 let mut paths = Vec::new();
717 for link_id in self.depot.links_under(origin)? {
718 let link_view = self.depot.link_view(link_id);
719 let relative_origin = link_view.origin();
720 let relative_destination = link_view.destination();
721 let canonical_origin = self.depot_dir.join(relative_origin);
722 let canonical_destination = self.install_base.join(relative_destination);
723 paths.push(CanonicalPair {
724 link_id,
725 origin: canonical_origin,
726 destination: canonical_destination,
727 });
728 }
729 Ok(paths)
730 }
731
732 fn install_symlink(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> {
733 debug_assert!(origin.is_absolute());
734 debug_assert!(destination.is_absolute());
735
736 if let Some(destination_parent) = destination.parent() {
737 std::fs::create_dir_all(destination_parent)
738 .context("Failed to create directories")?;
739 }
740
741 let destination_exists = destination.exists();
742 let destination_is_symlink = destination.is_symlink();
743
744 if destination_exists && !destination_is_symlink {
745 return Err(anyhow::anyhow!("destination already exists"));
746 }
747
748 if destination_is_symlink {
749 std::fs::remove_file(&destination)?;
750 }
751 std::os::unix::fs::symlink(origin, destination).context("Failed to create symlink")?;
752
753 Ok(())
754 }
755
756 fn uninstall_symlink(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> {
757 debug_assert!(origin.is_absolute());
758 debug_assert!(destination.is_absolute());
759
760 if destination.is_symlink() {
761 let symlink_destination = destination.read_link()?.canonicalize()?;
762 if symlink_destination == origin {
763 std::fs::remove_file(&destination)?;
764 }
765 }
766
767 Ok(())
768 }
769 }
770
771 pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result<Dotup> {
772 let depot_path = depot_path
773 .canonicalize()
774 .context("Failed to canonicalize depot path")?;
775 let install_base = install_base
776 .canonicalize()
777 .context("Failed to canonicalize install base")?;
778 if !install_base.is_dir() {
779 return Err(anyhow::anyhow!("Install base must be a directory"));
780 }
781 let depot = depot::read(&depot_path)?;
782 Dotup::new(depot, depot_path, install_base)
783 }
784
785 pub fn write(dotup: &Dotup) -> anyhow::Result<()> {
786 depot::write(&dotup.depot_path, &dotup.depot)?;
787 Ok(())
788 }
789}
790
791mod utils {
792 use std::path::PathBuf;
793
794 use crate::{
795 depot::{self, Depot},
796 dotup::{self, Dotup},
797 Flags,
798 };
799
800 pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot";
801
802 pub fn read_dotup(flags: &Flags) -> anyhow::Result<Dotup> {
803 let depot_path = depot_path_from_flags(flags)?;
804 let install_base = install_base_from_flags(flags);
805 dotup::read(depot_path, install_base)
806 }
807
808 pub fn write_dotup(dotup: &Dotup) -> anyhow::Result<()> {
809 dotup::write(dotup)
810 }
811
812 pub fn read_depot(flags: &Flags) -> anyhow::Result<Depot> {
813 let depot_path = depot_path_from_flags(flags)?;
814 let depot = depot::read(&depot_path)?;
815 Ok(depot)
816 }
817
818 pub fn write_depot(flags: &Flags, depot: &Depot) -> anyhow::Result<()> {
819 let depot_path = depot_path_from_flags(flags)?;
820 depot::write(&depot_path, depot)?;
821 Ok(())
822 }
823
824 pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result<PathBuf> {
825 match flags.depot {
826 Some(ref path) => Ok(path.clone()),
827 None => find_depot_path().ok_or_else(|| anyhow::anyhow!("Failed to find depot path")),
828 }
829 }
830
831 pub fn default_depot_path() -> PathBuf {
832 current_working_directory().join(DEFAULT_DEPOT_FILE_NAME)
833 }
834
835 pub fn find_depot_path() -> Option<PathBuf> {
836 let mut cwd = current_working_directory();
837 loop {
838 let path = cwd.join(DEFAULT_DEPOT_FILE_NAME);
839 if path.exists() {
840 break Some(path);
841 }
842 if !cwd.pop() {
843 break None;
844 }
845 }
846 }
847
848 pub fn install_base_from_flags(flags: &Flags) -> PathBuf {
849 match flags.install_base {
850 Some(ref path) => path.clone(),
851 None => default_install_base(),
852 }
853 }
854
855 pub fn default_install_base() -> PathBuf {
856 PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable"))
857 }
858
859 fn current_working_directory() -> PathBuf {
860 std::env::current_dir().expect("Failed to obtain current working directory")
861 }
862}
863
864use std::path::PathBuf;
865
866use clap::Parser;
867use utils::DEFAULT_DEPOT_FILE_NAME;
868
869#[derive(Parser, Debug)]
870pub struct Flags {
871 #[clap(long)]
872 depot: Option<PathBuf>,
873 #[clap(long)]
874 install_base: Option<PathBuf>,
875}
876
877#[derive(Parser, Debug)]
878#[clap(author, version, about, long_about = None)]
879struct Args {
880 #[clap(flatten)]
881 flags: Flags,
882 #[clap(subcommand)]
883 command: SubCommand,
884}
885
886#[derive(Parser, Debug)]
887enum SubCommand {
888 Init(InitArgs),
889 Link(LinkArgs),
890 Unlink(UnlinkArgs),
891 Install(InstallArgs),
892 Uninstall(UninstallArgs),
893 Mv(MvArgs),
894}
895
896fn main() -> anyhow::Result<()> {
897 let args = Args::parse();
898 match args.command {
899 SubCommand::Init(cmd_args) => command_init(args.flags, cmd_args),
900 SubCommand::Link(cmd_args) => command_link(args.flags, cmd_args),
901 SubCommand::Unlink(cmd_args) => command_unlink(args.flags, cmd_args),
902 SubCommand::Install(cmd_args) => command_install(args.flags, cmd_args),
903 SubCommand::Uninstall(cmd_args) => command_uninstall(args.flags, cmd_args),
904 SubCommand::Mv(cmd_args) => command_mv(args.flags, cmd_args),
905 }
906}
907
908#[derive(Parser, Debug)]
909struct InitArgs {
910 path: Option<PathBuf>,
911}
912
913fn command_init(_global_flags: Flags, args: InitArgs) -> anyhow::Result<()> {
914 let depot_path = {
915 let mut path = args.path.unwrap_or_else(utils::default_depot_path);
916 if path.is_dir() {
917 path = path.join(DEFAULT_DEPOT_FILE_NAME);
918 }
919 path
920 };
921
922 if depot_path.exists() {
923 println!("Depot at {} already exists", depot_path.display());
924 } else {
925 depot::write(&depot_path, &Default::default())?;
926 println!("Depot initialized at {}", depot_path.display());
927 }
928
929 Ok(())
930}
931
932#[derive(Parser, Debug)]
933struct LinkArgs {
934 origin: PathBuf,
935 destination: PathBuf,
936}
937
938fn command_link(global_flags: Flags, args: LinkArgs) -> anyhow::Result<()> {
939 let mut dotup = utils::read_dotup(&global_flags)?;
940 dotup.link(args.origin, args.destination);
941 utils::write_dotup(&dotup)?;
942 Ok(())
943}
944
945#[derive(Parser, Debug)]
946struct UnlinkArgs {
947 paths: Vec<PathBuf>,
948}
949
950fn command_unlink(global_flags: Flags, args: UnlinkArgs) -> anyhow::Result<()> {
951 let mut dotup = utils::read_dotup(&global_flags)?;
952 dotup.unlink(args.paths.into_iter());
953 utils::write_dotup(&dotup)?;
954 Ok(())
955}
956
957#[derive(Parser, Debug)]
958struct InstallArgs {
959 paths: Vec<PathBuf>,
960}
961
962fn command_install(global_flags: Flags, args: InstallArgs) -> anyhow::Result<()> {
963 let dotup = utils::read_dotup(&global_flags)?;
964 dotup.install(args.paths.into_iter());
965 utils::write_dotup(&dotup)?;
966 Ok(())
967}
968
969#[derive(Parser, Debug)]
970struct UninstallArgs {
971 paths: Vec<PathBuf>,
972}
973
974fn command_uninstall(global_flags: Flags, args: UninstallArgs) -> anyhow::Result<()> {
975 let dotup = utils::read_dotup(&global_flags)?;
976 dotup.uninstall(args.paths.into_iter());
977 utils::write_dotup(&dotup)?;
978 Ok(())
979}
980
981#[derive(Parser, Debug)]
982struct MvArgs {
983 paths: Vec<PathBuf>,
984}
985
986fn command_mv(global_flags: Flags, args: MvArgs) -> anyhow::Result<()> {
987 let mut dotup = utils::read_dotup(&global_flags)?;
988 let mut paths = args.paths;
989 if paths.len() < 2 {
990 return Err(anyhow::anyhow!("mv requires atleast 2 arguments"));
991 }
992 let to = paths.pop().unwrap();
993 let from = paths;
994 dotup.mv(from.iter(), &to);
995 utils::write_dotup(&dotup)?;
996 Ok(())
997}