Skip to main content

twin_cli/
symlink.rs

1/// シンボリックリンク管理モジュール
2///
3/// このモジュールの役割:
4/// - クロスプラットフォーム対応のシンボリックリンク作成
5/// - Unix: ln -s コマンドのラッパー
6/// - Windows: 開発者モード対応のmklinkラッパー(フォールバック機能付き)
7/// - リンクの検証と削除
8use crate::core::{SymlinkInfo, TwinError, TwinResult};
9use std::fs;
10use std::path::Path;
11#[cfg(windows)]
12use std::process::Command;
13
14/// プラットフォーム共通のトレイト
15pub trait SymlinkManager {
16    /// シンボリックリンクを作成
17    fn create_symlink(&self, source: &Path, target: &Path) -> TwinResult<SymlinkInfo>;
18
19    /// シンボリックリンクを削除
20    fn remove_symlink(&self, path: &Path) -> TwinResult<()>;
21
22    /// シンボリックリンクを検証
23    #[allow(dead_code)]
24    fn validate_symlink(&self, path: &Path) -> TwinResult<bool>;
25
26    /// 手動作成方法の説明を取得
27    #[allow(dead_code)]
28    fn get_manual_instructions(&self, source: &Path, target: &Path) -> String;
29}
30
31/// プラットフォーム別の実装を選択
32#[cfg(unix)]
33#[allow(dead_code)]
34pub type PlatformSymlinkManager = UnixSymlinkManager;
35
36#[cfg(windows)]
37#[allow(dead_code)]
38pub type PlatformSymlinkManager = WindowsSymlinkManager;
39
40/// Unix系OS用の実装
41#[cfg(unix)]
42pub struct UnixSymlinkManager;
43
44#[cfg(unix)]
45impl UnixSymlinkManager {
46    pub fn new() -> Self {
47        Self
48    }
49}
50
51#[cfg(unix)]
52impl Default for UnixSymlinkManager {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58#[cfg(unix)]
59impl SymlinkManager for UnixSymlinkManager {
60    fn create_symlink(&self, source: &Path, target: &Path) -> TwinResult<SymlinkInfo> {
61        // 透明性のあるコマンド実行ログ
62        if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
63            eprintln!(
64                "🔗 シンボリックリンク作成: {} -> {}",
65                target.display(),
66                source.display()
67            );
68        }
69
70        // ソースファイルの存在チェック
71        if !source.exists() {
72            return Err(TwinError::symlink(
73                format!("Source path does not exist: {}", source.display()),
74                Some(source.to_path_buf()),
75            ));
76        }
77
78        // 既存のリンクやファイルがある場合は削除
79        if target.exists() || target.is_symlink() {
80            fs::remove_file(target).ok();
81        }
82
83        // 親ディレクトリを作成
84        if let Some(parent) = target.parent() {
85            fs::create_dir_all(parent)?;
86        }
87
88        // シンボリックリンクを作成
89        #[cfg(unix)]
90        {
91            use std::os::unix::fs::symlink;
92            match symlink(source, target) {
93                Ok(_) => {
94                    if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok()
95                    {
96                        eprintln!("✅ シンボリックリンク作成成功");
97                    }
98                    let mut info = SymlinkInfo::new(source.to_path_buf(), target.to_path_buf());
99                    info.set_success();
100                    Ok(info)
101                }
102                Err(e) => {
103                    if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok()
104                    {
105                        eprintln!("❌ シンボリックリンク作成失敗: {}", e);
106                    }
107                    let mut info = SymlinkInfo::new(source.to_path_buf(), target.to_path_buf());
108                    info.set_error(format!("Failed to create symlink: {}", e));
109                    Err(TwinError::symlink(
110                        format!("Failed to create symlink: {}", e),
111                        Some(target.to_path_buf()),
112                    ))
113                }
114            }
115        }
116    }
117
118    fn remove_symlink(&self, path: &Path) -> TwinResult<()> {
119        if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
120            eprintln!("🗑️  シンボリックリンク削除: {}", path.display());
121        }
122
123        if path.is_symlink() {
124            fs::remove_file(path)?;
125            if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
126                eprintln!("✅ シンボリックリンク削除成功");
127            }
128        }
129        Ok(())
130    }
131
132    #[allow(dead_code)]
133    fn validate_symlink(&self, path: &Path) -> TwinResult<bool> {
134        if !path.exists() {
135            return Ok(false);
136        }
137
138        // シンボリックリンクかどうか確認
139        let metadata = fs::symlink_metadata(path)?;
140        if !metadata.file_type().is_symlink() {
141            return Ok(false);
142        }
143
144        // リンク先が存在するか確認
145        match fs::metadata(path) {
146            Ok(_) => Ok(true),
147            Err(_) => Ok(false), // 壊れたリンク
148        }
149    }
150
151    #[allow(dead_code)]
152    fn get_manual_instructions(&self, source: &Path, target: &Path) -> String {
153        format!(
154            "To manually create the symlink, run:\n  ln -s \"{}\" \"{}\"",
155            source.display(),
156            target.display()
157        )
158    }
159}
160
161/// Windows用の実装
162#[cfg(windows)]
163pub struct WindowsSymlinkManager {
164    /// 開発者モードが有効かどうか
165    developer_mode: bool,
166    /// 管理者権限で実行されているか
167    is_elevated: bool,
168}
169
170#[cfg(windows)]
171impl Default for WindowsSymlinkManager {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177#[cfg(windows)]
178impl WindowsSymlinkManager {
179    pub fn new() -> Self {
180        Self {
181            developer_mode: Self::check_developer_mode(),
182            is_elevated: Self::check_elevation(),
183        }
184    }
185
186    /// 開発者モードが有効か確認
187    fn check_developer_mode() -> bool {
188        // レジストリをチェック
189        let output = Command::new("reg")
190            .args([
191                "query",
192                "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock",
193                "/v",
194                "AllowDevelopmentWithoutDevLicense",
195            ])
196            .output();
197
198        if let Ok(output) = output {
199            let stdout = String::from_utf8_lossy(&output.stdout);
200            return stdout.contains("0x1");
201        }
202
203        false
204    }
205
206    /// 管理者権限で実行されているか確認
207    fn check_elevation() -> bool {
208        // 管理者権限が必要な操作を試みる
209        Command::new("net")
210            .args(["session"])
211            .output()
212            .map(|o| o.status.success())
213            .unwrap_or(false)
214    }
215
216    /// ファイルをコピー
217    fn copy_file(&self, source: &Path, target: &Path) -> TwinResult<()> {
218        if let Some(parent) = target.parent() {
219            fs::create_dir_all(parent)?;
220        }
221
222        fs::copy(source, target)?;
223        Ok(())
224    }
225}
226
227#[cfg(windows)]
228impl SymlinkManager for WindowsSymlinkManager {
229    fn create_symlink(&self, source: &Path, target: &Path) -> TwinResult<SymlinkInfo> {
230        // 透明性のあるコマンド実行ログ
231        if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
232            eprintln!(
233                "🔗 シンボリックリンク作成: {} -> {}",
234                target.display(),
235                source.display()
236            );
237        }
238
239        // ソースファイルの存在チェック
240        if !source.exists() {
241            return Err(TwinError::symlink(
242                format!("Source path does not exist: {}", source.display()),
243                Some(source.to_path_buf()),
244            ));
245        }
246
247        // 既存のファイルを削除
248        if target.exists() {
249            fs::remove_file(target).ok();
250            fs::remove_dir(target).ok();
251        }
252
253        // 親ディレクトリを作成
254        if let Some(parent) = target.parent() {
255            fs::create_dir_all(parent)?;
256        }
257
258        // 開発者モードまたは管理者権限があればシンボリックリンクを作成
259        let result = if self.developer_mode || self.is_elevated {
260            // 標準ライブラリのAPI を使用
261            #[cfg(windows)]
262            {
263                use std::os::windows::fs::{symlink_dir, symlink_file};
264                if source.is_dir() {
265                    symlink_dir(source, target).map_err(|e| {
266                        TwinError::symlink(
267                            format!("Failed to create directory symlink: {e}"),
268                            Some(target.to_path_buf()),
269                        )
270                    })
271                } else {
272                    symlink_file(source, target).map_err(|e| {
273                        TwinError::symlink(
274                            format!("Failed to create file symlink: {e}"),
275                            Some(target.to_path_buf()),
276                        )
277                    })
278                }
279            }
280        } else {
281            // 開発者モードが無効な場合、エラーメッセージを表示してコピー
282            eprintln!(
283                "⚠️  Warning: Symbolic link creation requires Developer Mode or Administrator privileges"
284            );
285            eprintln!("⚠️  Falling back to file copy instead");
286            self.copy_file(source, target)
287        };
288
289        let mut info = SymlinkInfo::new(source.to_path_buf(), target.to_path_buf());
290
291        match result {
292            Ok(_) => {
293                if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
294                    eprintln!("✅ シンボリックリンク作成成功");
295                }
296                info.set_success();
297                Ok(info)
298            }
299            Err(e) => {
300                if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
301                    eprintln!("❌ シンボリックリンク作成失敗: {e}");
302                }
303                info.set_error(e.to_string());
304                Err(e)
305            }
306        }
307    }
308
309    fn remove_symlink(&self, path: &Path) -> TwinResult<()> {
310        if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
311            eprintln!("🗑️  シンボリックリンク削除: {}", path.display());
312        }
313
314        if path.exists() {
315            let metadata = fs::symlink_metadata(path)?;
316            if metadata.is_dir() {
317                fs::remove_dir(path)?;
318            } else {
319                fs::remove_file(path)?;
320            }
321            if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
322                eprintln!("✅ シンボリックリンク削除成功");
323            }
324        }
325        Ok(())
326    }
327
328    #[allow(dead_code)]
329    fn validate_symlink(&self, path: &Path) -> TwinResult<bool> {
330        if !path.exists() {
331            return Ok(false);
332        }
333
334        // シンボリックリンクかジャンクションか確認
335        #[cfg(windows)]
336        {
337            use std::os::windows::fs::MetadataExt;
338            let metadata = fs::symlink_metadata(path)?;
339            let attrs = metadata.file_attributes();
340
341            // FILE_ATTRIBUTE_REPARSE_POINT をチェック
342            const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
343            if attrs & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
344                // リンク先が存在するか確認
345                return match fs::metadata(path) {
346                    Ok(_) => Ok(true),
347                    Err(_) => Ok(false),
348                };
349            }
350        }
351
352        Ok(false)
353    }
354
355    #[allow(dead_code)]
356    fn get_manual_instructions(&self, source: &Path, target: &Path) -> String {
357        if source.is_dir() {
358            format!(
359                "mklink /D \"{}\" \"{}\"",
360                target.display(),
361                source.display()
362            )
363        } else {
364            format!("mklink \"{}\" \"{}\"", target.display(), source.display())
365        }
366    }
367}
368
369/// ファクトリ関数
370pub fn create_symlink_manager() -> Box<dyn SymlinkManager> {
371    #[cfg(unix)]
372    {
373        Box::new(UnixSymlinkManager::new())
374    }
375
376    #[cfg(windows)]
377    {
378        Box::new(WindowsSymlinkManager::new())
379    }
380}