aboutsummaryrefslogtreecommitdiff
path: root/src/dotup.rs
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 /src/dotup.rs
parent52552b1b12ba8632a26a56a926b6767370901b56 (diff)
split code into different files
Diffstat (limited to 'src/dotup.rs')
-rw-r--r--src/dotup.rs533
1 files changed, 533 insertions, 0 deletions
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}