aboutsummaryrefslogtreecommitdiff
path: root/src/commands/sync.rs
blob: eaf3dabdecc4c91c5164910bb4a7dcd00cf11683 (plain)
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
125
use std::{
    io::{self,ErrorKind,Write},
    fs,
    path::PathBuf,
};
use copy_dir::copy_dir;

use crate::{resolve_home,Config,Entry};

#[derive(Debug, PartialEq, Eq)]
enum ConflictOption {
    Remove,
    Rename,
    Skip,
}

fn remove_ambiguous(path: &PathBuf) -> io::Result<()> {
    if path.is_dir() {
        fs::remove_dir_all(&path)?;
    } else {
        fs::remove_file(&path)?;
    }
    
    Ok(())
}

fn rename_conflict(path: &PathBuf) -> io::Result<()> {
    let rename_path = path.with_extension("old");

    if rename_path.try_exists()? {
        remove_ambiguous(&rename_path)?;
    }

    fs::rename(&path, &rename_path)?;
    println!("Old config renamed to {}.", rename_path.file_name().unwrap().to_str().unwrap());

    Ok(())
}

fn remove_existing(name: &str, path: &PathBuf) -> io::Result<Option<ConflictOption>> {
    loop {
        let mut input = String::new();

        print!("Remove existing config for {name}? (y/n/r[ename]) ");
        let _ = io::stdout().flush();
        io::stdin()
            .read_line(&mut input)
            .expect("Failed to read user input.");

        let choice = match input.to_lowercase().trim() {
            "y" => ConflictOption::Remove,
            "n" => ConflictOption::Skip,
            "r" => ConflictOption::Rename,
            _ => {
                println!("Try again :)");
                continue;
            },
        };

        match choice {
            ConflictOption::Remove =>
                remove_ambiguous(path)?,
            ConflictOption::Rename =>
                rename_conflict(path)?,
            ConflictOption::Skip =>
                println!("Skipping {name}."),
        };

        return Ok(Some(choice));
    }
}

fn sync_file(
    name: &str,
    src: &PathBuf,
    dest: &PathBuf,
    force: &bool
) -> io::Result<Option<ConflictOption>> {

    let copy_result = copy_dir(src, dest);

    if let Err(error) = copy_result {
        if error.kind() == ErrorKind::AlreadyExists {
            if *force {
                remove_ambiguous(dest)?;
                return Ok(Some(ConflictOption::Remove));
            } else {
                return remove_existing(name, dest);
            }
        }
        return Err(error);
    }

    Ok(None)    
}

pub fn sync_config(name: &str, entry: &Entry, configs_path: &PathBuf, force: &bool) {
    let src = configs_path.join(&entry.file);
    let mut dest = entry.parent.join(&entry.file);
    resolve_home(&mut dest);

    loop {
        let sync_result = sync_file(name, &src, &dest, force);

        match sync_result {
            Ok(None) => println!("Updated {}.", dest.to_str().unwrap()),
            Ok(Some(choice)) => match choice {
                ConflictOption::Skip => break,
                _ => continue,
            },
            Err(error) => {
                eprintln!("Skipping sync of {src:?} to {dest:?}: {error:?}");
                break;
            },
        };

        break;
    }
}

pub fn sync_all(config: &Config, configs_path: &PathBuf, force: bool) {
    for (name, entry) in config {
        sync_config(&name, &entry, configs_path, &force);
    }
}