1use derivative::Derivative;
26use dotfiles_core::error::DotfilesError;
27use dotfiles_core::error::ErrorType;
28use dotfiles_core::path::convert_path_to_absolute;
29use dotfiles_core::path::process_home_dir_in_path;
30use dotfiles_core::settings::{initialize_settings_object, Setting, Settings};
31use dotfiles_core::yaml_util::{self, get_boolean_setting_from_context};
32use dotfiles_core::{action::SKIP_IN_CI_SETTING, Action};
33use filesystem::FakeFileSystem;
34use filesystem::FileSystem;
35use filesystem::OsFileSystem;
36use filesystem::UnixFileSystem;
37use getset::CopyGetters;
38use getset::Getters;
39use log::error;
40use log::info;
41use std::format;
42use std::io;
43use std::io::ErrorKind;
44use std::path::Path;
45use std::path::PathBuf;
46use strict_yaml_rust::StrictYaml;
47
48pub const PATH_SETTING: &str = "path";
50pub const TARGET_SETTING: &str = "target";
52pub const FORCE_SETTING: &str = "force";
54pub const RELINK_SETTING: &str = "relink";
57pub const CREATE_PARENT_DIRS_SETTING: &str = "create_parent_dirs";
59pub const IGNORE_MISSING_TARGET_SETTING: &str = "ignore_missing_target";
61pub const RESOLVE_SYMLINK_TARGET_SETTING: &str = "resolve_symlink_target";
63
64pub fn default_settings() -> Settings {
66 initialize_settings_object(&[
67 (FORCE_SETTING.to_owned(), Setting::Boolean(false)),
68 (RELINK_SETTING.to_owned(), Setting::Boolean(false)),
69 (
70 CREATE_PARENT_DIRS_SETTING.to_owned(),
71 Setting::Boolean(false),
72 ),
73 (
74 IGNORE_MISSING_TARGET_SETTING.to_owned(),
75 Setting::Boolean(false),
76 ),
77 (
78 RESOLVE_SYMLINK_TARGET_SETTING.to_owned(),
79 Setting::Boolean(false),
80 ),
81 (SKIP_IN_CI_SETTING.to_owned(), Setting::Boolean(false)),
82 ])
83}
84
85#[derive(Derivative, Getters, CopyGetters)]
89#[derivative(Debug, PartialEq)]
90pub struct LinkAction<F: FileSystem + UnixFileSystem> {
91 skip_in_ci: bool,
93 #[derivative(Debug = "ignore", PartialEq = "ignore")]
98 fs: F,
99 #[getset(get = "pub")]
101 path: String,
102 #[getset(get = "pub")]
104 target: String,
105 #[getset(get_copy = "pub")]
107 relink: bool,
108 #[getset(get_copy = "pub")]
110 force: bool,
111 #[getset(get_copy = "pub")]
113 create_parent_dirs: bool,
114 #[getset(get_copy = "pub")]
116 ignore_missing_target: bool,
117 #[getset(get_copy = "pub")]
120 resolve_symlink_target: bool,
121 current_dir: PathBuf,
124}
125
126pub type NativeLinkAction = LinkAction<OsFileSystem>;
128pub type FakeLinkAction = LinkAction<FakeFileSystem>;
130
131impl<F: FileSystem + UnixFileSystem> LinkAction<F> {
132 #[allow(clippy::too_many_arguments)]
134 pub fn new(
135 fs: F,
136 path: String,
137 target: String,
138 context_settings: &'_ Settings,
139 defaults: &'_ Settings,
140 current_dir: PathBuf,
141 ) -> Result<Self, DotfilesError> {
142 let relink =
143 get_boolean_setting_from_context(RELINK_SETTING, context_settings, defaults).unwrap();
144 let force =
145 get_boolean_setting_from_context(FORCE_SETTING, context_settings, defaults).unwrap();
146 let create_parent_dirs =
147 get_boolean_setting_from_context(CREATE_PARENT_DIRS_SETTING, context_settings, defaults)
148 .unwrap();
149 let ignore_missing_target =
150 get_boolean_setting_from_context(IGNORE_MISSING_TARGET_SETTING, context_settings, defaults)
151 .unwrap();
152 let resolve_symlink_target =
153 get_boolean_setting_from_context(RESOLVE_SYMLINK_TARGET_SETTING, context_settings, defaults)
154 .unwrap();
155 let skip_in_ci =
156 get_boolean_setting_from_context(SKIP_IN_CI_SETTING, context_settings, defaults).unwrap();
157 let action = Self {
158 skip_in_ci,
159 fs,
160 path,
161 target,
162 relink,
163 force,
164 create_parent_dirs,
165 ignore_missing_target,
166 resolve_symlink_target,
167 current_dir,
168 };
169 log::trace!("Creating new {:?}", action);
170 Ok(action)
171 }
172}
173
174impl<F: FileSystem + UnixFileSystem> Action for LinkAction<F> {
175 fn execute(&self) -> Result<(), DotfilesError> {
176 fn create_symlink<F: FileSystem + UnixFileSystem>(
177 fs: &'_ F,
178 action: &'_ LinkAction<F>,
179 path: PathBuf,
180 mut target: PathBuf,
181 ) -> io::Result<()> {
182 let target_exists = fs.is_dir(&target) || fs.is_file(&target);
183 let path_exists = fs.is_dir(&path) || fs.is_file(&path);
184 let path_is_symlink = fs.get_symlink_src(&path).is_ok();
185 let target_is_symlink = fs.get_symlink_src(&target).is_ok();
186 if target_is_symlink && action.resolve_symlink_target() {
187 fn resolve_symlink_target<F: FileSystem + UnixFileSystem, P: AsRef<Path>>(
188 fs: &'_ F,
189 link_path: P,
190 ) -> io::Result<PathBuf> {
191 match fs.get_symlink_src(link_path.as_ref()) {
192 Ok(link_target) => resolve_symlink_target(fs, link_target),
193 Err(e) if [ErrorKind::IsADirectory, ErrorKind::InvalidInput].contains(&e.kind()) => {
194 Ok(PathBuf::from(link_path.as_ref()))
195 }
196 Err(e) => Err(e),
197 }
198 }
199 target = resolve_symlink_target(fs, &target)?
200 }
201 if target_exists || action.ignore_missing_target() {
202 if !fs.is_dir(path.parent().unwrap()) && action.create_parent_dirs() {
203 fs.create_dir_all(path.parent().unwrap())?
204 }
205 match (path_exists, action.force(), fs.is_dir(&path), path_is_symlink, action.relink()) {
206 (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) =>
210 return Err(io::Error::new(
212 ErrorKind::AlreadyExists,
213 format!("{:?} already exists. Use `force` to delete a file/directory or `relink` to recreate a symlink", path))),
214 _ => ()
215 }
216 fs.symlink(&target, &path)
217 } else {
218 Err(io::Error::new(
219 ErrorKind::NotFound,
220 format!(
221 "Couldn't find target file {target:?} to link to, use `ignore_missing_target` to ignore",
222 ),
223 ))
224 }
225 }
226 let path: PathBuf = PathBuf::from(self.path());
227 let mut path = process_home_dir_in_path(&path);
228 path = convert_path_to_absolute(&path, Some(&self.current_dir))?;
229 let target = PathBuf::from(self.target());
230 let mut target = process_home_dir_in_path(&target);
231 target = convert_path_to_absolute(&target, Some(&self.current_dir))?;
232 match create_symlink(&self.fs, self, path, target) {
233 Ok(()) => {
234 info!("Created symlink {} -> {}", self.path, self.target);
235 Ok(())
236 }
237 Err(err) => {
238 error!(
239 "Couldn't create symlink {} -> {}: {}",
240 self.path, self.target, err
241 );
242 Err(DotfilesError::from(
243 err.to_string(),
244 ErrorType::FileSystemError { fs_error: err },
245 ))
246 }
247 }
248 }
249
250 fn skip_in_ci(&self) -> bool {
251 self.skip_in_ci
252 }
253}
254
255pub fn parse_action<F: FileSystem + UnixFileSystem + Clone>(
257 fs: F,
258 settings: &Settings,
259 yaml: &StrictYaml,
260 current_directory: &Path,
261) -> Result<LinkAction<F>, DotfilesError> {
262 parse_shortened_action(fs.clone(), settings, yaml, current_directory)
263 .or_else(|_| parse_full_action(fs, settings, yaml, current_directory))
264}
265
266fn parse_full_action<F: FileSystem + UnixFileSystem>(
267 fs: F,
268 context_settings: &Settings,
269 yaml: &StrictYaml,
270 current_dir: &Path,
271) -> Result<LinkAction<F>, DotfilesError> {
272 let defaults = default_settings();
273 let path = yaml_util::get_string_setting_from_yaml_or_context(
274 PATH_SETTING,
275 yaml,
276 context_settings,
277 &defaults,
278 )?;
279 let target = yaml_util::get_string_setting_from_yaml_or_context(
280 TARGET_SETTING,
281 yaml,
282 context_settings,
283 &defaults,
284 )?;
285 let action_settings: Result<Settings, DotfilesError> = defaults
286 .keys()
287 .map(|name| {
288 yaml_util::get_setting_from_yaml_hash_or_from_context(name, yaml, context_settings, &defaults)
289 .map(|setting| (name.to_owned(), setting))
290 })
291 .collect();
292
293 LinkAction::<F>::new(
294 fs,
295 path,
296 target,
297 &action_settings?,
298 &defaults,
299 current_dir.to_owned(),
300 )
301}
302
303fn parse_shortened_action<F: FileSystem + UnixFileSystem>(
304 fs: F,
305 context_settings: &Settings,
306 yaml: &StrictYaml,
307 current_dir: &Path,
308) -> Result<LinkAction<F>, DotfilesError> {
309 let defaults = default_settings();
310 if let StrictYaml::Hash(hash) = yaml {
311 match hash.len() {
312 1 => {
313 if let (StrictYaml::String(path), StrictYaml::String(target)) = hash.front().unwrap() {
314 LinkAction::<F>::new(
315 fs,
316 path.clone(),
317 target.clone(),
318 context_settings,
319 &defaults,
320 current_dir.to_owned()
321 )
322 } else {
323 Err(DotfilesError::from_wrong_yaml(
324 "StrictYaml passed to configure a short Link action is not a hash of string to string, cant parse".into(),
325 yaml.to_owned(), StrictYaml::Hash(Default::default())))
326 }
327 }
328
329 x => Err(DotfilesError::from(
330 format!(
331 "StrictYaml passed to configure a short Link action is a hash with {x} values, must be just 1",),
332 ErrorType::InconsistentConfigurationError,
333 )),
334 }
335 } else {
336 Err(DotfilesError::from_wrong_yaml(
337 "StrictYaml passed to configure a Link action is not a Hash".into(),
338 yaml.to_owned(),
339 StrictYaml::Hash(Default::default()),
340 ))
341 }
342}