use anyhow::{Context, Result};
use std::io;
#[cfg(windows)]
use std::os::windows::fs as windows_fs;
use std::path::Path;
#[cfg(windows)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SymlinkType {
File,
Directory,
}
#[cfg(windows)]
pub fn determine_symlink_type(target: &Path) -> Result<SymlinkType> {
if target.exists() {
let metadata = std::fs::metadata(target).with_context(|| {
format!(
"Failed to read metadata for symlink target: {}",
target.display()
)
})?;
if metadata.is_dir() {
Ok(SymlinkType::Directory)
} else {
Ok(SymlinkType::File)
}
} else {
if target
.to_string_lossy()
.ends_with(std::path::MAIN_SEPARATOR)
{
Ok(SymlinkType::Directory)
} else if target.extension().is_none() && !target.to_string_lossy().contains('.') {
Ok(SymlinkType::Directory)
} else {
Ok(SymlinkType::File)
}
}
}
#[cfg(windows)]
pub fn create_symlink(link: &Path, target: &Path) -> Result<()> {
let symlink_type = determine_symlink_type(target)?;
if let Some(parent) = link.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!(
"Failed to create parent directory for symlink: {}",
parent.display()
)
})?;
}
if link.exists() {
if link.symlink_metadata()?.file_type().is_symlink() {
std::fs::remove_file(link)
.or_else(|_| std::fs::remove_dir(link))
.with_context(|| {
format!("Failed to remove existing symlink: {}", link.display())
})?;
}
}
match symlink_type {
SymlinkType::File => {
windows_fs::symlink_file(target, link)
.map_err(|e| handle_symlink_error(e, link, target, "file"))?;
}
SymlinkType::Directory => {
windows_fs::symlink_dir(target, link)
.map_err(|e| handle_symlink_error(e, link, target, "directory"))?;
}
}
Ok(())
}
#[cfg(windows)]
fn handle_symlink_error(
error: io::Error,
link: &Path,
target: &Path,
link_type: &str,
) -> anyhow::Error {
use std::io::ErrorKind;
match error.kind() {
ErrorKind::PermissionDenied => {
anyhow::anyhow!(
"Permission denied creating {} symlink '{}' -> '{}'. \
Windows requires either: \
1) Run as Administrator, or \
2) Developer Mode enabled (Windows 10+), or \
3) SeCreateSymbolicLinkPrivilege granted to user",
link_type,
link.display(),
target.display()
)
}
ErrorKind::NotFound => {
anyhow::anyhow!(
"Failed to create {} symlink '{}' -> '{}': target path or parent directory not found",
link_type,
link.display(),
target.display()
)
}
_ => {
anyhow::anyhow!(
"Failed to create {} symlink '{}' -> '{}': {}",
link_type,
link.display(),
target.display(),
error
)
}
}
}
#[cfg(windows)]
pub fn read_symlink(link: &Path) -> Result<std::path::PathBuf> {
std::fs::read_link(link)
.with_context(|| format!("Failed to read symlink target: {}", link.display()))
}
#[cfg(windows)]
pub fn is_symlink(path: &Path) -> Result<bool> {
let metadata = path
.symlink_metadata()
.with_context(|| format!("Failed to read symlink metadata: {}", path.display()))?;
Ok(metadata.file_type().is_symlink())
}
#[cfg(windows)]
pub fn copy_symlink(source: &Path, dest: &Path) -> Result<()> {
let target = read_symlink(source)?;
create_symlink(dest, &target)?;
Ok(())
}
#[cfg(not(windows))]
pub fn create_symlink(_link: &Path, _target: &Path) -> Result<()> {
Err(anyhow::anyhow!(
"Windows symlink functions called on non-Windows platform"
))
}
#[cfg(not(windows))]
pub fn read_symlink(_link: &Path) -> Result<std::path::PathBuf> {
Err(anyhow::anyhow!(
"Windows symlink functions called on non-Windows platform"
))
}
#[cfg(not(windows))]
pub fn is_symlink(_path: &Path) -> Result<bool> {
Err(anyhow::anyhow!(
"Windows symlink functions called on non-Windows platform"
))
}
#[cfg(not(windows))]
pub fn copy_symlink(_source: &Path, _dest: &Path) -> Result<()> {
Err(anyhow::anyhow!(
"Windows symlink functions called on non-Windows platform"
))
}
#[cfg(test)]
#[cfg(windows)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_determine_symlink_type() {
assert_eq!(
determine_symlink_type(Path::new("C:\\some\\dir\\")).unwrap(),
SymlinkType::Directory
);
assert_eq!(
determine_symlink_type(Path::new("C:\\some\\file.txt")).unwrap(),
SymlinkType::File
);
}
#[test]
#[ignore] fn test_create_file_symlink() {
let temp_dir = tempdir().unwrap();
let target = temp_dir.path().join("target.txt");
let link = temp_dir.path().join("link.txt");
std::fs::write(&target, "test content").unwrap();
create_symlink(&link, &target).unwrap();
assert!(is_symlink(&link).unwrap());
assert_eq!(read_symlink(&link).unwrap(), target);
}
#[test]
#[ignore] fn test_create_directory_symlink() {
let temp_dir = tempdir().unwrap();
let target = temp_dir.path().join("target_dir");
let link = temp_dir.path().join("link_dir");
std::fs::create_dir(&target).unwrap();
create_symlink(&link, &target).unwrap();
assert!(is_symlink(&link).unwrap());
assert_eq!(read_symlink(&link).unwrap(), target);
}
}