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::util::default_concurrency;
14use super::walk::{walk, WalkParams};
15
16#[cfg(feature = "checksum")]
17use super::hash::detect_duplicates;
18#[cfg(feature = "checksum")]
19use super::report::DEFAULT_MAX_REPORTED_DUPLICATE_GROUPS;
20
21pub const DEFAULT_TOP_N_LARGEST: usize = 10;
23
24pub struct AnalyzeBuilder {
25 root: PathBuf,
26 filter: AnalysisFilter,
27 error_strategy: AnalysisErrorStrategy,
28 max_depth: Option<usize>,
29 follow_symlinks: bool,
30 top_n_largest: usize,
31 detect_mime_types: bool,
32 max_reported_errors: usize,
33 walk_concurrency: Option<usize>,
34 estimate_total: bool,
35 #[cfg(feature = "checksum")]
36 detect_duplicates: bool,
37 #[cfg(feature = "checksum")]
38 hash_concurrency: Option<usize>,
39 #[cfg(feature = "checksum")]
40 max_reported_duplicates: usize,
41}
42
43impl AnalyzeBuilder {
44 pub(crate) fn new(root: impl Into<PathBuf>) -> Self {
45 Self {
46 root: root.into(),
47 filter: AnalysisFilter::default(),
48 error_strategy: AnalysisErrorStrategy::default(),
49 max_depth: None,
50 follow_symlinks: false,
51 top_n_largest: DEFAULT_TOP_N_LARGEST,
52 detect_mime_types: false,
53 max_reported_errors: DEFAULT_MAX_REPORTED_ERRORS,
54 walk_concurrency: None,
55 estimate_total: false,
56 #[cfg(feature = "checksum")]
57 detect_duplicates: false,
58 #[cfg(feature = "checksum")]
59 hash_concurrency: None,
60 #[cfg(feature = "checksum")]
61 max_reported_duplicates: DEFAULT_MAX_REPORTED_DUPLICATE_GROUPS,
62 }
63 }
64
65 pub fn extensions(mut self, exts: impl IntoIterator<Item = impl Into<String>>) -> Self {
73 self.filter.extensions = Some(exts.into_iter().map(Into::into).collect());
74 self
75 }
76
77 pub fn exclude_globs(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
83 self.filter.exclude_patterns = patterns.into_iter().map(Into::into).collect();
84 self
85 }
86
87 pub fn min_size(mut self, bytes: u64) -> Self {
91 self.filter.min_size = Some(bytes);
92 self
93 }
94
95 pub fn max_size(mut self, bytes: u64) -> Self {
99 self.filter.max_size = Some(bytes);
100 self
101 }
102
103 pub fn modified_after(mut self, t: SystemTime) -> Self {
110 self.filter.modified_after = Some(t);
111 self
112 }
113
114 pub fn modified_before(mut self, t: SystemTime) -> Self {
121 self.filter.modified_before = Some(t);
122 self
123 }
124
125 pub fn max_depth(mut self, depth: usize) -> Self {
130 self.max_depth = Some(depth);
131 self
132 }
133
134 pub fn follow_symlinks(mut self, follow: bool) -> Self {
138 self.follow_symlinks = follow;
139 self
140 }
141
142 pub fn walk_concurrency(mut self, n: usize) -> Self {
152 self.walk_concurrency = Some(n);
153 self
154 }
155
156 pub fn estimate_total(mut self, enable: bool) -> Self {
164 self.estimate_total = enable;
165 self
166 }
167
168 pub fn top_n_largest(mut self, n: usize) -> Self {
172 self.top_n_largest = n;
173 self
174 }
175
176 pub fn detect_mime_types(mut self, enable: bool) -> Self {
180 self.detect_mime_types = enable;
181 self
182 }
183
184 pub fn on_error(mut self, strategy: AnalysisErrorStrategy) -> Self {
185 self.error_strategy = strategy;
186 self
187 }
188
189 pub fn max_reported_errors(mut self, n: usize) -> Self {
192 self.max_reported_errors = n;
193 self
194 }
195
196 #[cfg(feature = "checksum")]
199 pub fn detect_duplicates(mut self, enable: bool) -> Self {
200 self.detect_duplicates = enable;
201 self
202 }
203
204 #[cfg(feature = "checksum")]
207 pub fn hash_concurrency(mut self, n: usize) -> Self {
208 self.hash_concurrency = Some(n);
209 self
210 }
211
212 #[cfg(feature = "checksum")]
215 pub fn max_reported_duplicates(mut self, n: usize) -> Self {
216 self.max_reported_duplicates = n;
217 self
218 }
219
220 pub fn start(self) -> Result<AnalysisHandle> {
221 let cancel = CancellationToken::new();
222 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
223 let reporter = AnalysisProgressReporter::new(tx);
224 let cancel_for_task = cancel.clone();
225
226 #[cfg(feature = "checksum")]
227 let detect_duplicates_enabled = self.detect_duplicates;
228
229 let params = WalkParams {
230 root: self.root,
231 filter: self.filter,
232 error_strategy: self.error_strategy,
233 max_depth: self.max_depth,
234 follow_symlinks: self.follow_symlinks,
235 top_n_largest: self.top_n_largest,
236 detect_mime_types: self.detect_mime_types,
237 #[cfg(feature = "checksum")]
238 collect_duplicate_candidates: detect_duplicates_enabled,
239 max_reported_errors: self.max_reported_errors,
240 walk_concurrency: self.walk_concurrency.unwrap_or_else(default_concurrency),
241 estimate_total: self.estimate_total,
242 };
243
244 #[cfg(feature = "checksum")]
245 let error_strategy = self.error_strategy;
246 #[cfg(feature = "checksum")]
247 let hash_concurrency = self.hash_concurrency;
248 #[cfg(feature = "checksum")]
249 let max_reported_duplicates = self.max_reported_duplicates;
250 #[cfg(feature = "checksum")]
251 let max_reported_errors = self.max_reported_errors;
252
253 let join_handle = tokio::spawn(async move {
254 let started = Instant::now();
255 let outcome = walk(params, cancel_for_task.clone(), reporter.clone()).await?;
256
257 #[cfg(feature = "checksum")]
258 let (duplicates, duplicate_groups_total, duplicate_bytes_wasted, errors, errors_total) = {
259 let mut errors = outcome.errors;
260 let mut errors_total = outcome.errors_total;
261 if detect_duplicates_enabled {
262 let hash_outcome = detect_duplicates(
263 outcome.duplicate_candidates,
264 hash_concurrency,
265 max_reported_duplicates,
266 max_reported_errors.saturating_sub(errors.len()),
267 error_strategy,
268 &cancel_for_task,
269 &reporter,
270 )
271 .await?;
272 errors.extend(hash_outcome.errors);
273 errors_total += hash_outcome.errors_total;
274 (
275 hash_outcome.groups,
276 hash_outcome.groups_total,
277 hash_outcome.bytes_wasted,
278 errors,
279 errors_total,
280 )
281 } else {
282 (Vec::new(), 0, 0, errors, errors_total)
283 }
284 };
285 #[cfg(not(feature = "checksum"))]
286 let (errors, errors_total) = (outcome.errors, outcome.errors_total);
287
288 Ok(AnalysisReport {
289 file_count: outcome.file_count,
290 dir_count: outcome.dir_count,
291 total_size: outcome.total_size,
292 largest_files: outcome.largest_files,
293 by_extension: outcome.by_extension,
294 by_mime: outcome.by_mime,
295 age_buckets: outcome.age_buckets,
296 errors,
297 errors_total,
298 #[cfg(feature = "checksum")]
299 duplicates,
300 #[cfg(feature = "checksum")]
301 duplicate_groups_total,
302 #[cfg(feature = "checksum")]
303 duplicate_bytes_wasted,
304 duration: started.elapsed(),
305 })
306 });
307
308 Ok(AnalysisHandle::new(join_handle, rx, cancel))
309 }
310}