use std::path::{Path, PathBuf};
use crate::error::{Result, SkillError};
const EXCLUDE_FILES: &[&str] = &["metadata.json"];
const EXCLUDE_DIRS: &[&str] = &[".git"];
pub(super) async fn clean_and_create(path: &Path) -> Result<()> {
drop(tokio::fs::remove_dir_all(path).await);
tokio::fs::create_dir_all(path)
.await
.map_err(|e| SkillError::io(path, e))
}
pub(super) async fn copy_directory(src: &Path, dest: &Path) -> Result<()> {
tokio::fs::create_dir_all(dest)
.await
.map_err(|e| SkillError::io(dest, e))?;
let mut entries = tokio::fs::read_dir(src)
.await
.map_err(|e| SkillError::io(src, e))?;
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| SkillError::io(src, e))?
{
let name = entry.file_name();
let name_str = name.to_string_lossy();
let ft = entry
.file_type()
.await
.map_err(|e| SkillError::io(src, e))?;
if ft.is_dir() {
if EXCLUDE_DIRS.contains(&name_str.as_ref()) || name_str.starts_with('_') {
continue;
}
let sub_dest = dest.join(&name);
Box::pin(copy_directory(&entry.path(), &sub_dest)).await?;
} else if ft.is_symlink() {
if EXCLUDE_FILES.contains(&name_str.as_ref()) || name_str.starts_with('_') {
continue;
}
let src_path = entry.path();
let dest_file = dest.join(&name);
match tokio::fs::metadata(&src_path).await {
Ok(meta) if meta.is_dir() => {
Box::pin(copy_directory(&src_path, &dest_file)).await?;
}
Ok(_) => {
drop(tokio::fs::copy(&src_path, &dest_file).await);
}
Err(_) => {
tracing::warn!("Skipping broken symlink: {}", src_path.display());
}
}
} else {
if EXCLUDE_FILES.contains(&name_str.as_ref()) || name_str.starts_with('_') {
continue;
}
let dest_file = dest.join(&name);
tokio::fs::copy(entry.path(), &dest_file)
.await
.map_err(|e| SkillError::io(&dest_file, e))?;
}
}
Ok(())
}
async fn resolve_parent_symlinks(path: &Path) -> PathBuf {
let resolved = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
let Some(dir) = resolved.parent() else {
return resolved;
};
let base = resolved.file_name().unwrap_or_default().to_os_string();
tokio::fs::canonicalize(dir)
.await
.map_or(resolved, |real_dir| real_dir.join(base))
}
#[cfg(unix)]
async fn symlink_already_points_to(link_path: &Path, resolved_target: &Path) -> bool {
let Ok(existing) = tokio::fs::read_link(link_path).await else {
return false;
};
let existing_abs = if existing.is_relative() {
link_path
.parent()
.unwrap_or_else(|| Path::new("."))
.join(&existing)
} else {
existing
};
let existing_resolved = std::path::absolute(&existing_abs).unwrap_or(existing_abs);
existing_resolved == resolved_target
}
pub(super) async fn create_symlink(target: &Path, link_path: &Path) -> bool {
let resolved_target = std::path::absolute(target).unwrap_or_else(|_| target.to_path_buf());
let resolved_link = std::path::absolute(link_path).unwrap_or_else(|_| link_path.to_path_buf());
let real_target = tokio::fs::canonicalize(&resolved_target)
.await
.unwrap_or_else(|_| resolved_target.clone());
let real_link = tokio::fs::canonicalize(&resolved_link)
.await
.unwrap_or_else(|_| resolved_link.clone());
if real_target == real_link {
return true;
}
let real_target_parent = resolve_parent_symlinks(target).await;
let real_link_parent = resolve_parent_symlinks(link_path).await;
if real_target_parent == real_link_parent {
return true;
}
if let Ok(meta) = tokio::fs::symlink_metadata(link_path).await {
if meta.is_symlink() {
#[cfg(unix)]
if symlink_already_points_to(link_path, &resolved_target).await {
return true;
}
drop(tokio::fs::remove_file(link_path).await);
} else {
drop(tokio::fs::remove_dir_all(link_path).await);
}
} else {
drop(tokio::fs::remove_file(link_path).await);
}
if let Some(parent) = link_path.parent()
&& tokio::fs::create_dir_all(parent).await.is_err()
{
return false;
}
#[cfg(unix)]
{
let real_link_dir =
resolve_parent_symlinks(link_path.parent().unwrap_or_else(|| Path::new("."))).await;
let rel =
pathdiff::diff_paths(target, &real_link_dir).unwrap_or_else(|| target.to_path_buf());
tokio::fs::symlink(&rel, link_path).await.is_ok()
}
#[cfg(windows)]
{
let target = target.to_path_buf();
let link = link_path.to_path_buf();
tokio::task::spawn_blocking(move || junction::create(&target, &link).is_ok())
.await
.unwrap_or(false)
}
#[cfg(not(any(unix, windows)))]
{
false
}
}