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
126
127
128
129
130
131
132
133
134
135
use crate::atoms::Outcome;

use super::super::Atom;
use super::FileAtom;
use std::path::PathBuf;
use tracing::{debug, error, warn};

pub struct Link {
    pub source: PathBuf,
    pub target: PathBuf,
}

impl FileAtom for Link {
    fn get_path(&self) -> &PathBuf {
        &self.source
    }
}

impl std::fmt::Display for Link {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "The file {} contents needs to be linked from {}",
            self.target.display(),
            self.source.display(),
        )
    }
}

impl Atom for Link {
    fn plan(&self) -> anyhow::Result<Outcome> {
        // First, ensure source exists and can be linked to
        if !self.source.exists() {
            error!(
                "Cannot plan: source file is missing: {}",
                self.source.display()
            );

            return Ok(Outcome {
                side_effects: vec![],
                should_run: false,
            });
        }

        // Target file doesn't exist, we can run safely
        if !self.target.exists() {
            return Ok(Outcome {
                side_effects: vec![],
                should_run: true,
            });
        }
        // Target file exists, lets check if it's a symlink which can be safely updated
        // or return a false and emit some logging that we can't create the link
        // without purging a file
        let link = match std::fs::read_link(&self.target) {
            Ok(link) => link,
            Err(err) => {
                warn!(
                    "Cannot plan: target already exists and isn't a link: {}",
                    self.target.display()
                );
                debug!("Cannot plan: {}", err);

                return Ok(Outcome {
                    side_effects: vec![],
                    should_run: false,
                });
            }
        };

        let source = if cfg!(target_os = "windows") {
            const PREFIX: &str = r"\\?\";
            PathBuf::from(&self.source.display().to_string().replace(PREFIX, ""))
        } else {
            self.source.to_owned()
        };

        // If this file doesn't link to what we expect, lets make it so
        Ok(Outcome {
            side_effects: vec![],
            should_run: !link.eq(&source),
        })
    }

    #[cfg(unix)]
    fn execute(&mut self) -> anyhow::Result<()> {
        std::os::unix::fs::symlink(&self.source, &self.target)?;

        Ok(())
    }

    #[cfg(windows)]
    fn execute(&mut self) -> anyhow::Result<()> {
        if self.target.is_dir() {
            std::os::windows::fs::symlink_dir(&self.source, &self.target)?;
        } else {
            std::os::windows::fs::symlink_file(&self.source, &self.target)?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn it_can() {
        let from_dir = match tempfile::tempdir() {
            Ok(dir) => dir,
            Err(_) => {
                assert_eq!(false, true);
                return;
            }
        };

        let to_file = match tempfile::NamedTempFile::new() {
            std::result::Result::Ok(file) => file,
            std::result::Result::Err(_) => {
                assert_eq!(false, true);
                return;
            }
        };

        let mut atom = Link {
            target: from_dir.path().join("symlink"),
            source: to_file.path().to_path_buf(),
        };
        assert_eq!(true, atom.plan().unwrap().should_run);
        assert_eq!(true, atom.execute().is_ok());
        assert_eq!(false, atom.plan().unwrap().should_run);
    }
}