Skip to main content

purger_core/
scanner.rs

1use anyhow::{Context, Result};
2use ignore::{DirEntry, WalkBuilder};
3use rayon::prelude::*;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
7use std::sync::{Arc, Mutex};
8use tracing::{debug, info, warn};
9
10use crate::filter::ProjectFilter;
11use crate::project::RustProject;
12
13/// 项目扫描器配置
14#[derive(Debug, Clone)]
15pub struct ScanConfig {
16    pub max_depth: Option<usize>,
17    pub follow_links: bool,
18    pub respect_gitignore: bool,
19    pub ignore_hidden: bool,
20    pub parallel: bool,
21
22    // 性能优化选项
23    /// 是否延迟计算目录大小(只在需要时计算)
24    pub lazy_size_calculation: bool,
25
26    // 过滤选项
27    /// 保留最近N天编译的项目(基于target目录的最后修改时间)
28    pub keep_days: Option<u32>,
29    /// 保留target目录小于指定大小的项目(字节)
30    pub keep_size: Option<u64>,
31    /// 忽略的路径列表(绝对路径或相对路径)
32    pub ignore_paths: Vec<PathBuf>,
33}
34
35impl Default for ScanConfig {
36    fn default() -> Self {
37        Self {
38            max_depth: Some(10), // 默认最大深度10层
39            follow_links: false,
40            respect_gitignore: true,
41            ignore_hidden: true,
42            parallel: true,
43
44            // 性能优化默认值
45            lazy_size_calculation: false, // 默认立即计算大小
46
47            // 过滤选项默认值
48            keep_days: None,
49            keep_size: None,
50            ignore_paths: Vec::new(),
51        }
52    }
53}
54
55/// Rust项目扫描器
56pub struct ProjectScanner {
57    config: ScanConfig,
58    // 简单的项目缓存,避免重复解析相同的项目
59    cache: Arc<Mutex<HashMap<PathBuf, RustProject>>>,
60}
61
62impl ProjectScanner {
63    /// 创建新的扫描器
64    pub fn new(config: ScanConfig) -> Self {
65        Self {
66            config,
67            cache: Arc::new(Mutex::new(HashMap::new())),
68        }
69    }
70
71    /// 扫描指定路径下的所有Rust项目
72    pub fn scan<P: AsRef<Path>>(&self, root_path: P) -> Result<Vec<RustProject>> {
73        self.scan_with_cancel_and_progress(root_path, None, None)
74    }
75
76    pub fn scan_with_cancel_and_progress<P: AsRef<Path>>(
77        &self,
78        root_path: P,
79        cancel_flag: Option<&AtomicBool>,
80        on_cargo_toml_found: Option<&(dyn Fn(usize) + Sync)>,
81    ) -> Result<Vec<RustProject>> {
82        let root_path = root_path.as_ref();
83        let start_time = std::time::Instant::now();
84        info!("开始扫描路径: {:?}", root_path);
85
86        if !root_path.exists() {
87            anyhow::bail!("路径不存在: {:?}", root_path);
88        }
89
90        if !root_path.is_dir() {
91            anyhow::bail!("路径不是目录: {:?}", root_path);
92        }
93
94        // 优化的文件遍历
95        let cargo_dirs = self.find_cargo_projects(root_path, cancel_flag, on_cargo_toml_found)?;
96        let find_time = start_time.elapsed();
97        info!(
98            "找到 {} 个Cargo.toml文件,耗时: {:?}",
99            cargo_dirs.len(),
100            find_time
101        );
102
103        if cancel_flag.is_some_and(|flag| flag.load(Ordering::Relaxed)) {
104            anyhow::bail!("扫描已取消");
105        }
106
107        // 并行或串行处理项目
108        let parse_start = std::time::Instant::now();
109        let projects = if self.config.parallel {
110            self.process_projects_parallel(cargo_dirs)?
111        } else {
112            self.process_projects_sequential(cargo_dirs)?
113        };
114        let parse_time = parse_start.elapsed();
115
116        info!(
117            "成功解析 {} 个Rust项目,耗时: {:?}",
118            projects.len(),
119            parse_time
120        );
121        info!("总扫描时间: {:?}", start_time.elapsed());
122
123        // 应用过滤器
124        let filtered_projects = self.apply_filters(projects);
125
126        Ok(filtered_projects)
127    }
128
129    /// 优化的Cargo项目查找方法
130    fn find_cargo_projects(
131        &self,
132        root_path: &Path,
133        cancel_flag: Option<&AtomicBool>,
134        on_cargo_toml_found: Option<&(dyn Fn(usize) + Sync)>,
135    ) -> Result<Vec<PathBuf>> {
136        let mut builder = WalkBuilder::new(root_path);
137        builder
138            .follow_links(self.config.follow_links)
139            .git_ignore(self.config.respect_gitignore)
140            .hidden(self.config.ignore_hidden);
141
142        if let Some(depth) = self.config.max_depth {
143            builder.max_depth(Some(depth));
144        }
145
146        // 启用并行遍历以提升性能
147        if self.config.parallel {
148            // 使用系统CPU核心数,但限制最大线程数避免过度并发
149            let thread_count = std::cmp::min(
150                std::thread::available_parallelism()
151                    .map(|n| n.get())
152                    .unwrap_or(4),
153                8,
154            );
155            builder.threads(thread_count);
156            debug!("启用并行文件遍历,线程数: {}", thread_count);
157        }
158
159        let walker = builder.build_parallel();
160        let cargo_dirs = std::sync::Mutex::new(Vec::new());
161        let found_count = AtomicUsize::new(0);
162
163        walker.run(|| {
164            let cargo_dirs = &cargo_dirs;
165            let found_count = &found_count;
166            Box::new(move |entry| {
167                if cancel_flag.is_some_and(|flag| flag.load(Ordering::Relaxed)) {
168                    return ignore::WalkState::Quit;
169                }
170
171                match entry {
172                    Ok(entry) => {
173                        if let Some(project_dir) =
174                            self.process_entry(entry, found_count, on_cargo_toml_found)
175                        {
176                            if let Ok(mut dirs) = cargo_dirs.lock() {
177                                dirs.push(project_dir);
178                            }
179                        }
180                    }
181                    Err(e) => {
182                        warn!("扫描错误: {}", e);
183                    }
184                }
185                ignore::WalkState::Continue
186            })
187        });
188
189        let cargo_dirs = cargo_dirs
190            .into_inner()
191            .unwrap_or_else(|poison| poison.into_inner());
192        Ok(cargo_dirs)
193    }
194
195    /// 处理单个目录条目
196    fn process_entry(
197        &self,
198        entry: DirEntry,
199        found_count: &AtomicUsize,
200        on_cargo_toml_found: Option<&(dyn Fn(usize) + Sync)>,
201    ) -> Option<PathBuf> {
202        let path = entry.path();
203
204        // 检查是否为Cargo.toml文件
205        if path.file_name()? == "Cargo.toml" && path.is_file() {
206            debug!("发现Cargo.toml: {:?}", path);
207            let count = found_count.fetch_add(1, Ordering::Relaxed) + 1;
208            if count % 50 == 0 {
209                if let Some(callback) = on_cargo_toml_found {
210                    callback(count);
211                }
212            }
213            return path.parent().map(|p| p.to_path_buf());
214        }
215
216        None
217    }
218
219    /// 并行处理项目(带缓存优化)
220    fn process_projects_parallel(&self, cargo_dirs: Vec<PathBuf>) -> Result<Vec<RustProject>> {
221        let cache = Arc::clone(&self.cache);
222        let lazy_size_calculation = self.config.lazy_size_calculation;
223
224        let projects: Vec<_> = cargo_dirs
225            .into_par_iter()
226            .filter_map(|dir| {
227                // 先检查缓存
228                if let Ok(cache_guard) = cache.lock() {
229                    if let Some(cached_project) = cache_guard.get(&dir) {
230                        debug!("从缓存获取项目: {}", cached_project.name);
231                        return Some(cached_project.clone());
232                    }
233                }
234
235                // 缓存未命中,解析项目
236                let project_result = if lazy_size_calculation {
237                    RustProject::from_path_lazy(&dir)
238                } else {
239                    RustProject::from_path(&dir)
240                };
241
242                match project_result {
243                    Ok(project) => {
244                        debug!("成功解析项目: {}", project.name);
245
246                        // 更新缓存
247                        if let Ok(mut cache_guard) = cache.lock() {
248                            cache_guard.insert(dir, project.clone());
249                        }
250
251                        Some(project)
252                    }
253                    Err(e) => {
254                        warn!("解析项目失败 {:?}: {}", dir, e);
255                        None
256                    }
257                }
258            })
259            .collect();
260
261        Ok(projects)
262    }
263
264    /// 串行处理项目
265    fn process_projects_sequential(&self, cargo_dirs: Vec<PathBuf>) -> Result<Vec<RustProject>> {
266        let mut projects = Vec::new();
267
268        for dir in cargo_dirs {
269            let project_result = if self.config.lazy_size_calculation {
270                RustProject::from_path_lazy(&dir)
271            } else {
272                RustProject::from_path(&dir)
273            };
274
275            match project_result {
276                Ok(project) => {
277                    debug!("成功解析项目: {}", project.name);
278                    projects.push(project);
279                }
280                Err(e) => {
281                    warn!("解析项目失败 {:?}: {}", dir, e);
282                    // 继续处理其他项目,不中断整个扫描过程
283                }
284            }
285        }
286
287        Ok(projects)
288    }
289
290    /// 扫描单个项目(用于验证特定路径)
291    pub fn scan_single<P: AsRef<Path>>(&self, project_path: P) -> Result<RustProject> {
292        let project_path = project_path.as_ref();
293
294        if !project_path.join("Cargo.toml").exists() {
295            anyhow::bail!("路径不是Rust项目: {:?}", project_path);
296        }
297
298        let project_result = if self.config.lazy_size_calculation {
299            RustProject::from_path_lazy(project_path)
300        } else {
301            RustProject::from_path(project_path)
302        };
303
304        project_result.context("解析Rust项目失败")
305    }
306
307    /// 过滤有target目录的项目
308    pub fn filter_with_target(projects: Vec<RustProject>) -> Vec<RustProject> {
309        projects.into_iter().filter(|p| p.has_target).collect()
310    }
311
312    /// 按大小排序项目(从大到小)
313    pub fn sort_by_size(mut projects: Vec<RustProject>) -> Vec<RustProject> {
314        projects.sort_by(|a, b| b.target_size.cmp(&a.target_size));
315        projects
316    }
317
318    /// 应用过滤器
319    fn apply_filters(&self, projects: Vec<RustProject>) -> Vec<RustProject> {
320        // 如果没有配置任何过滤条件,直接返回
321        if self.config.keep_days.is_none()
322            && self.config.keep_size.is_none()
323            && self.config.ignore_paths.is_empty()
324        {
325            return projects;
326        }
327
328        let filter = ProjectFilter::new(self.config.clone());
329        filter.filter_projects(projects)
330    }
331}
332
333impl Default for ProjectScanner {
334    fn default() -> Self {
335        Self::new(ScanConfig::default())
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use std::fs;
343    use std::time::SystemTime;
344    use tempfile::TempDir;
345
346    fn create_test_project(dir: &Path, name: &str, has_target: bool) -> Result<()> {
347        let project_dir = dir.join(name);
348        fs::create_dir_all(&project_dir)?;
349
350        let cargo_toml = format!(
351            r#"
352[package]
353name = "{name}"
354version = "0.1.0"
355edition = "2021"
356"#
357        );
358
359        fs::write(project_dir.join("Cargo.toml"), cargo_toml)?;
360
361        if has_target {
362            let target_dir = project_dir.join("target");
363            fs::create_dir_all(&target_dir)?;
364            fs::write(target_dir.join("test.txt"), "test content")?;
365        }
366
367        Ok(())
368    }
369
370    #[test]
371    fn test_scanner_basic() -> Result<()> {
372        let temp_dir = TempDir::new()?;
373        let root = temp_dir.path();
374
375        // 创建测试项目
376        create_test_project(root, "project1", true)?;
377        create_test_project(root, "project2", false)?;
378        create_test_project(root, "project3", true)?;
379
380        let scanner = ProjectScanner::default();
381        let projects = scanner.scan(root)?;
382
383        assert_eq!(projects.len(), 3);
384
385        let with_target = ProjectScanner::filter_with_target(projects);
386        assert_eq!(with_target.len(), 2);
387
388        Ok(())
389    }
390
391    #[test]
392    fn test_scan_single() -> Result<()> {
393        let temp_dir = TempDir::new()?;
394        let root = temp_dir.path();
395
396        create_test_project(root, "single_project", true)?;
397
398        let scanner = ProjectScanner::default();
399        let project = scanner.scan_single(root.join("single_project"))?;
400
401        assert_eq!(project.name, "single_project");
402        assert!(project.has_target);
403
404        Ok(())
405    }
406
407    #[test]
408    fn test_scan_with_max_depth() -> Result<()> {
409        let temp_dir = TempDir::new()?;
410        let root = temp_dir.path();
411
412        // 创建浅层项目(在根目录的子目录中)
413        create_test_project(root, "shallow_project", true)?;
414
415        // 创建深层项目
416        let deep_dir = root.join("level1").join("level2");
417        std::fs::create_dir_all(&deep_dir)?;
418        create_test_project(&deep_dir, "deep_project", true)?;
419
420        // 无深度限制,应该找到两个项目
421        let config = ScanConfig {
422            max_depth: None,
423            ..Default::default()
424        };
425        let scanner = ProjectScanner::new(config);
426        let projects = scanner.scan(root)?;
427        println!("无深度限制找到 {} 个项目", projects.len());
428        assert!(!projects.is_empty());
429
430        // 限制深度为2,应该找到浅层项目(在子目录中)
431        let config = ScanConfig {
432            max_depth: Some(2),
433            ..Default::default()
434        };
435        let scanner = ProjectScanner::new(config);
436        let projects = scanner.scan(root)?;
437        println!("深度限制2找到 {} 个项目", projects.len());
438
439        // 应该至少找到浅层项目
440        assert!(!projects.is_empty());
441
442        // 检查是否包含浅层项目
443        let has_shallow = projects.iter().any(|p| p.name == "shallow_project");
444        assert!(has_shallow, "应该包含浅层项目");
445
446        // 限制深度为1,应该找不到任何项目(因为项目都在子目录中)
447        let config = ScanConfig {
448            max_depth: Some(1),
449            ..Default::default()
450        };
451        let scanner = ProjectScanner::new(config);
452        let projects = scanner.scan(root)?;
453        println!("深度限制1找到 {} 个项目", projects.len());
454
455        // 深度1应该找不到项目,因为项目在子目录中
456        // 这个测试验证了深度限制确实在工作
457
458        Ok(())
459    }
460
461    #[test]
462    fn test_scan_parallel_vs_sequential() -> Result<()> {
463        let temp_dir = TempDir::new()?;
464        let root = temp_dir.path();
465
466        // 创建多个项目
467        for i in 0..5 {
468            create_test_project(root, &format!("project_{i}"), i % 2 == 0)?;
469        }
470
471        // 并行扫描
472        let config = ScanConfig {
473            parallel: true,
474            ..Default::default()
475        };
476        let scanner = ProjectScanner::new(config);
477        let parallel_projects = scanner.scan(root)?;
478
479        // 串行扫描
480        let config = ScanConfig {
481            parallel: false,
482            ..Default::default()
483        };
484        let scanner = ProjectScanner::new(config);
485        let sequential_projects = scanner.scan(root)?;
486
487        // 结果应该相同
488        assert_eq!(parallel_projects.len(), sequential_projects.len());
489        assert_eq!(parallel_projects.len(), 5);
490
491        Ok(())
492    }
493
494    #[test]
495    fn test_filter_with_target() {
496        let projects = vec![
497            RustProject {
498                path: PathBuf::from("/test1"),
499                name: "test1".to_string(),
500                target_size: 1000,
501                last_modified: SystemTime::now(),
502                is_workspace: false,
503                has_target: true,
504            },
505            RustProject {
506                path: PathBuf::from("/test2"),
507                name: "test2".to_string(),
508                target_size: 0,
509                last_modified: SystemTime::now(),
510                is_workspace: false,
511                has_target: false,
512            },
513        ];
514
515        let filtered = ProjectScanner::filter_with_target(projects);
516        assert_eq!(filtered.len(), 1);
517        assert_eq!(filtered[0].name, "test1");
518    }
519
520    #[test]
521    fn test_sort_by_size() {
522        let projects = vec![
523            RustProject {
524                path: PathBuf::from("/small"),
525                name: "small".to_string(),
526                target_size: 100,
527                last_modified: SystemTime::now(),
528                is_workspace: false,
529                has_target: true,
530            },
531            RustProject {
532                path: PathBuf::from("/large"),
533                name: "large".to_string(),
534                target_size: 1000,
535                last_modified: SystemTime::now(),
536                is_workspace: false,
537                has_target: true,
538            },
539            RustProject {
540                path: PathBuf::from("/medium"),
541                name: "medium".to_string(),
542                target_size: 500,
543                last_modified: SystemTime::now(),
544                is_workspace: false,
545                has_target: true,
546            },
547        ];
548
549        let sorted = ProjectScanner::sort_by_size(projects);
550        assert_eq!(sorted[0].name, "large");
551        assert_eq!(sorted[1].name, "medium");
552        assert_eq!(sorted[2].name, "small");
553    }
554
555    #[test]
556    fn test_scan_nonexistent_path() {
557        let scanner = ProjectScanner::default();
558        let result = scanner.scan("/nonexistent/path");
559        assert!(result.is_err());
560    }
561
562    #[test]
563    fn test_scan_single_invalid_project() {
564        let temp_dir = TempDir::new().unwrap();
565        let root = temp_dir.path();
566
567        // 创建一个没有Cargo.toml的目录
568        std::fs::create_dir_all(root.join("not_a_project")).unwrap();
569
570        let scanner = ProjectScanner::default();
571        let result = scanner.scan_single(root.join("not_a_project"));
572        assert!(result.is_err());
573    }
574
575    #[test]
576    fn test_scan_permission_denied() {
577        let scanner = ProjectScanner::default();
578
579        // 在Windows上,尝试访问系统保护的目录
580        #[cfg(windows)]
581        {
582            let result = scanner.scan(std::path::Path::new("C:\\System Volume Information"));
583            // 这应该失败或返回空结果
584            if let Ok(projects) = result {
585                assert!(projects.is_empty());
586            }
587            // 权限错误是预期的
588        }
589
590        // 在Unix系统上测试
591        #[cfg(unix)]
592        {
593            let result = scanner.scan(std::path::Path::new("/root"));
594            if let Ok(projects) = result {
595                assert!(projects.is_empty());
596            }
597            // 权限错误是预期的
598        }
599    }
600
601    #[test]
602    fn test_scan_corrupted_cargo_toml() -> Result<()> {
603        let temp_dir = TempDir::new()?;
604        let root = temp_dir.path();
605
606        // 创建一个有损坏Cargo.toml的项目
607        let project_path = root.join("corrupted_project");
608        std::fs::create_dir_all(&project_path)?;
609
610        // 写入无效的TOML
611        std::fs::write(project_path.join("Cargo.toml"), "invalid toml content [[[[")?;
612
613        let scanner = ProjectScanner::default();
614        let project = scanner.scan_single(&project_path)?;
615        assert_eq!(project.name, "corrupted_project");
616        assert!(!project.is_workspace);
617        assert!(!project.has_target);
618
619        Ok(())
620    }
621
622    #[test]
623    fn test_scan_includes_corrupted_manifest() -> Result<()> {
624        let temp_dir = TempDir::new()?;
625        let root = temp_dir.path();
626
627        create_test_project(root, "good_project", true)?;
628
629        let corrupted_path = root.join("corrupted_project");
630        std::fs::create_dir_all(&corrupted_path)?;
631        std::fs::write(
632            corrupted_path.join("Cargo.toml"),
633            "invalid toml content [[[[",
634        )?;
635
636        let scanner = ProjectScanner::default();
637        let projects = scanner.scan(root)?;
638
639        assert_eq!(projects.len(), 2);
640        assert!(projects.iter().any(|p| p.name == "good_project"));
641        assert!(projects.iter().any(|p| p.name == "corrupted_project"));
642
643        Ok(())
644    }
645
646    #[test]
647    fn test_scan_empty_directory() -> Result<()> {
648        let temp_dir = TempDir::new()?;
649        let root = temp_dir.path();
650
651        let scanner = ProjectScanner::default();
652        let projects = scanner.scan(root)?;
653
654        // 空目录应该返回空的项目列表
655        assert!(projects.is_empty());
656
657        Ok(())
658    }
659
660    #[test]
661    fn test_scan_very_deep_directory() -> Result<()> {
662        let temp_dir = TempDir::new()?;
663        let mut current_path = temp_dir.path().to_path_buf();
664
665        // 创建一个深的目录结构(但不要太深,避免超过默认深度限制)
666        for i in 0..5 {
667            current_path = current_path.join(format!("level_{i}"));
668            std::fs::create_dir_all(&current_path)?;
669        }
670
671        // 在深层创建一个项目
672        create_test_project(&current_path, "deep_project", true)?;
673
674        // 使用足够大的深度限制
675        let config = ScanConfig {
676            max_depth: Some(20), // 足够深
677            ..Default::default()
678        };
679        let scanner = ProjectScanner::new(config);
680        let projects = scanner.scan(temp_dir.path())?;
681
682        // 应该能找到深层的项目
683        assert!(!projects.is_empty());
684        let has_deep = projects.iter().any(|p| p.name == "deep_project");
685        assert!(has_deep, "应该找到深层项目");
686
687        Ok(())
688    }
689}