1pub mod notify_rw_lock;
2pub mod tui;
3
4use crate::notify_rw_lock::{NotifyRwLock, NotifySender};
5use cargo_toml::Manifest;
6use crossbeam_channel::{unbounded, Receiver, Sender};
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::time::SystemTime;
10use uuid::Uuid;
11
12struct Job(PathBuf, Sender<Job>);
15
16pub struct Progress {
17 pub total: usize,
18 pub scanned: usize,
19}
20
21pub fn find_cargo_projects(
25 path: &Path,
26 mut num_threads: usize,
27 notify_tx: NotifySender,
28) -> (
29 Receiver<anyhow::Result<ProjectTargetAnalysis>>,
30 Arc<NotifyRwLock<Progress>>,
31) {
32 let progress = Arc::new(NotifyRwLock::new(
33 notify_tx,
34 Progress {
35 total: 1, scanned: 0,
37 },
38 ));
39 if num_threads == 0 {
40 num_threads = num_cpus::get();
41 }
42
43 let (result_tx, result_rx) = unbounded();
44 let path = path.to_owned();
45 std::thread::spawn({
46 let progress = progress.clone();
47 move || {
48 std::thread::scope(move |scope| {
49 let (job_tx, job_rx) = unbounded();
50
51 (0..num_threads)
52 .map(|_| (job_rx.clone(), result_tx.clone()))
53 .for_each(|(job_rx, result_tx)| {
54 scope.spawn({
55 let progress = progress.clone();
56 || {
57 job_rx.into_iter().for_each(move |job| {
58 find_cargo_projects_task(
59 job,
60 result_tx.clone(),
61 progress.clone(),
62 )
63 })
64 }
65 });
66 });
67
68 job_tx.clone().send(Job(path, job_tx)).unwrap();
69 });
70 }
71 });
72
73 (result_rx, progress)
74}
75
76fn find_cargo_projects_task(
81 job: Job,
82 results: Sender<anyhow::Result<ProjectTargetAnalysis>>,
83 progress: Arc<NotifyRwLock<Progress>>,
84) {
85 let path = job.0;
86 let job_sender = job.1;
87
88 let read_dir = match path.read_dir() {
89 Ok(it) => it,
90 Err(_e) => {
91 progress.write().scanned += 1;
92 return;
93 }
94 };
95
96 let (dirs, files): (Vec<_>, Vec<_>) = read_dir
97 .filter_map(|it| it.ok())
98 .partition(|it| it.file_type().is_ok_and(|t| t.is_dir()));
99 let dirs: Vec<_> = dirs.iter().map(|it| it.path()).collect();
100 let files: Vec<_> = files.iter().map(|it| it.path()).collect();
101
102 let has_cargo_toml = files
103 .iter()
104 .any(|it| it.file_name().unwrap_or_default().to_string_lossy() == "Cargo.toml");
105
106 for it in dirs {
108 let filename = it.file_name().unwrap_or_default().to_string_lossy();
109 match filename.as_ref() {
110 ".git" | ".cargo" => (),
114 _ => {
116 job_sender
117 .send(Job(it.to_path_buf(), job_sender.clone()))
118 .unwrap();
119 progress.write().total += 1;
120 }
121 }
122 }
123
124 if has_cargo_toml {
126 results.send(ProjectTargetAnalysis::analyze(&path)).unwrap();
127 }
128 progress.write().scanned += 1;
129}
130
131#[derive(Clone, Debug)]
132pub struct ProjectTargetAnalysis {
133 pub id: Uuid,
134 pub project_path: PathBuf,
136 pub project_name: Option<String>,
138 pub size: u64,
140 pub last_modified: SystemTime,
142 pub selected_for_cleanup: bool,
144}
145
146impl ProjectTargetAnalysis {
147 pub fn analyze(path: &Path) -> anyhow::Result<Self> {
149 let (size, last_modified) = Self::recursive_scan_target(path.join("target"));
150 let cargo_manifest = Manifest::from_path(path.join("Cargo.toml"))?;
151 Ok(Self {
152 id: Uuid::new_v4(),
153 project_path: path.to_owned(),
154 project_name: cargo_manifest.package.map(|p| p.name),
155 size,
156 last_modified,
157 selected_for_cleanup: false,
158 })
159 }
160
161 fn recursive_scan_target<T: AsRef<Path>>(path: T) -> (u64, SystemTime) {
163 let path = path.as_ref();
164
165 let default = (0, SystemTime::UNIX_EPOCH);
166
167 if !path.exists() {
168 return default;
169 }
170
171 match (path.is_file(), path.metadata()) {
172 (true, Ok(md)) => (md.len(), md.modified().unwrap_or(default.1)),
173 _ => path
174 .read_dir()
175 .map(|rd| {
176 rd.filter_map(|it| it.ok().map(|it| it.path()))
177 .map(Self::recursive_scan_target)
178 .fold(default, |a, b| (a.0 + b.0, a.1.max(b.1)))
179 })
180 .unwrap_or(default),
181 }
182 }
183}