use crate::errors::{ConfGuardContext, ConfGuardError, ConfGuardResult};
use pathdiff::diff_paths;
use std::path::{Path, PathBuf};
use std::{env, fs};
use fs_extra::dir;
use fs_extra::dir::CopyOptions;
#[cfg(unix)]
use std::os::unix::fs as platform_fs;
#[cfg(windows)]
use std::os::windows::fs as platform_fs;
use tracing::{debug, info, instrument};
#[instrument]
pub fn create_link(original_path: &Path, link_path: &Path, relative: bool) -> ConfGuardResult<()> {
if !original_path.exists() {
return Err(ConfGuardError::OriginalPathNotFound(
original_path.display().to_string(),
));
}
if let Some(parent) = link_path.parent() {
std::fs::create_dir_all(parent)?;
}
let target_path = if relative {
if let Some(link_parent) = link_path.parent() {
diff_paths(original_path, link_parent)
.ok_or(ConfGuardError::FailedToCreateRelativePath)?
} else {
original_path.to_path_buf()
}
} else {
original_path.to_path_buf()
};
#[cfg(unix)]
{
platform_fs::symlink(target_path, link_path)?;
}
#[cfg(windows)]
{
if original_path.is_dir() {
platform_fs::symlink_dir(target_path, link_path)?;
} else {
platform_fs::symlink_file(target_path, link_path)?;
}
}
Ok(())
}
#[instrument]
pub fn replace_link_with_target(link_path: &Path) -> ConfGuardResult<()> {
validate_utf8_path(link_path)?;
if !link_path.exists() {
return Err(ConfGuardError::LinkNotFound(link_path.to_path_buf()));
}
debug!("metadata: {:?}", std::fs::symlink_metadata(link_path)?);
if !link_path.symlink_metadata()?.file_type().is_symlink() {
return Err(ConfGuardError::PathNotSymbolicLink(link_path.to_path_buf()));
}
let link_target = std::fs::read_link(link_path).context("read link")?;
let link_path_abs = to_absolute_path(validate_utf8_path(link_path)?)?;
debug!(
"link_path_abs: {:?}, link_target: {:?}, CWD: {:?}",
link_path_abs,
link_target,
std::env::current_dir()?
);
let original_dir = env::current_dir()?;
env::set_current_dir(link_path_abs.parent().unwrap())?;
let link_target_metadata = fs::metadata(&link_target).context("read link metadata")?;
debug!("link_target_metadata: {:?}", link_target_metadata);
fs::remove_file(&link_path_abs).with_context(|| {
format!(
"remove file: {:?}, CWD: {:?}",
link_path_abs,
env::current_dir().unwrap_or_else(|_| PathBuf::from("unknown"))
)
})?;
if link_target_metadata.is_file() {
fs::rename(&link_target, &link_path_abs)?;
} else if link_target_metadata.is_dir() {
let options = CopyOptions {
copy_inside: true,
..Default::default()
};
dir::move_dir(&link_target, &link_path_abs, &options)?;
}
info!(
"Replaced link {:?} with target {:?}",
link_path, link_target
);
env::set_current_dir(original_dir)?;
Ok(())
}
fn validate_utf8_path(path: &Path) -> ConfGuardResult<&str> {
path.to_str()
.ok_or_else(|| ConfGuardError::InvalidUtf8Path(path.to_path_buf()))
}
fn to_absolute_path(relative_path: &str) -> ConfGuardResult<PathBuf> {
let current_directory = env::current_dir()?;
let absolute_path = current_directory.join(relative_path);
validate_utf8_path(&absolute_path)?;
Ok(absolute_path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::helper::testing;
use crate::util::testing::{create_file_with_content, print_active_env_vars, TEST_ENV_VARS};
use std::fs::File;
use std::io::{Read, Write};
#[test]
fn test_create_relative_link_file() -> ConfGuardResult<()> {
let base_dir = testing::setup_test_dir();
print_active_env_vars(TEST_ENV_VARS);
let original_file = base_dir.join("original/nested/file.txt");
create_file_with_content(original_file.to_str().unwrap(), "Hello world")?;
let link_path = base_dir.join("links/link");
create_link(&original_file, &link_path, true)?;
assert!(link_path.exists());
assert!(link_path.symlink_metadata()?.file_type().is_symlink());
let mut file = File::open(&link_path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
assert_eq!(content, "Hello world");
Ok(())
}
#[test]
fn test_create_relative_link() -> ConfGuardResult<()> {
let base_dir = testing::setup_test_dir();
print_active_env_vars(TEST_ENV_VARS);
let original_dir = base_dir.join("original");
fs::create_dir_all(&original_dir)?;
let link_parent = base_dir.join("links");
let link_path = link_parent.join("link");
create_link(&original_dir, &link_path, true)?;
assert!(link_path.exists());
assert!(link_path.symlink_metadata()?.file_type().is_symlink());
Ok(())
}
}