1#![cfg(unix)]
23use crate::link::directive::*;
27use derivative::Derivative;
28use dotfiles_core::action::Action;
29use dotfiles_core::action::SKIP_IN_CI_SETTING;
30use dotfiles_core::error::DotfilesError;
31use dotfiles_core::error::ErrorType;
32use dotfiles_core::path::convert_path_to_absolute;
33use dotfiles_core::path::process_home_dir_in_path;
34use dotfiles_core::settings::Settings;
35use dotfiles_core::yaml_util::get_boolean_setting_from_context;
36use dotfiles_core_macros::ConditionalAction;
37use filesystem::FakeFileSystem;
38use filesystem::FileSystem;
39use filesystem::OsFileSystem;
40use filesystem::UnixFileSystem;
41use getset::CopyGetters;
42use getset::Getters;
43use log::error;
44use log::info;
45use std::format;
46use std::io;
47use std::io::ErrorKind;
48use std::path::Path;
49use std::path::PathBuf;
50
51#[derive(Derivative, Getters, CopyGetters, ConditionalAction)]
55#[derivative(Debug, PartialEq)]
56pub struct LinkAction<'a, F: FileSystem + UnixFileSystem> {
57  skip_in_ci: bool,
59  #[derivative(Debug = "ignore", PartialEq = "ignore")]
64  fs: &'a F,
65  #[getset(get = "pub")]
67  path: String,
68  #[getset(get = "pub")]
70  target: String,
71  #[getset(get_copy = "pub")]
73  relink: bool,
74  #[getset(get_copy = "pub")]
76  force: bool,
77  #[getset(get_copy = "pub")]
79  create_parent_dirs: bool,
80  #[getset(get_copy = "pub")]
82  ignore_missing_target: bool,
83  #[getset(get_copy = "pub")]
86  resolve_symlink_target: bool,
87  current_dir: PathBuf,
90}
91
92pub type NativeLinkAction<'a> = LinkAction<'a, OsFileSystem>;
94pub type FakeLinkAction<'a> = LinkAction<'a, FakeFileSystem>;
96
97impl<'a, F: FileSystem + UnixFileSystem> LinkAction<'a, F> {
98  #[allow(clippy::too_many_arguments)]
100  pub fn new(
101    fs: &'a F,
102    path: String,
103    target: String,
104    context_settings: &'_ Settings,
105    defaults: &'_ Settings,
106    current_dir: PathBuf,
107  ) -> Result<Self, DotfilesError> {
108    let relink =
109      get_boolean_setting_from_context(RELINK_SETTING, context_settings, defaults).unwrap();
110    let force =
111      get_boolean_setting_from_context(FORCE_SETTING, context_settings, defaults).unwrap();
112    let create_parent_dirs =
113      get_boolean_setting_from_context(CREATE_PARENT_DIRS_SETTING, context_settings, defaults)
114        .unwrap();
115    let ignore_missing_target =
116      get_boolean_setting_from_context(IGNORE_MISSING_TARGET_SETTING, context_settings, defaults)
117        .unwrap();
118    let resolve_symlink_target =
119      get_boolean_setting_from_context(RESOLVE_SYMLINK_TARGET_SETTING, context_settings, defaults)
120        .unwrap();
121    let skip_in_ci =
122      get_boolean_setting_from_context(SKIP_IN_CI_SETTING, context_settings, defaults).unwrap();
123    let action = Self {
124      skip_in_ci,
125      fs,
126      path,
127      target,
128      relink,
129      force,
130      create_parent_dirs,
131      ignore_missing_target,
132      resolve_symlink_target,
133      current_dir,
134    };
135    log::trace!("Creating new {:?}", action);
136    Ok(action)
137  }
138}
139
140impl<F: FileSystem + UnixFileSystem> Action<'_> for LinkAction<'_, F> {
141  fn execute(&self) -> Result<(), DotfilesError> {
142    fn create_symlink<F: FileSystem + UnixFileSystem>(
143      fs: &'_ F,
144      action: &'_ LinkAction<F>,
145      path: PathBuf,
146      mut target: PathBuf,
147    ) -> io::Result<()> {
148      let target_exists = fs.is_dir(&target) || fs.is_file(&target);
149      let path_exists = fs.is_dir(&path) || fs.is_file(&path);
150      let path_is_symlink = fs.get_symlink_src(&path).is_ok();
151      let target_is_symlink = fs.get_symlink_src(&target).is_ok();
152      if target_is_symlink && action.resolve_symlink_target() {
153        fn resolve_symlink_target<F: FileSystem + UnixFileSystem, P: AsRef<Path>>(
154          fs: &'_ F,
155          link_path: P,
156        ) -> io::Result<PathBuf> {
157          match fs.get_symlink_src(link_path.as_ref()) {
158            Ok(link_target) => resolve_symlink_target(fs, link_target),
159            Err(e) if [ErrorKind::IsADirectory, ErrorKind::InvalidInput].contains(&e.kind()) => {
160              Ok(PathBuf::from(link_path.as_ref()))
161            }
162            Err(e) => Err(e),
163          }
164        }
165        target = resolve_symlink_target(fs, &target)?
166      }
167      if target_exists || action.ignore_missing_target() {
168        if !fs.is_dir(path.parent().unwrap()) && action.create_parent_dirs() {
169          fs.create_dir_all(path.parent().unwrap())?
170        }
171        match (path_exists, action.force(), fs.is_dir(&path), path_is_symlink, action.relink()) {
172                        (true, true, true, _, _ ) =>fs.remove_dir_all(&path)?, (true, true, false, _, _ ) =>fs.remove_file(&path)?, (true, false, _, true, true ) =>fs.remove_file(&path)?, (true, false, _, true, false) =>
176                            return Err(io::Error::new(
178                                ErrorKind::AlreadyExists,
179                                format!("{:?} already exists. Use `force` to delete a file/directory or `relink` to recreate a symlink", &path))),
180                        _ => ()
181                    }
182        fs.symlink(&target, &path)
183      } else {
184        Err(io::Error::new(
185          ErrorKind::NotFound,
186          format!(
187            "Couldn't find target file {target:?} to link to, use `ignore_missing_target` to ignore",
188          ),
189        ))
190      }
191    }
192    let path: PathBuf = PathBuf::from(self.path());
193    let mut path = process_home_dir_in_path(&path);
194    path = convert_path_to_absolute(&path, Some(&self.current_dir))?;
195    let target = PathBuf::from(self.target());
196    let mut target = process_home_dir_in_path(&target);
197    target = convert_path_to_absolute(&target, Some(&self.current_dir))?;
198    match create_symlink(self.fs, self, path, target) {
199      Ok(()) => {
200        info!("Created symlink {} -> {}", &self.path, &self.target);
201        Ok(())
202      }
203      Err(err) => {
204        error!(
205          "Couldn't create symlink {} -> {}: {}",
206          &self.path, &self.target, err
207        );
208        Err(DotfilesError::from(
209          err.to_string(),
210          ErrorType::FileSystemError { fs_error: err },
211        ))
212      }
213    }
214  }
215}