1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
use crate::{
    common::{util, AbsolutePath, FormattedItem, FormattedItems},
    verbose_println,
};
use derive_more::From;
use failure::Fail;
use std::{
    fs,
    io::{self, Write},
    path::Path,
};

enum YN {
    Yes,
    No,
}
use self::YN::*;

/// Prompts the user with `prompt` and asks for a yes/no answer.
/// Will continue asking until input resembling yes/no is given.
fn read_yes_or_no(prompt: &str) -> io::Result<YN> {
    let mut buf = String::new();
    loop {
        print!("{} (y/n) ", prompt);
        io::stdout().flush()?;

        io::stdin().read_line(&mut buf)?;
        buf = buf.trim().to_lowercase();

        if buf.is_empty() {
            continue;
        }

        if buf.starts_with("yes") || "yes".starts_with(&buf) {
            return Ok(Yes);
        } else if buf.starts_with("no") || "no".starts_with(&buf) {
            return Ok(No);
        } else {
            buf.clear();
            continue;
        }
    }
}

#[cfg(unix)]
fn symlink(source: impl AsRef<Path>, dest: impl AsRef<Path>) -> io::Result<()> {
    std::os::unix::fs::symlink(source, dest)
}

fn link_item(item: &FormattedItem, dry_run: bool) -> Result<(), Error> {
    let (source, dest) = (item.source(), item.dest());

    // Performs the actual linking after all validation
    // is finished.
    let link = |item: &FormattedItem| -> Result<(), Error> {
        verbose_println!("Linking {}", item);

        if dry_run {
            return Ok(());
        }

        fs::create_dir_all(dest.parent().unwrap_or(dest))?;
        symlink(source, dest)?;

        Ok(())
    };

    if !dest.exists() {
        link(item)?
    } else {
        match fs::read_link(dest) {
            // If the file at `dest` is already a link to source, ignore it.
            Ok(ref target) if target.as_path() == source.as_path() => {
                verbose_println!("Skipping identical {}", dest)
            },
            // If the file at `dest` is anything else, ask if it should be overwritten
            _ => {
                let prompt = format!("Overwrite {}?", dest);
                match read_yes_or_no(&prompt)? {
                    No => println!("Skipping {}", dest),
                    Yes => {
                        match util::file_type(dest)? {
                            util::FileType::File | util::FileType::Symlink => {
                                fs::remove_file(dest)?
                            },
                            // To be careful, we don't want to overwrite directories. Especially
                            // since dotman currently only links files and not whole directories.
                            // To make sure the user _absolutely_ wants to overwrite a directory
                            // with a file symlink, we ask them to delete the directory manually
                            // before running dotman.
                            util::FileType::Directory => {
                                return Err(DirectoryOverwrite(dest.clone()))
                            },
                        };
                        link(item)?;
                    },
                }
            },
        }
    }

    Ok(())
}

pub fn link_items(items: FormattedItems, dry_run: bool) -> Result<(), Error> {
    for item in &items {
        link_item(item, dry_run)?;
    }

    Ok(())
}

#[derive(Debug, From, Fail)]
pub enum Error {
    #[fail(display = "error creating symlinks ({})", _0)]
    IoError(#[fail(cause)] io::Error),

    #[fail(
        display = "won't delete directory {}. Please remove it manually if you want.",
        _0
    )]
    DirectoryOverwrite(AbsolutePath),
}
use self::Error::*;