aboutsummaryrefslogtreecommitdiff
path: root/src/dotup.rs
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2022-09-23 13:45:57 +0100
committerdiogo464 <[email protected]>2022-09-23 13:45:57 +0100
commite5a38bab67f790803ff98484fc5835adba7bf62a (patch)
treef93bc54b128db58c3e84c0db8908dda729222dc7 /src/dotup.rs
parente28be1bbe34e5430333d5bb95437e75bcfaf2edb (diff)
rewrite
Diffstat (limited to 'src/dotup.rs')
-rw-r--r--src/dotup.rs593
1 files changed, 0 insertions, 593 deletions
diff --git a/src/dotup.rs b/src/dotup.rs
deleted file mode 100644
index 8de7920..0000000
--- a/src/dotup.rs
+++ /dev/null
@@ -1,593 +0,0 @@
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_origin(origin.as_ref())?;
135 let destination_ends_with_slash = utils::path_ends_with_slash(destination.as_ref());
136 let mut destination = self.prepare_relative_destination(destination.as_ref())?;
137 if destination_ends_with_slash {
138 if let Some(filename) = origin.file_name() {
139 destination.push(filename);
140 }
141 }
142 self.depot.link_create(origin, destination)?;
143 };
144 match link_result {
145 Ok(_) => {}
146 Err(e) => println!("Failed to create link : {e}"),
147 }
148 }
149
150 pub fn unlink(&mut self, paths: impl Iterator<Item = impl AsRef<Path>>, uninstall: bool) {
151 for origin in paths {
152 let unlink_result: anyhow::Result<()> = try {
153 let origin = self.prepare_relative_origin(origin.as_ref())?;
154 let links_under: Vec<_> = self.depot.links_under(&origin)?.collect();
155 for link_id in links_under {
156 if uninstall && self.symlink_is_installed_by_link_id(link_id)? {
157 self.symlink_uninstall_by_link_id(link_id)?;
158 }
159 self.depot.link_remove(link_id);
160 }
161 };
162 match unlink_result {
163 Ok(_) => {}
164 Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()),
165 }
166 }
167 }
168
169 pub fn install(&self, paths: impl Iterator<Item = impl AsRef<Path>>) {
170 let install_result: anyhow::Result<()> = try {
171 let link_ids = self.link_ids_from_paths_iter(paths)?;
172 self.depot.links_verify_install(link_ids.iter().copied())?;
173
174 for link_id in link_ids {
175 self.symlink_install_by_link_id(link_id)?;
176 }
177 };
178 if let Err(e) = install_result {
179 println!("error while installing : {e}");
180 }
181 }
182
183 pub fn uninstall(&self, paths: impl Iterator<Item = impl AsRef<Path>>) {
184 let uninstall_result: anyhow::Result<()> = try {
185 let link_ids = self.link_ids_from_paths_iter(paths)?;
186 for link_id in link_ids {
187 if self.symlink_is_installed_by_link_id(link_id)? {
188 self.symlink_uninstall_by_link_id(link_id)?;
189 }
190 }
191 };
192 if let Err(e) = uninstall_result {
193 println!("error while uninstalling {e}",);
194 }
195 }
196
197 pub fn mv(
198 &mut self,
199 origins: impl Iterator<Item = impl AsRef<Path>>,
200 destination: impl AsRef<Path>,
201 ) {
202 let mv_result: anyhow::Result<()> = try {
203 let origins = {
204 let mut v = Vec::new();
205 for origin in origins {
206 v.push(
207 origin
208 .as_ref()
209 .canonicalize()
210 .context("failed to canonicalize origin path")?,
211 );
212 }
213 v
214 };
215 let destination = utils::weakly_canonical(destination.as_ref());
216 log::debug!("mv destination : {}", destination.display());
217
218 // if we are moving multiple links then the destination must be a directory
219 if origins.len() > 1 && !destination.is_dir() {
220 println!("destination must be a directory");
221 return;
222 }
223
224 for origin in origins {
225 let destination = if destination.is_dir() {
226 // unwrap: origin must have a filename
227 destination.join(origin.file_name().unwrap())
228 } else {
229 destination.to_owned()
230 };
231 self.mv_one(&origin, &destination)?;
232 }
233 };
234 if let Err(e) = mv_result {
235 println!("error moving : {e}");
236 }
237 }
238
239 fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> {
240 log::debug!("mv_one : {} to {}", origin.display(), destination.display());
241
242 let relative_origin = self.prepare_relative_origin(origin)?;
243 let relative_destination = self.prepare_relative_origin(destination)?;
244 match self.depot.link_find(&relative_origin)? {
245 Some(link_id) => {
246 let is_installed = self.symlink_is_installed_by_link_id(link_id)?;
247 let original_origin = self.depot.link_view(link_id).origin().to_owned();
248 log::debug!("is_installed = {is_installed}",);
249 log::debug!("original_origin = {}", original_origin.display());
250 log::debug!("link_destination = {}", relative_destination.display());
251
252 self.depot.link_move(link_id, relative_destination)?;
253 if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file")
254 {
255 // unwrap: moving the link back to its origin place has to work
256 self.depot.link_move(link_id, original_origin).unwrap();
257 return Err(e);
258 }
259 // reinstall because we just moved the origin
260 if is_installed {
261 self.symlink_install_by_link_id(link_id)
262 .context("failed to reinstall link while moving")?;
263 }
264 }
265 None => {
266 if origin.is_dir() {
267 let mut links_installed: HashSet<_> = Default::default();
268 if self.depot.has_links_under(&relative_origin)? {
269 let links_under: Vec<_> =
270 self.depot.links_under(&relative_origin)?.collect();
271 for &link_id in links_under.iter() {
272 let link_view = self.depot.link_view(link_id);
273 if self.symlink_is_installed_by_link_id(link_id)? {
274 links_installed.insert(link_id);
275 }
276 // unwrap: the link is under `origin` so stripping the prefix should
277 // not fail
278 let origin_extra =
279 link_view.origin().strip_prefix(&relative_origin).unwrap();
280 let new_destination = relative_destination.join(origin_extra);
281 self.depot.link_move(link_id, new_destination)?;
282 }
283 }
284 std::fs::rename(origin, destination)?;
285 for link_id in links_installed {
286 self.symlink_install_by_link_id(link_id)?;
287 }
288 } else {
289 std::fs::rename(origin, destination)?;
290 }
291 }
292 }
293 Ok(())
294 }
295
296 pub fn status(&self, paths: impl Iterator<Item = impl AsRef<Path>>) {
297 let status_result: anyhow::Result<()> = try {
298 // canonicalize and remove paths whose parent we already have
299 let paths = paths.map(utils::weakly_canonical).collect::<HashSet<_>>();
300 let paths = paths
301 .iter()
302 .filter(|p| !paths.iter().any(|x| p.starts_with(x) && p != &x));
303
304 for path in paths {
305 let item = self.status_path_to_item(path)?;
306 self.status_print_item(item, 0)?;
307 }
308 };
309 if let Err(e) = status_result {
310 println!("error while displaying status : {e}");
311 }
312 }
313 fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result<StatusItem> {
314 debug_assert!(canonical_path.is_absolute());
315 debug_assert!(canonical_path.exists());
316 let relative_path = self.prepare_relative_origin(canonical_path)?;
317
318 let item = if canonical_path.is_dir() {
319 if let Some(link_id) = self.depot.link_find(&relative_path)? {
320 let destination = self.depot.link_view(link_id).destination().to_owned();
321 StatusItem::Link {
322 origin: relative_path,
323 destination,
324 is_directory: true,
325 }
326 } else if self.depot.has_links_under(&relative_path)? {
327 let mut items = Vec::new();
328 let mut collected_rel_paths = HashSet::<PathBuf>::new();
329 let directory_paths = utils::collect_paths_in_dir(&canonical_path)?;
330 for canonical_item_path in directory_paths {
331 let item = self.status_path_to_item(&canonical_item_path)?;
332 match &item {
333 StatusItem::Link { origin, .. } | StatusItem::Directory { origin, .. } => {
334 collected_rel_paths.insert(origin.to_owned());
335 }
336 _ => {}
337 }
338 items.push(item);
339 }
340
341 for dir_node in self.depot.read_dir(&relative_path)? {
342 match dir_node {
343 DirNode::Link(link_id) => {
344 let link_view = self.depot.link_view(link_id);
345 let link_rel_path = link_view.origin();
346 let link_rel_dest = link_view.destination();
347 if !collected_rel_paths.contains(link_rel_path) {
348 items.push(StatusItem::Link {
349 origin: link_rel_path.to_owned(),
350 destination: link_rel_dest.to_owned(),
351 is_directory: false,
352 });
353 }
354 }
355 DirNode::Directory(_) => {}
356 }
357 }
358
359 StatusItem::Directory {
360 origin: relative_path,
361 items,
362 }
363 } else {
364 StatusItem::Unlinked {
365 origin: relative_path,
366 is_directory: true,
367 }
368 }
369 } else if let Some(link_id) = self.depot.link_find(&relative_path)? {
370 let destination = self.depot.link_view(link_id).destination().to_owned();
371 StatusItem::Link {
372 origin: relative_path,
373 destination,
374 is_directory: false,
375 }
376 } else {
377 StatusItem::Unlinked {
378 origin: relative_path,
379 is_directory: false,
380 }
381 };
382 Ok(item)
383 }
384 fn status_print_item(&self, item: StatusItem, depth: u32) -> anyhow::Result<()> {
385 fn print_depth(d: u32) {
386 for _ in 0..d.saturating_sub(1) {
387 print!(" ");
388 }
389 }
390 fn origin_color(exists: bool, is_installed: bool) -> Color {
391 if !exists {
392 Color::Red
393 } else if is_installed {
394 Color::Green
395 } else {
396 Color::RGB(255, 127, 0)
397 }
398 }
399
400 let destination_color = Color::Blue;
401
402 print_depth(depth);
403 match item {
404 StatusItem::Link {
405 origin,
406 destination,
407 is_directory,
408 } => {
409 let canonical_origin = self.depot_dir.join(&origin);
410 let canonical_destination = self.install_base.join(&destination);
411 let file_name = Self::status_get_filename(&canonical_origin);
412 let is_installed =
413 self.symlink_is_installed(&canonical_origin, &canonical_destination)?;
414 let exists = canonical_origin.exists();
415 let origin_color = origin_color(exists, is_installed);
416 let directory_extra = if is_directory { "/" } else { "" };
417 println!(
418 "{}{} -> {}",
419 origin_color.paint(file_name),
420 directory_extra,
421 destination_color.paint(destination.display().to_string())
422 );
423 }
424 StatusItem::Directory { origin, mut items } => {
425 items.sort_by(|a, b| StatusItem::display_ord_cmp(a, b).reverse());
426 let directory_name = Self::status_get_filename(&origin);
427 if depth != 0 {
428 println!("{}/", directory_name);
429 }
430 for item in items {
431 self.status_print_item(item, depth + 1)?;
432 }
433 }
434 StatusItem::Unlinked {
435 origin,
436 is_directory,
437 } => {
438 let file_name = Self::status_get_filename(&origin);
439 let directory_extra = if is_directory { "/" } else { "" };
440 println!("{}{}", file_name, directory_extra);
441 }
442 }
443 Ok(())
444 }
445 fn status_get_filename(path: &Path) -> &str {
446 path.file_name()
447 .and_then(|s| s.to_str())
448 .unwrap_or_default()
449 }
450
451 fn prepare_relative_path(path: &Path, base: &Path) -> anyhow::Result<PathBuf> {
452 let canonical = utils::weakly_canonical(path);
453 let relative = canonical
454 .strip_prefix(base)
455 .context("Invalid origin path, not under depot directory")?;
456 Ok(relative.to_owned())
457 }
458
459 fn prepare_relative_origin(&self, path: &Path) -> anyhow::Result<PathBuf> {
460 Self::prepare_relative_path(path, &self.depot_dir)
461 }
462
463 fn prepare_relative_destination(&self, path: &Path) -> anyhow::Result<PathBuf> {
464 Self::prepare_relative_path(path, &self.install_base)
465 }
466
467 fn link_ids_from_paths_iter(
468 &self,
469 paths: impl Iterator<Item = impl AsRef<Path>>,
470 ) -> anyhow::Result<Vec<LinkID>> {
471 let mut link_ids = HashSet::<LinkID>::default();
472 for path in paths {
473 let path = self.prepare_relative_origin(path.as_ref())?;
474 link_ids.extend(self.depot.links_under(&path)?);
475 }
476 Ok(Vec::from_iter(link_ids.into_iter()))
477 }
478
479 fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result<bool> {
480 let canonical_pair = self.canonical_pair_from_link_id(link_id);
481 self.symlink_is_installed(&canonical_pair.origin, &canonical_pair.destination)
482 }
483
484 fn symlink_is_installed(&self, origin: &Path, destination: &Path) -> anyhow::Result<bool> {
485 debug_assert!(origin.is_absolute());
486 debug_assert!(destination.is_absolute());
487
488 if destination.is_symlink() {
489 let symlink_destination = destination.read_link()?;
490 match symlink_destination.canonicalize() {
491 Ok(canonicalized) => Ok(origin == canonicalized),
492 Err(_) => Ok(false),
493 }
494 } else {
495 Ok(false)
496 }
497 }
498
499 fn symlink_install_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> {
500 let canonical_pair = self.canonical_pair_from_link_id(link_id);
501 self.symlink_install(&canonical_pair.origin, &canonical_pair.destination)
502 }
503
504 fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> {
505 debug_assert!(origin.is_absolute());
506 debug_assert!(destination.is_absolute());
507 log::debug!(
508 "symlink_install : {} -> {}",
509 origin.display(),
510 destination.display()
511 );
512
513 let destination_parent = destination
514 .parent()
515 .ok_or_else(|| anyhow::anyhow!("destination has no parent component"))?;
516 std::fs::create_dir_all(destination_parent).context("Failed to create directories")?;
517 // need to do this beacause if the destination path ends in '/' because the symlink
518 // functions will treat it as a directory but we want a file with that name.
519 let destination = destination.with_file_name(destination.file_name().unwrap());
520
521 let destination_exists = destination.exists();
522 let destination_is_symlink = destination.is_symlink();
523
524 if destination_exists && !destination_is_symlink {
525 return Err(anyhow::anyhow!("destination already exists"));
526 }
527
528 if destination_is_symlink {
529 log::debug!("symlink already exists, removing before recreating");
530 std::fs::remove_file(&destination)?;
531 }
532
533 log::debug!(
534 "creating filesystem symlink {} -> {}",
535 origin.display(),
536 destination.display()
537 );
538 std::os::unix::fs::symlink(origin, destination).context("failed to create symlink")?;
539
540 Ok(())
541 }
542
543 fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> {
544 debug_assert!(origin.is_absolute());
545 debug_assert!(destination.is_absolute());
546 let destination = destination.with_file_name(destination.file_name().unwrap());
547
548 if destination.is_symlink() {
549 let symlink_destination = destination.read_link()?.canonicalize()?;
550 if symlink_destination == origin {
551 std::fs::remove_file(&destination)?;
552 }
553 }
554
555 Ok(())
556 }
557
558 fn symlink_uninstall_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> {
559 let canonical_pair = self.canonical_pair_from_link_id(link_id);
560 self.symlink_uninstall(&canonical_pair.origin, &canonical_pair.destination)
561 }
562
563 fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair {
564 let link_view = self.depot.link_view(link_id);
565 let relative_origin = link_view.origin();
566 let relative_destination = link_view.destination();
567 let canonical_origin = self.depot_dir.join(relative_origin);
568 let canonical_destination = self.install_base.join(relative_destination);
569 CanonicalPair {
570 origin: canonical_origin,
571 destination: canonical_destination,
572 }
573 }
574}
575
576pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result<Dotup> {
577 let depot_path = depot_path
578 .canonicalize()
579 .context("Failed to canonicalize depot path")?;
580 let install_base = install_base
581 .canonicalize()
582 .context("Failed to canonicalize install base")?;
583 if !install_base.is_dir() {
584 return Err(anyhow::anyhow!("Install base must be a directory"));
585 }
586 let depot = depot::read(&depot_path)?;
587 Dotup::new(depot, depot_path, install_base)
588}
589
590pub fn write(dotup: &Dotup) -> anyhow::Result<()> {
591 depot::write(&dotup.depot_path, &dotup.depot)?;
592 Ok(())
593}