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}