diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 997 |
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)] | ||
| 2 | pub 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 | |||
| 573 | mod 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 | |||
| 791 | mod 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 | |||
| 864 | use std::path::PathBuf; | ||
| 865 | |||
| 866 | use clap::Parser; | ||
| 867 | use utils::DEFAULT_DEPOT_FILE_NAME; | ||
| 868 | |||
| 869 | #[derive(Parser, Debug)] | ||
| 870 | pub 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)] | ||
| 879 | struct Args { | ||
| 880 | #[clap(flatten)] | ||
| 881 | flags: Flags, | ||
| 882 | #[clap(subcommand)] | ||
| 883 | command: SubCommand, | ||
| 884 | } | ||
| 885 | |||
| 886 | #[derive(Parser, Debug)] | ||
| 887 | enum SubCommand { | ||
| 888 | Init(InitArgs), | ||
| 889 | Link(LinkArgs), | ||
| 890 | Unlink(UnlinkArgs), | ||
| 891 | Install(InstallArgs), | ||
| 892 | Uninstall(UninstallArgs), | ||
| 893 | Mv(MvArgs), | ||
| 894 | } | ||
| 895 | |||
| 896 | fn 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)] | ||
| 909 | struct InitArgs { | ||
| 910 | path: Option<PathBuf>, | ||
| 911 | } | ||
| 912 | |||
| 913 | fn 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)] | ||
| 933 | struct LinkArgs { | ||
| 934 | origin: PathBuf, | ||
| 935 | destination: PathBuf, | ||
| 936 | } | ||
| 937 | |||
| 938 | fn 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)] | ||
| 946 | struct UnlinkArgs { | ||
| 947 | paths: Vec<PathBuf>, | ||
| 948 | } | ||
| 949 | |||
| 950 | fn 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)] | ||
| 958 | struct InstallArgs { | ||
| 959 | paths: Vec<PathBuf>, | ||
| 960 | } | ||
| 961 | |||
| 962 | fn 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)] | ||
| 970 | struct UninstallArgs { | ||
| 971 | paths: Vec<PathBuf>, | ||
| 972 | } | ||
| 973 | |||
| 974 | fn 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)] | ||
| 982 | struct MvArgs { | ||
| 983 | paths: Vec<PathBuf>, | ||
| 984 | } | ||
| 985 | |||
| 986 | fn 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 | } | ||
