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#[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 pub lazy_size_calculation: bool,
25
26 pub keep_days: Option<u32>,
29 pub keep_size: Option<u64>,
31 pub ignore_paths: Vec<PathBuf>,
33}
34
35impl Default for ScanConfig {
36 fn default() -> Self {
37 Self {
38 max_depth: Some(10), follow_links: false,
40 respect_gitignore: true,
41 ignore_hidden: true,
42 parallel: true,
43
44 lazy_size_calculation: false, keep_days: None,
49 keep_size: None,
50 ignore_paths: Vec::new(),
51 }
52 }
53}
54
55pub struct ProjectScanner {
57 config: ScanConfig,
58 cache: Arc<Mutex<HashMap<PathBuf, RustProject>>>,
60}
61
62impl ProjectScanner {
63 pub fn new(config: ScanConfig) -> Self {
65 Self {
66 config,
67 cache: Arc::new(Mutex::new(HashMap::new())),
68 }
69 }
70
71 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 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 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 let filtered_projects = self.apply_filters(projects);
125
126 Ok(filtered_projects)
127 }
128
129 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 if self.config.parallel {
148 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 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 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 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 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 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 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 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 }
284 }
285 }
286
287 Ok(projects)
288 }
289
290 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 pub fn filter_with_target(projects: Vec<RustProject>) -> Vec<RustProject> {
309 projects.into_iter().filter(|p| p.has_target).collect()
310 }
311
312 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 fn apply_filters(&self, projects: Vec<RustProject>) -> Vec<RustProject> {
320 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 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 create_test_project(root, "shallow_project", true)?;
414
415 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 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 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 assert!(!projects.is_empty());
441
442 let has_shallow = projects.iter().any(|p| p.name == "shallow_project");
444 assert!(has_shallow, "应该包含浅层项目");
445
446 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 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 for i in 0..5 {
468 create_test_project(root, &format!("project_{i}"), i % 2 == 0)?;
469 }
470
471 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 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 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 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 #[cfg(windows)]
581 {
582 let result = scanner.scan(std::path::Path::new("C:\\System Volume Information"));
583 if let Ok(projects) = result {
585 assert!(projects.is_empty());
586 }
587 }
589
590 #[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 }
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 let project_path = root.join("corrupted_project");
608 std::fs::create_dir_all(&project_path)?;
609
610 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 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 for i in 0..5 {
667 current_path = current_path.join(format!("level_{i}"));
668 std::fs::create_dir_all(¤t_path)?;
669 }
670
671 create_test_project(¤t_path, "deep_project", true)?;
673
674 let config = ScanConfig {
676 max_depth: Some(20), ..Default::default()
678 };
679 let scanner = ProjectScanner::new(config);
680 let projects = scanner.scan(temp_dir.path())?;
681
682 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}