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 #[cfg(feature = "checksum")]
35 detect_duplicates: bool,
36 #[cfg(feature = "checksum")]
37 hash_concurrency: Option<usize>,
38 #[cfg(feature = "checksum")]
39 max_reported_duplicates: usize,
40}
41
42impl AnalyzeBuilder {
43 pub(crate) fn new(root: impl Into<PathBuf>) -> Self {
44 Self {
45 root: root.into(),
46 filter: AnalysisFilter::default(),
47 error_strategy: AnalysisErrorStrategy::default(),
48 max_depth: None,
49 follow_symlinks: false,
50 top_n_largest: DEFAULT_TOP_N_LARGEST,
51 detect_mime_types: false,
52 max_reported_errors: DEFAULT_MAX_REPORTED_ERRORS,
53 walk_concurrency: None,
54 #[cfg(feature = "checksum")]
55 detect_duplicates: false,
56 #[cfg(feature = "checksum")]
57 hash_concurrency: None,
58 #[cfg(feature = "checksum")]
59 max_reported_duplicates: DEFAULT_MAX_REPORTED_DUPLICATE_GROUPS,
60 }
61 }
62
63 pub fn extensions(mut self, exts: impl IntoIterator<Item = impl Into<String>>) -> Self {
67 self.filter.extensions = Some(exts.into_iter().map(Into::into).collect());
68 self
69 }
70
71 pub fn exclude_globs(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
75 self.filter.exclude_patterns = patterns.into_iter().map(Into::into).collect();
76 self
77 }
78
79 pub fn min_size(mut self, bytes: u64) -> Self {
80 self.filter.min_size = Some(bytes);
81 self
82 }
83
84 pub fn max_size(mut self, bytes: u64) -> Self {
85 self.filter.max_size = Some(bytes);
86 self
87 }
88
89 pub fn modified_after(mut self, t: SystemTime) -> Self {
92 self.filter.modified_after = Some(t);
93 self
94 }
95
96 pub fn modified_before(mut self, t: SystemTime) -> Self {
99 self.filter.modified_before = Some(t);
100 self
101 }
102
103 pub fn max_depth(mut self, depth: usize) -> Self {
108 self.max_depth = Some(depth);
109 self
110 }
111
112 pub fn follow_symlinks(mut self, follow: bool) -> Self {
116 self.follow_symlinks = follow;
117 self
118 }
119
120 pub fn walk_concurrency(mut self, n: usize) -> Self {
130 self.walk_concurrency = Some(n);
131 self
132 }
133
134 pub fn top_n_largest(mut self, n: usize) -> Self {
138 self.top_n_largest = n;
139 self
140 }
141
142 pub fn detect_mime_types(mut self, enable: bool) -> Self {
146 self.detect_mime_types = enable;
147 self
148 }
149
150 pub fn on_error(mut self, strategy: AnalysisErrorStrategy) -> Self {
151 self.error_strategy = strategy;
152 self
153 }
154
155 pub fn max_reported_errors(mut self, n: usize) -> Self {
158 self.max_reported_errors = n;
159 self
160 }
161
162 #[cfg(feature = "checksum")]
165 pub fn detect_duplicates(mut self, enable: bool) -> Self {
166 self.detect_duplicates = enable;
167 self
168 }
169
170 #[cfg(feature = "checksum")]
173 pub fn hash_concurrency(mut self, n: usize) -> Self {
174 self.hash_concurrency = Some(n);
175 self
176 }
177
178 #[cfg(feature = "checksum")]
181 pub fn max_reported_duplicates(mut self, n: usize) -> Self {
182 self.max_reported_duplicates = n;
183 self
184 }
185
186 pub fn start(self) -> Result<AnalysisHandle> {
187 let cancel = CancellationToken::new();
188 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
189 let reporter = AnalysisProgressReporter::new(tx);
190 let cancel_for_task = cancel.clone();
191
192 #[cfg(feature = "checksum")]
193 let detect_duplicates_enabled = self.detect_duplicates;
194
195 let params = WalkParams {
196 root: self.root,
197 filter: self.filter,
198 error_strategy: self.error_strategy,
199 max_depth: self.max_depth,
200 follow_symlinks: self.follow_symlinks,
201 top_n_largest: self.top_n_largest,
202 detect_mime_types: self.detect_mime_types,
203 #[cfg(feature = "checksum")]
204 collect_duplicate_candidates: detect_duplicates_enabled,
205 max_reported_errors: self.max_reported_errors,
206 walk_concurrency: self.walk_concurrency.unwrap_or_else(default_concurrency),
207 };
208
209 #[cfg(feature = "checksum")]
210 let error_strategy = self.error_strategy;
211 #[cfg(feature = "checksum")]
212 let hash_concurrency = self.hash_concurrency;
213 #[cfg(feature = "checksum")]
214 let max_reported_duplicates = self.max_reported_duplicates;
215 #[cfg(feature = "checksum")]
216 let max_reported_errors = self.max_reported_errors;
217
218 let join_handle = tokio::spawn(async move {
219 let started = Instant::now();
220 let outcome = walk(params, cancel_for_task.clone(), reporter.clone()).await?;
221
222 #[cfg(feature = "checksum")]
223 let (duplicates, duplicate_groups_total, duplicate_bytes_wasted, errors, errors_total) = {
224 let mut errors = outcome.errors;
225 let mut errors_total = outcome.errors_total;
226 if detect_duplicates_enabled {
227 let hash_outcome = detect_duplicates(
228 outcome.duplicate_candidates,
229 hash_concurrency,
230 max_reported_duplicates,
231 max_reported_errors.saturating_sub(errors.len()),
232 error_strategy,
233 &cancel_for_task,
234 &reporter,
235 )
236 .await?;
237 errors.extend(hash_outcome.errors);
238 errors_total += hash_outcome.errors_total;
239 (
240 hash_outcome.groups,
241 hash_outcome.groups_total,
242 hash_outcome.bytes_wasted,
243 errors,
244 errors_total,
245 )
246 } else {
247 (Vec::new(), 0, 0, errors, errors_total)
248 }
249 };
250 #[cfg(not(feature = "checksum"))]
251 let (errors, errors_total) = (outcome.errors, outcome.errors_total);
252
253 Ok(AnalysisReport {
254 file_count: outcome.file_count,
255 dir_count: outcome.dir_count,
256 total_size: outcome.total_size,
257 largest_files: outcome.largest_files,
258 by_extension: outcome.by_extension,
259 by_mime: outcome.by_mime,
260 age_buckets: outcome.age_buckets,
261 errors,
262 errors_total,
263 #[cfg(feature = "checksum")]
264 duplicates,
265 #[cfg(feature = "checksum")]
266 duplicate_groups_total,
267 #[cfg(feature = "checksum")]
268 duplicate_bytes_wasted,
269 duration: started.elapsed(),
270 })
271 });
272
273 Ok(AnalysisHandle::new(join_handle, rx, cancel))
274 }
275}