#![cfg(unix)]
use crate::link::directive::*;
use derivative::Derivative;
use dotfiles_core::action::Action;
use dotfiles_core::action::SKIP_IN_CI_SETTING;
use dotfiles_core::error::DotfilesError;
use dotfiles_core::error::ErrorType;
use dotfiles_core::path::convert_path_to_absolute;
use dotfiles_core::path::process_home_dir_in_path;
use dotfiles_core::settings::Settings;
use dotfiles_core::yaml_util::get_boolean_setting_from_context;
use dotfiles_core_macros::ConditionalAction;
use filesystem::FakeFileSystem;
use filesystem::FileSystem;
use filesystem::OsFileSystem;
use filesystem::UnixFileSystem;
use getset::CopyGetters;
use getset::Getters;
use log::error;
use log::info;
use std::format;
use std::io;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
#[derive(Derivative, Getters, CopyGetters, ConditionalAction)]
#[derivative(Debug, PartialEq)]
pub struct LinkAction<'a, F: FileSystem + UnixFileSystem> {
skip_in_ci: bool,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
fs: &'a F,
#[getset(get = "pub")]
path: String,
#[getset(get = "pub")]
target: String,
#[getset(get_copy = "pub")]
relink: bool,
#[getset(get_copy = "pub")]
force: bool,
#[getset(get_copy = "pub")]
create_parent_dirs: bool,
#[getset(get_copy = "pub")]
ignore_missing_target: bool,
#[getset(get_copy = "pub")]
resolve_symlink_target: bool,
current_dir: PathBuf,
}
pub type NativeLinkAction<'a> = LinkAction<'a, OsFileSystem>;
pub type FakeLinkAction<'a> = LinkAction<'a, FakeFileSystem>;
impl<'a, F: FileSystem + UnixFileSystem> LinkAction<'a, F> {
#[allow(clippy::too_many_arguments)]
pub fn new(
fs: &'a F,
path: String,
target: String,
context_settings: &'_ Settings,
defaults: &'_ Settings,
current_dir: PathBuf,
) -> Result<Self, DotfilesError> {
let relink =
get_boolean_setting_from_context(RELINK_SETTING, context_settings, defaults).unwrap();
let force =
get_boolean_setting_from_context(FORCE_SETTING, context_settings, defaults).unwrap();
let create_parent_dirs =
get_boolean_setting_from_context(CREATE_PARENT_DIRS_SETTING, context_settings, defaults)
.unwrap();
let ignore_missing_target =
get_boolean_setting_from_context(IGNORE_MISSING_TARGET_SETTING, context_settings, defaults)
.unwrap();
let resolve_symlink_target =
get_boolean_setting_from_context(RESOLVE_SYMLINK_TARGET_SETTING, context_settings, defaults)
.unwrap();
let skip_in_ci =
get_boolean_setting_from_context(SKIP_IN_CI_SETTING, context_settings, defaults).unwrap();
let action = Self {
skip_in_ci,
fs,
path,
target,
relink,
force,
create_parent_dirs,
ignore_missing_target,
resolve_symlink_target,
current_dir,
};
log::trace!("Creating new {:?}", action);
Ok(action)
}
}
impl<F: FileSystem + UnixFileSystem> Action<'_> for LinkAction<'_, F> {
fn execute(&self) -> Result<(), DotfilesError> {
fn create_symlink<F: FileSystem + UnixFileSystem>(
fs: &'_ F,
action: &'_ LinkAction<F>,
path: PathBuf,
mut target: PathBuf,
) -> io::Result<()> {
let target_exists = fs.is_dir(&target) || fs.is_file(&target);
let path_exists = fs.is_dir(&path) || fs.is_file(&path);
let path_is_symlink = fs.get_symlink_src(&path).is_ok();
let target_is_symlink = fs.get_symlink_src(&target).is_ok();
if target_is_symlink && action.resolve_symlink_target() {
fn resolve_symlink_target<F: FileSystem + UnixFileSystem, P: AsRef<Path>>(
fs: &'_ F,
link_path: P,
) -> io::Result<PathBuf> {
match fs.get_symlink_src(link_path.as_ref()) {
Ok(link_target) => resolve_symlink_target(fs, link_target),
Err(e) if [ErrorKind::IsADirectory, ErrorKind::InvalidInput].contains(&e.kind()) => {
Ok(PathBuf::from(link_path.as_ref()))
}
Err(e) => Err(e),
}
}
target = resolve_symlink_target(fs, &target)?
}
if target_exists || action.ignore_missing_target() {
if !fs.is_dir(path.parent().unwrap()) && action.create_parent_dirs() {
fs.create_dir_all(path.parent().unwrap())?
}
match (path_exists, action.force(), fs.is_dir(&path), path_is_symlink, action.relink()) {
(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) =>
return Err(io::Error::new(
ErrorKind::AlreadyExists,
format!("{:?} already exists. Use `force` to delete a file/directory or `relink` to recreate a symlink", &path))),
_ => ()
}
fs.symlink(&target, &path)
} else {
Err(io::Error::new(
ErrorKind::NotFound,
format!(
"Couldn't find target file {target:?} to link to, use `ignore_missing_target` to ignore",
),
))
}
}
let path: PathBuf = PathBuf::from(self.path());
let mut path = process_home_dir_in_path(&path);
path = convert_path_to_absolute(&path, Some(&self.current_dir))?;
let target = PathBuf::from(self.target());
let mut target = process_home_dir_in_path(&target);
target = convert_path_to_absolute(&target, Some(&self.current_dir))?;
match create_symlink(self.fs, self, path, target) {
Ok(()) => {
info!("Created symlink {} -> {}", &self.path, &self.target);
Ok(())
}
Err(err) => {
error!(
"Couldn't create symlink {} -> {}: {}",
&self.path, &self.target, err
);
Err(DotfilesError::from(
err.to_string(),
ErrorType::FileSystemError { fs_error: err },
))
}
}
}
}