lib/linker/
mod.rs

1use crate::{
2    common::{util, AbsolutePath, FormattedItem, FormattedItems},
3    verbose_println,
4};
5use derive_more::From;
6use failure::Fail;
7use std::{
8    fs,
9    io::{self, Write},
10    path::Path,
11};
12
13enum YN {
14    Yes,
15    No,
16}
17use YN::*;
18
19/// Prompts the user with `prompt` and asks for a yes/no answer.
20/// Will continue asking until input resembling yes/no is given.
21fn read_yes_or_no(prompt: &str) -> io::Result<YN> {
22    let mut buf = String::new();
23    loop {
24        print!("{} (y/n) ", prompt);
25        io::stdout().flush()?;
26
27        io::stdin().read_line(&mut buf)?;
28        buf = buf.trim().to_lowercase();
29
30        if buf.is_empty() {
31            continue;
32        }
33
34        if buf.starts_with("yes") || "yes".starts_with(&buf) {
35            return Ok(Yes);
36        } else if buf.starts_with("no") || "no".starts_with(&buf) {
37            return Ok(No);
38        } else {
39            buf.clear();
40            continue;
41        }
42    }
43}
44
45#[cfg(unix)]
46fn symlink(source: impl AsRef<Path>, dest: impl AsRef<Path>) -> io::Result<()> {
47    std::os::unix::fs::symlink(source, dest)
48}
49
50fn link_item(item: &FormattedItem, dry_run: bool) -> Result<(), Error> {
51    let (source, dest) = (item.source(), item.dest());
52
53    // Performs the actual linking after all validation
54    // is finished.
55    let link = |item: &FormattedItem| -> Result<(), Error> {
56        verbose_println!("Linking {}", item);
57
58        if !dry_run {
59            fs::create_dir_all(dest.parent().unwrap_or(dest))?;
60            symlink(source, dest)?;
61        }
62
63        Ok(())
64    };
65
66    if !dest.exists() {
67        link(item)?
68    } else {
69        match fs::read_link(dest) {
70            // If the file at `dest` is already a link to `source`, ignore it.
71            Ok(ref target) if target.as_path() == source.as_path() => {
72                verbose_println!("Skipping identical {}", dest)
73            },
74            // If the file at `dest` is anything else, ask if it should be overwritten
75            _ => {
76                let prompt = format!("Overwrite {}?", dest);
77                match read_yes_or_no(&prompt)? {
78                    No => println!("Skipping {}", dest),
79                    Yes => {
80                        match util::file_type(dest)? {
81                            util::FileType::File | util::FileType::Symlink => {
82                                fs::remove_file(dest)?
83                            },
84                            // To be careful, we don't want to overwrite directories. Especially
85                            // since dotman currently only links files and not whole directories.
86                            // To make sure the user _absolutely_ wants to overwrite a directory
87                            // with a file symlink, we ask them to delete the directory manually
88                            // before running dotman.
89                            util::FileType::Directory => {
90                                return Err(DirectoryOverwrite(dest.clone()))
91                            },
92                        };
93                        link(item)?;
94                    },
95                }
96            },
97        }
98    }
99
100    Ok(())
101}
102
103pub fn link_items(items: FormattedItems, dry_run: bool) -> Result<(), Error> {
104    for item in &items {
105        link_item(item, dry_run)?;
106    }
107
108    Ok(())
109}
110
111#[derive(Debug, From, Fail)]
112pub enum Error {
113    #[fail(display = "error creating symlinks ({})", _0)]
114    IoError(#[fail(cause)] io::Error),
115
116    #[fail(
117        display = "won't delete directory {}. Please remove it manually if you want.",
118        _0
119    )]
120    DirectoryOverwrite(AbsolutePath),
121}
122use Error::*;