feat: Add cross-platform symlink helper module with robust error handling

This commit is contained in:
Moritz Böhme 2025-02-23 16:34:16 +01:00
parent 2809b6e818
commit aa18a35f8d
2 changed files with 135 additions and 0 deletions

View file

@ -1,4 +1,5 @@
mod tag_engine;
mod symlink;
use std::collections::BTreeSet;
use std::error::Error;

134
src/symlink.rs Normal file
View file

@ -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<PathBuf>, 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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(_))));
}
}