diff --git a/src/main.rs b/src/main.rs index c895f46..229d353 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod tag_engine; +mod symlink; use std::collections::BTreeSet; use std::error::Error; diff --git a/src/symlink.rs b/src/symlink.rs new file mode 100644 index 0000000..6816c15 --- /dev/null +++ b/src/symlink.rs @@ -0,0 +1,134 @@ +use std::path::{Path, PathBuf}; +use thiserror::Error; +use fs_err as fs; + +#[derive(Error, Debug)] +pub enum SymlinkError { + #[error("Failed to create directory {path}: {source}")] + CreateDir { + path: String, + source: std::io::Error, + }, + #[error("Failed to create symlink from {from} to {to}: {source}")] + CreateLink { + from: String, + to: String, + source: std::io::Error, + }, + #[error("Invalid path: {0}")] + InvalidPath(String), +} + +pub fn create_symlink_tree(paths: Vec, target_dir: &Path) -> Result<(), SymlinkError> { + for path in paths { + // Ensure path is absolute and clean + let abs_path = fs::canonicalize(&path).map_err(|_| { + SymlinkError::InvalidPath(path.to_string_lossy().into_owned()) + })?; + + // Get the file name for the symlink + let file_name = abs_path.file_name().ok_or_else(|| { + SymlinkError::InvalidPath(abs_path.to_string_lossy().into_owned()) + })?; + + // Create target directory if it doesn't exist + fs::create_dir_all(target_dir).map_err(|e| SymlinkError::CreateDir { + path: target_dir.to_string_lossy().into_owned(), + source: e, + })?; + + // Create the symlink + let link_path = target_dir.join(file_name); + #[cfg(unix)] + std::os::unix::fs::symlink(&abs_path, &link_path).map_err(|e| SymlinkError::CreateLink { + from: abs_path.to_string_lossy().into_owned(), + to: link_path.to_string_lossy().into_owned(), + source: e, + })?; + + #[cfg(windows)] + std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| SymlinkError::CreateLink { + from: abs_path.to_string_lossy().into_owned(), + to: link_path.to_string_lossy().into_owned(), + source: e, + })?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_create_symlink_tree_basic() -> Result<(), Box> { + let source_dir = TempDir::new()?; + let target_dir = TempDir::new()?; + + // Create a test file + let test_file = source_dir.path().join("test.txt"); + fs::write(&test_file, "test content")?; + + // Create symlink tree + create_symlink_tree( + vec![test_file.clone()], + target_dir.path() + )?; + + // Verify symlink exists and points to correct file + let symlink = target_dir.path().join("test.txt"); + assert!(symlink.exists()); + assert!(symlink.is_symlink()); + + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + assert_eq!( + fs::metadata(&test_file)?.ino(), + fs::metadata(&symlink)?.ino() + ); + } + + Ok(()) + } + + #[test] + fn test_create_symlink_tree_nested() -> Result<(), Box> { + let source_dir = TempDir::new()?; + let target_dir = TempDir::new()?; + + // Create nested test file + let nested_dir = source_dir.path().join("nested"); + fs::create_dir_all(&nested_dir)?; + let test_file = nested_dir.join("test.txt"); + fs::write(&test_file, "test content")?; + + // Create symlink tree + create_symlink_tree( + vec![test_file], + &target_dir.path().join("nested") + )?; + + // Verify directory and symlink were created + let symlink = target_dir.path().join("nested/test.txt"); + assert!(symlink.exists()); + assert!(symlink.is_symlink()); + + Ok(()) + } + + #[test] + fn test_create_symlink_tree_invalid_path() { + let target_dir = TempDir::new().unwrap(); + + // Try to create symlink with non-existent source + let result = create_symlink_tree( + vec![PathBuf::from("/nonexistent/path")], + target_dir.path() + ); + + assert!(matches!(result, Err(SymlinkError::InvalidPath(_)))); + } +}