1use std::path::PathBuf;
2use std::time::{Instant, SystemTime};
3
4use tokio_util::sync::CancellationToken;
5
6use crate::error::Result;
7
8use super::error_strategy::AnalysisErrorStrategy;
9use super::filter::AnalysisFilter;
10use super::handle::AnalysisHandle;
11use super::progress::AnalysisProgressReporter;
12use super::report::{AnalysisReport, DEFAULT_MAX_REPORTED_ERRORS};
13use super::walk::{walk, WalkParams};
14
15#[cfg(feature = "checksum")]
16use super::hash::detect_duplicates;
17#[cfg(feature = "checksum")]
18use super::report::DEFAULT_MAX_REPORTED_DUPLICATE_GROUPS;
19
20pub const DEFAULT_TOP_N_LARGEST: usize = 10;
22
23pub struct AnalyzeBuilder {
24 root: PathBuf,
25 filter: AnalysisFilter,
26 error_strategy: AnalysisErrorStrategy,
27 max_depth: Option<usize>,
28 follow_symlinks: bool,
29 top_n_largest: usize,
30 detect_mime_types: bool,
31 max_reported_errors: usize,
32 #[cfg(feature = "checksum")]
33 detect_duplicates: bool,
34 #[cfg(feature = "checksum")]
35 hash_concurrency: Option<usize>,
36 #[cfg(feature = "checksum")]
37 max_reported_duplicates: usize,
38}
39
40impl AnalyzeBuilder {
41 pub(crate) fn new(root: impl Into<PathBuf>) -> Self {
42 Self {
43 root: root.into(),
44 filter: AnalysisFilter::default(),
45 error_strategy: AnalysisErrorStrategy::default(),
46 max_depth: None,
47 follow_symlinks: false,
48 top_n_largest: DEFAULT_TOP_N_LARGEST,
49 detect_mime_types: false,
50 max_reported_errors: DEFAULT_MAX_REPORTED_ERRORS,
51 #[cfg(feature = "checksum")]
52 detect_duplicates: false,
53 #[cfg(feature = "checksum")]
54 hash_concurrency: None,
55 #[cfg(feature = "checksum")]
56 max_reported_duplicates: DEFAULT_MAX_REPORTED_DUPLICATE_GROUPS,
57 }
58 }
59
60 pub fn extensions(mut self, exts: impl IntoIterator<Item = impl Into<String>>) -> Self {
64 self.filter.extensions = Some(exts.into_iter().map(Into::into).collect());
65 self
66 }
67
68 pub fn exclude_globs(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
72 self.filter.exclude_patterns = patterns.into_iter().map(Into::into).collect();
73 self
74 }
75
76 pub fn min_size(mut self, bytes: u64) -> Self {
77 self.filter.min_size = Some(bytes);
78 self
79 }
80
81 pub fn max_size(mut self, bytes: u64) -> Self {
82 self.filter.max_size = Some(bytes);
83 self
84 }
85
86 pub fn modified_after(mut self, t: SystemTime) -> Self {
89 self.filter.modified_after = Some(t);
90 self
91 }
92
93 pub fn modified_before(mut self, t: SystemTime) -> Self {
96 self.filter.modified_before = Some(t);
97 self
98 }
99
100 pub fn max_depth(mut self, depth: usize) -> Self {
105 self.max_depth = Some(depth);
106 self
107 }
108
109 pub fn follow_symlinks(mut self, follow: bool) -> Self {
113 self.follow_symlinks = follow;
114 self
115 }
116
117 pub fn top_n_largest(mut self, n: usize) -> Self {
121 self.top_n_largest = n;
122 self
123 }
124
125 pub fn detect_mime_types(mut self, enable: bool) -> Self {
129 self.detect_mime_types = enable;
130 self
131 }
132
133 pub fn on_error(mut self, strategy: AnalysisErrorStrategy) -> Self {
134 self.error_strategy = strategy;
135 self
136 }
137
138 pub fn max_reported_errors(mut self, n: usize) -> Self {
141 self.max_reported_errors = n;
142 self
143 }
144
145 #[cfg(feature = "checksum")]
148 pub fn detect_duplicates(mut self, enable: bool) -> Self {
149 self.detect_duplicates = enable;
150 self
151 }
152
153 #[cfg(feature = "checksum")]
156 pub fn hash_concurrency(mut self, n: usize) -> Self {
157 self.hash_concurrency = Some(n);
158 self
159 }
160
161 #[cfg(feature = "checksum")]
164 pub fn max_reported_duplicates(mut self, n: usize) -> Self {
165 self.max_reported_duplicates = n;
166 self
167 }
168
169 pub fn start(self) -> Result<AnalysisHandle> {
170 let cancel = CancellationToken::new();
171 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
172 let reporter = AnalysisProgressReporter::new(tx);
173 let cancel_for_task = cancel.clone();
174
175 #[cfg(feature = "checksum")]
176 let detect_duplicates_enabled = self.detect_duplicates;
177
178 let params = WalkParams {
179 root: self.root,
180 filter: self.filter,
181 error_strategy: self.error_strategy,
182 max_depth: self.max_depth,
183 follow_symlinks: self.follow_symlinks,
184 top_n_largest: self.top_n_largest,
185 detect_mime_types: self.detect_mime_types,
186 #[cfg(feature = "checksum")]
187 collect_duplicate_candidates: detect_duplicates_enabled,
188 max_reported_errors: self.max_reported_errors,
189 };
190
191 #[cfg(feature = "checksum")]
192 let error_strategy = self.error_strategy;
193 #[cfg(feature = "checksum")]
194 let hash_concurrency = self.hash_concurrency;
195 #[cfg(feature = "checksum")]
196 let max_reported_duplicates = self.max_reported_duplicates;
197 #[cfg(feature = "checksum")]
198 let max_reported_errors = self.max_reported_errors;
199
200 let join_handle = tokio::spawn(async move {
201 let started = Instant::now();
202 let outcome = walk(params, cancel_for_task.clone(), reporter.clone()).await?;
203
204 #[cfg(feature = "checksum")]
205 let (duplicates, duplicate_groups_total, duplicate_bytes_wasted, errors, errors_total) = {
206 let mut errors = outcome.errors;
207 let mut errors_total = outcome.errors_total;
208 if detect_duplicates_enabled {
209 let hash_outcome = detect_duplicates(
210 outcome.duplicate_candidates,
211 hash_concurrency,
212 max_reported_duplicates,
213 max_reported_errors.saturating_sub(errors.len()),
214 error_strategy,
215 &cancel_for_task,
216 &reporter,
217 )
218 .await?;
219 errors.extend(hash_outcome.errors);
220 errors_total += hash_outcome.errors_total;
221 (
222 hash_outcome.groups,
223 hash_outcome.groups_total,
224 hash_outcome.bytes_wasted,
225 errors,
226 errors_total,
227 )
228 } else {
229 (Vec::new(), 0, 0, errors, errors_total)
230 }
231 };
232 #[cfg(not(feature = "checksum"))]
233 let (errors, errors_total) = (outcome.errors, outcome.errors_total);
234
235 Ok(AnalysisReport {
236 file_count: outcome.file_count,
237 dir_count: outcome.dir_count,
238 total_size: outcome.total_size,
239 largest_files: outcome.largest_files,
240 by_extension: outcome.by_extension,
241 by_mime: outcome.by_mime,
242 age_buckets: outcome.age_buckets,
243 errors,
244 errors_total,
245 #[cfg(feature = "checksum")]
246 duplicates,
247 #[cfg(feature = "checksum")]
248 duplicate_groups_total,
249 #[cfg(feature = "checksum")]
250 duplicate_bytes_wasted,
251 duration: started.elapsed(),
252 })
253 });
254
255 Ok(AnalysisHandle::new(join_handle, rx, cancel))
256 }
257}