comtrya_lib/atoms/file/
link.rs

1use crate::atoms::Outcome;
2
3use super::super::Atom;
4use super::FileAtom;
5use std::path::PathBuf;
6use tracing::{error, warn};
7
8pub struct Link {
9    pub source: PathBuf,
10    pub target: PathBuf,
11}
12
13impl FileAtom for Link {
14    fn get_path(&self) -> &PathBuf {
15        &self.source
16    }
17}
18
19impl std::fmt::Display for Link {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(
22            f,
23            "The file {} contents needs to be linked from {}",
24            self.target.display(),
25            self.source.display(),
26        )
27    }
28}
29
30impl Atom for Link {
31    fn plan(&self) -> anyhow::Result<Outcome> {
32        // First, ensure source exists and can be linked to
33        if !self.source.exists() {
34            error!(
35                "Cannot plan: source file is missing: {}",
36                self.source.display()
37            );
38
39            return Ok(Outcome {
40                side_effects: vec![],
41                should_run: false,
42            });
43        }
44
45        // Target file doesn't exist, we can run safely
46        if !self.target.exists() {
47            return Ok(Outcome {
48                side_effects: vec![],
49                should_run: true,
50            });
51        }
52        // Target file exists, lets check if it's a symlink which can be safely updated
53        // or return a false and emit some logging that we can't create the link
54        // without purging a file
55        let link = match std::fs::read_link(&self.target) {
56            Ok(link) => link,
57            Err(err) => {
58                warn!(
59                    "Cannot plan: target already exists and isn't a link: {}",
60                    self.target.display()
61                );
62                error!("Cannot plan: {}", err);
63
64                return Ok(Outcome {
65                    side_effects: vec![],
66                    should_run: false,
67                });
68            }
69        };
70
71        let source = if cfg!(target_os = "windows") {
72            const PREFIX: &str = r"\\?\";
73            PathBuf::from(&self.source.display().to_string().replace(PREFIX, ""))
74        } else {
75            self.source.to_owned()
76        };
77
78        // If this file doesn't link to what we expect, lets make it so
79        Ok(Outcome {
80            side_effects: vec![],
81            should_run: !link.eq(&source),
82        })
83    }
84
85    #[cfg(unix)]
86    fn execute(&mut self) -> anyhow::Result<()> {
87        std::os::unix::fs::symlink(&self.source, &self.target)?;
88
89        Ok(())
90    }
91
92    #[cfg(windows)]
93    fn execute(&mut self) -> anyhow::Result<()> {
94        if self.target.is_dir() {
95            std::os::windows::fs::symlink_dir(&self.source, &self.target)?;
96        } else {
97            std::os::windows::fs::symlink_file(&self.source, &self.target)?;
98        }
99
100        Ok(())
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use pretty_assertions::assert_eq;
108
109    #[test]
110    fn it_can() {
111        let from_dir = match tempfile::tempdir() {
112            Ok(dir) => dir,
113            Err(_) => {
114                assert_eq!(false, true);
115                return;
116            }
117        };
118
119        let to_file = match tempfile::NamedTempFile::new() {
120            std::result::Result::Ok(file) => file,
121            std::result::Result::Err(_) => {
122                assert_eq!(false, true);
123                return;
124            }
125        };
126
127        let mut atom = Link {
128            target: from_dir.path().join("symlink"),
129            source: to_file.path().to_path_buf(),
130        };
131        assert_eq!(true, atom.plan().unwrap().should_run);
132        assert_eq!(true, atom.execute().is_ok());
133        assert_eq!(false, atom.plan().unwrap().should_run);
134    }
135}