1use std::path::{Path, PathBuf};
4use std::time::{Duration, SystemTime};
5
6use serde::{Deserialize, Serialize};
7
8use crate::config::ScanConfig;
9use crate::error::ScanWarning;
10use crate::node::FileNode;
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct TreeStats {
15 pub total_size: u64,
17 pub total_files: u64,
19 pub total_dirs: u64,
21 pub total_symlinks: u64,
23 pub max_depth: u32,
25 pub largest_file: Option<(PathBuf, u64)>,
27 pub oldest_file: Option<(PathBuf, SystemTime)>,
29 pub newest_file: Option<(PathBuf, SystemTime)>,
31}
32
33impl TreeStats {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn record_file(&mut self, path: &Path, size: u64, modified: SystemTime, depth: u32) {
41 self.total_files += 1;
42 self.total_size += size;
43 self.max_depth = self.max_depth.max(depth);
44
45 if self.largest_file.as_ref().is_none_or(|(_, s)| size > *s) {
47 self.largest_file = Some((path.to_path_buf(), size));
48 }
49
50 if self.oldest_file.as_ref().is_none_or(|(_, t)| modified < *t) {
52 self.oldest_file = Some((path.to_path_buf(), modified));
53 }
54
55 if self.newest_file.as_ref().is_none_or(|(_, t)| modified > *t) {
57 self.newest_file = Some((path.to_path_buf(), modified));
58 }
59 }
60
61 pub fn record_dir(&mut self, depth: u32) {
63 self.total_dirs += 1;
64 self.max_depth = self.max_depth.max(depth);
65 }
66
67 pub fn record_symlink(&mut self) {
69 self.total_symlinks += 1;
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct FileTree {
78 pub root: FileNode,
80
81 pub root_path: PathBuf,
83
84 pub scanned_at: SystemTime,
86
87 pub scan_duration: Duration,
89
90 pub config: ScanConfig,
92
93 pub stats: TreeStats,
95
96 pub warnings: Vec<ScanWarning>,
98}
99
100impl FileTree {
101 pub fn new(
103 root: FileNode,
104 root_path: PathBuf,
105 config: ScanConfig,
106 stats: TreeStats,
107 scan_duration: Duration,
108 warnings: Vec<ScanWarning>,
109 ) -> Self {
110 Self {
111 root,
112 root_path,
113 scanned_at: SystemTime::now(),
114 scan_duration,
115 config,
116 stats,
117 warnings,
118 }
119 }
120
121 #[inline]
123 pub fn total_size(&self) -> u64 {
124 self.root.size
125 }
126
127 #[inline]
129 pub fn total_files(&self) -> u64 {
130 self.stats.total_files
131 }
132
133 #[inline]
135 pub fn total_dirs(&self) -> u64 {
136 self.stats.total_dirs
137 }
138
139 #[inline]
141 pub fn has_warnings(&self) -> bool {
142 !self.warnings.is_empty()
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_tree_stats_default() {
152 let stats = TreeStats::default();
153 assert_eq!(stats.total_size, 0);
154 assert_eq!(stats.total_files, 0);
155 assert_eq!(stats.total_dirs, 0);
156 }
157
158 #[test]
159 fn test_tree_stats_record_file() {
160 let mut stats = TreeStats::new();
161 let now = SystemTime::now();
162
163 stats.record_file(Path::new("/test/file.txt"), 1024, now, 2);
164
165 assert_eq!(stats.total_files, 1);
166 assert_eq!(stats.total_size, 1024);
167 assert_eq!(stats.max_depth, 2);
168 assert!(stats.largest_file.is_some());
169 }
170
171 #[test]
172 fn test_tree_stats_record_file_tracks_extremes() {
173 let mut stats = TreeStats::new();
174 let t1 = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
175 let t2 = SystemTime::UNIX_EPOCH + Duration::from_secs(2000);
176
177 stats.record_file(Path::new("/small_old"), 100, t1, 1);
178 stats.record_file(Path::new("/big_new"), 999, t2, 2);
179
180 assert_eq!(stats.largest_file.as_ref().unwrap().1, 999);
181 assert_eq!(stats.oldest_file.as_ref().unwrap().1, t1);
182 assert_eq!(stats.newest_file.as_ref().unwrap().1, t2);
183 }
184}