Skip to main content

file_engine/analysis/
report.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::{Duration, SystemTime};
4
5use crate::error::Error;
6
7/// A file discovered and matched by `AnalyzeBuilder`'s filters.
8///
9/// Deliberately a separate type from `profiler::Entry` rather than a
10/// reuse: `profiler` (and everything in it) is gated behind the
11/// `operations` feature, while `analyze` doesn't require `operations` —
12/// reaching into `profiler::Entry` would force every `analyze`-only
13/// build to pull in the whole copy/move/sync pipeline just for this
14/// struct.
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
16pub struct Entry {
17    pub path: PathBuf,
18    pub relative_path: PathBuf,
19    pub size: u64,
20    /// `None` when the platform/filesystem doesn't report mtimes.
21    pub modified: Option<SystemTime>,
22}
23
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub struct ExtensionStats {
26    pub count: usize,
27    pub total_size: u64,
28}
29
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
31pub struct MimeStats {
32    pub count: usize,
33    pub total_size: u64,
34}
35
36/// Files bucketed by how long ago they were last modified, relative to a
37/// single `now` captured once when the walk starts — not re-read per
38/// entry, so results are deterministic within one run regardless of how
39/// long the walk takes. Each entry lands in the first bucket whose
40/// boundary it's younger than.
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42pub struct AgeBuckets {
43    pub under_1_day: usize,
44    pub under_1_week: usize,
45    pub under_1_month: usize,
46    pub under_1_year: usize,
47    pub older: usize,
48    /// Entries with no readable `modified` time on this platform/filesystem.
49    pub unknown: usize,
50}
51
52/// A set of files sharing both size and blake3 content hash. Only
53/// produced when `.detect_duplicates(true)` (feature `checksum`) is set.
54#[cfg(feature = "checksum")]
55#[derive(Debug, Clone)]
56pub struct DuplicateGroup {
57    pub hash: [u8; 32],
58    pub size: u64,
59    pub paths: Vec<PathBuf>,
60}
61
62/// Default cap on how many `(path, Error)` pairs `AnalysisReport::errors`
63/// retains — see `AnalysisReport::errors_total` for the uncapped count.
64/// Chosen to keep a report from a badly-permissioned tree bounded in
65/// memory; override with `AnalyzeBuilder::max_reported_errors`.
66pub const DEFAULT_MAX_REPORTED_ERRORS: usize = 1000;
67
68/// Default cap on how many `DuplicateGroup`s `AnalysisReport::duplicates`
69/// retains — see `AnalysisReport::duplicate_groups_total` and
70/// `AnalysisReport::duplicate_bytes_wasted`, both uncapped, for the sums
71/// that stay accurate even once the sample is truncated.
72#[cfg(feature = "checksum")]
73pub const DEFAULT_MAX_REPORTED_DUPLICATE_GROUPS: usize = 1000;
74
75/// `#[non_exhaustive]`: a new aggregate (e.g. permission-mode breakdown)
76/// is additive, not a breaking change for callers who construct nothing
77/// and only read fields — mirrors `Progress`'s reasoning in
78/// `src/progress.rs`.
79#[derive(Debug)]
80#[non_exhaustive]
81pub struct AnalysisReport {
82    pub file_count: usize,
83    pub dir_count: usize,
84    pub total_size: u64,
85    /// Largest matched files, descending by size, capped at
86    /// `AnalyzeBuilder::top_n_largest`.
87    pub largest_files: Vec<Entry>,
88    /// Keyed by lowercased extension without the leading dot; entries
89    /// with no extension are grouped under the empty string `""`.
90    pub by_extension: HashMap<String, ExtensionStats>,
91    /// Populated only when `.detect_mime_types(true)` is set; empty
92    /// otherwise. Keyed by the MIME type `infer` reports, or `"unknown"`
93    /// when it can't classify the file's header.
94    pub by_mime: HashMap<String, MimeStats>,
95    pub age_buckets: AgeBuckets,
96    /// Sample of encountered errors, first-N by walk order (not sorted
97    /// by severity or size), capped at `AnalyzeBuilder::max_reported_errors`.
98    pub errors: Vec<(PathBuf, Error)>,
99    /// True count of errors encountered, independent of the `errors` cap.
100    pub errors_total: usize,
101    /// Sample of duplicate groups, first-N found, capped at
102    /// `AnalyzeBuilder::max_reported_duplicates`. Empty unless
103    /// `.detect_duplicates(true)` was set.
104    #[cfg(feature = "checksum")]
105    pub duplicates: Vec<DuplicateGroup>,
106    /// True count of duplicate groups found, independent of the
107    /// `duplicates` cap.
108    #[cfg(feature = "checksum")]
109    pub duplicate_groups_total: usize,
110    /// Sum of `size * (paths.len() - 1)` over *every* duplicate group
111    /// found, not just the capped sample — the number people actually
112    /// want out of duplicate detection shouldn't degrade just because
113    /// the detailed list got truncated.
114    #[cfg(feature = "checksum")]
115    pub duplicate_bytes_wasted: u64,
116    pub duration: Duration,
117}