dotfiles_actions/link/
action.rs

1// Copyright (c) 2021-2022 Miguel Barreto and others
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22#![cfg(unix)]
23//! This module contains the [LinkAction] that creates a new symlink
24//! when executed
25
26use 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/// [LinkAction] creates a new symlink `path` that points to `target`.
52///
53/// It is equivalent to running `ln -s <target> <path>`
54#[derive(Derivative, Getters, CopyGetters, ConditionalAction)]
55#[derivative(Debug, PartialEq)]
56pub struct LinkAction<'a, F: FileSystem + UnixFileSystem> {
57  /// Skips this action if it is running in a CI environment.
58  skip_in_ci: bool,
59  /// FileSystem to use to create the directory.
60  ///
61  /// Having a filesystem instance here allows us to use fakes/mocks to use
62  /// in unit tests.
63  #[derivative(Debug = "ignore", PartialEq = "ignore")]
64  fs: &'a F,
65  /// Path of the new symlink
66  #[getset(get = "pub")]
67  path: String,
68  /// Path that the symlink points to.
69  #[getset(get = "pub")]
70  target: String,
71  /// Force to re-create the symlink if it exists already
72  #[getset(get_copy = "pub")]
73  relink: bool,
74  /// Force to replace an existing file or directory when executed.
75  #[getset(get_copy = "pub")]
76  force: bool,
77  /// Create all parent directories if they do not exist already
78  #[getset(get_copy = "pub")]
79  create_parent_dirs: bool,
80  /// Succeed even if `target` doesn't point to an existing file or dir.
81  #[getset(get_copy = "pub")]
82  ignore_missing_target: bool,
83  /// If the target is another symlink, resolve the ultimate concrete file
84  /// or directory that it points to and make it the target
85  #[getset(get_copy = "pub")]
86  resolve_symlink_target: bool,
87  /// Current directory that will be used to determine relative file locations if necessary. It
88  /// must match the parent directory of the configuration file that declared this action.
89  current_dir: PathBuf,
90}
91
92/// A native create action that works on the real filesystem.
93pub type NativeLinkAction<'a> = LinkAction<'a, OsFileSystem>;
94/// A Fake create action that works on a fake test filesystem.
95pub type FakeLinkAction<'a> = LinkAction<'a, FakeFileSystem>;
96
97impl<'a, F: FileSystem + UnixFileSystem> LinkAction<'a, F> {
98  /// Constructs a new [LinkAction]
99  #[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)?, // path exists, force, is_dir
173                        (true, true, false, _, _ ) =>fs.remove_file(&path)?, // path exists, force, is_file
174                        (true, false, _, true, true ) =>fs.remove_file(&path)?, // path exists, no force, is_symlink, relink
175                        (true, false, _, true, false) =>
176                            // path exists, no force, is symlink, no relink
177                            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}