use crate::core::{SymlinkInfo, TwinError, TwinResult};
use std::fs;
use std::path::Path;
#[cfg(windows)]
use std::process::Command;
pub trait SymlinkManager {
fn create_symlink(&self, source: &Path, target: &Path) -> TwinResult<SymlinkInfo>;
fn remove_symlink(&self, path: &Path) -> TwinResult<()>;
#[allow(dead_code)]
fn validate_symlink(&self, path: &Path) -> TwinResult<bool>;
#[allow(dead_code)]
fn get_manual_instructions(&self, source: &Path, target: &Path) -> String;
}
#[cfg(unix)]
#[allow(dead_code)]
pub type PlatformSymlinkManager = UnixSymlinkManager;
#[cfg(windows)]
#[allow(dead_code)]
pub type PlatformSymlinkManager = WindowsSymlinkManager;
#[cfg(unix)]
pub struct UnixSymlinkManager;
#[cfg(unix)]
impl UnixSymlinkManager {
pub fn new() -> Self {
Self
}
}
#[cfg(unix)]
impl Default for UnixSymlinkManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(unix)]
impl SymlinkManager for UnixSymlinkManager {
fn create_symlink(&self, source: &Path, target: &Path) -> TwinResult<SymlinkInfo> {
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
eprintln!(
"🔗 シンボリックリンク作成: {} -> {}",
target.display(),
source.display()
);
}
if !source.exists() {
return Err(TwinError::symlink(
format!("Source path does not exist: {}", source.display()),
Some(source.to_path_buf()),
));
}
if target.exists() || target.is_symlink() {
fs::remove_file(target).ok();
}
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
match symlink(source, target) {
Ok(_) => {
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok()
{
eprintln!("✅ シンボリックリンク作成成功");
}
let mut info = SymlinkInfo::new(source.to_path_buf(), target.to_path_buf());
info.set_success();
Ok(info)
}
Err(e) => {
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok()
{
eprintln!("❌ シンボリックリンク作成失敗: {}", e);
}
let mut info = SymlinkInfo::new(source.to_path_buf(), target.to_path_buf());
info.set_error(format!("Failed to create symlink: {}", e));
Err(TwinError::symlink(
format!("Failed to create symlink: {}", e),
Some(target.to_path_buf()),
))
}
}
}
}
fn remove_symlink(&self, path: &Path) -> TwinResult<()> {
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
eprintln!("🗑️ シンボリックリンク削除: {}", path.display());
}
if path.is_symlink() {
fs::remove_file(path)?;
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
eprintln!("✅ シンボリックリンク削除成功");
}
}
Ok(())
}
#[allow(dead_code)]
fn validate_symlink(&self, path: &Path) -> TwinResult<bool> {
if !path.exists() {
return Ok(false);
}
let metadata = fs::symlink_metadata(path)?;
if !metadata.file_type().is_symlink() {
return Ok(false);
}
match fs::metadata(path) {
Ok(_) => Ok(true),
Err(_) => Ok(false), }
}
#[allow(dead_code)]
fn get_manual_instructions(&self, source: &Path, target: &Path) -> String {
format!(
"To manually create the symlink, run:\n ln -s \"{}\" \"{}\"",
source.display(),
target.display()
)
}
}
#[cfg(windows)]
pub struct WindowsSymlinkManager {
developer_mode: bool,
is_elevated: bool,
}
#[cfg(windows)]
impl Default for WindowsSymlinkManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(windows)]
impl WindowsSymlinkManager {
pub fn new() -> Self {
Self {
developer_mode: Self::check_developer_mode(),
is_elevated: Self::check_elevation(),
}
}
fn check_developer_mode() -> bool {
let output = Command::new("reg")
.args([
"query",
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock",
"/v",
"AllowDevelopmentWithoutDevLicense",
])
.output();
if let Ok(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
return stdout.contains("0x1");
}
false
}
fn check_elevation() -> bool {
Command::new("net")
.args(["session"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn copy_file(&self, source: &Path, target: &Path) -> TwinResult<()> {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(source, target)?;
Ok(())
}
}
#[cfg(windows)]
impl SymlinkManager for WindowsSymlinkManager {
fn create_symlink(&self, source: &Path, target: &Path) -> TwinResult<SymlinkInfo> {
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
eprintln!(
"🔗 シンボリックリンク作成: {} -> {}",
target.display(),
source.display()
);
}
if !source.exists() {
return Err(TwinError::symlink(
format!("Source path does not exist: {}", source.display()),
Some(source.to_path_buf()),
));
}
if target.exists() {
fs::remove_file(target).ok();
fs::remove_dir(target).ok();
}
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
let result = if self.developer_mode || self.is_elevated {
#[cfg(windows)]
{
use std::os::windows::fs::{symlink_dir, symlink_file};
if source.is_dir() {
symlink_dir(source, target).map_err(|e| {
TwinError::symlink(
format!("Failed to create directory symlink: {e}"),
Some(target.to_path_buf()),
)
})
} else {
symlink_file(source, target).map_err(|e| {
TwinError::symlink(
format!("Failed to create file symlink: {e}"),
Some(target.to_path_buf()),
)
})
}
}
} else {
eprintln!(
"⚠️ Warning: Symbolic link creation requires Developer Mode or Administrator privileges"
);
eprintln!("⚠️ Falling back to file copy instead");
self.copy_file(source, target)
};
let mut info = SymlinkInfo::new(source.to_path_buf(), target.to_path_buf());
match result {
Ok(_) => {
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
eprintln!("✅ シンボリックリンク作成成功");
}
info.set_success();
Ok(info)
}
Err(e) => {
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
eprintln!("❌ シンボリックリンク作成失敗: {e}");
}
info.set_error(e.to_string());
Err(e)
}
}
}
fn remove_symlink(&self, path: &Path) -> TwinResult<()> {
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
eprintln!("🗑️ シンボリックリンク削除: {}", path.display());
}
if path.exists() {
let metadata = fs::symlink_metadata(path)?;
if metadata.is_dir() {
fs::remove_dir(path)?;
} else {
fs::remove_file(path)?;
}
if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
eprintln!("✅ シンボリックリンク削除成功");
}
}
Ok(())
}
#[allow(dead_code)]
fn validate_symlink(&self, path: &Path) -> TwinResult<bool> {
if !path.exists() {
return Ok(false);
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
let metadata = fs::symlink_metadata(path)?;
let attrs = metadata.file_attributes();
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
if attrs & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return match fs::metadata(path) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
};
}
}
Ok(false)
}
#[allow(dead_code)]
fn get_manual_instructions(&self, source: &Path, target: &Path) -> String {
if source.is_dir() {
format!(
"mklink /D \"{}\" \"{}\"",
target.display(),
source.display()
)
} else {
format!("mklink \"{}\" \"{}\"", target.display(), source.display())
}
}
}
pub fn create_symlink_manager() -> Box<dyn SymlinkManager> {
#[cfg(unix)]
{
Box::new(UnixSymlinkManager::new())
}
#[cfg(windows)]
{
Box::new(WindowsSymlinkManager::new())
}
}