1use std::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, 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 Default for TreeStats {
34 fn default() -> Self {
35 Self {
36 total_size: 0,
37 total_files: 0,
38 total_dirs: 0,
39 total_symlinks: 0,
40 max_depth: 0,
41 largest_file: None,
42 oldest_file: None,
43 newest_file: None,
44 }
45 }
46}
47
48impl TreeStats {
49 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn record_file(&mut self, path: PathBuf, size: u64, modified: SystemTime, depth: u32) {
56 self.total_files += 1;
57 self.total_size += size;
58 self.max_depth = self.max_depth.max(depth);
59
60 if self.largest_file.as_ref().is_none_or(|(_, s)| size > *s) {
62 self.largest_file = Some((path.clone(), size));
63 }
64
65 if self.oldest_file.as_ref().is_none_or(|(_, t)| modified < *t) {
67 self.oldest_file = Some((path.clone(), modified));
68 }
69
70 if self.newest_file.as_ref().is_none_or(|(_, t)| modified > *t) {
72 self.newest_file = Some((path, modified));
73 }
74 }
75
76 pub fn record_dir(&mut self, depth: u32) {
78 self.total_dirs += 1;
79 self.max_depth = self.max_depth.max(depth);
80 }
81
82 pub fn record_symlink(&mut self) {
84 self.total_symlinks += 1;
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct FileTree {
91 pub root: FileNode,
93
94 pub root_path: PathBuf,
96
97 pub scanned_at: SystemTime,
99
100 pub scan_duration: Duration,
102
103 pub config: ScanConfig,
105
106 pub stats: TreeStats,
108
109 pub warnings: Vec<ScanWarning>,
111}
112
113impl FileTree {
114 pub fn new(
116 root: FileNode,
117 root_path: PathBuf,
118 config: ScanConfig,
119 stats: TreeStats,
120 scan_duration: Duration,
121 warnings: Vec<ScanWarning>,
122 ) -> Self {
123 Self {
124 root,
125 root_path,
126 scanned_at: SystemTime::now(),
127 scan_duration,
128 config,
129 stats,
130 warnings,
131 }
132 }
133
134 pub fn total_size(&self) -> u64 {
136 self.root.size
137 }
138
139 pub fn total_files(&self) -> u64 {
141 self.stats.total_files
142 }
143
144 pub fn total_dirs(&self) -> u64 {
146 self.stats.total_dirs
147 }
148
149 pub fn has_warnings(&self) -> bool {
151 !self.warnings.is_empty()
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_tree_stats_default() {
161 let stats = TreeStats::default();
162 assert_eq!(stats.total_size, 0);
163 assert_eq!(stats.total_files, 0);
164 assert_eq!(stats.total_dirs, 0);
165 }
166
167 #[test]
168 fn test_tree_stats_record_file() {
169 let mut stats = TreeStats::new();
170 let now = SystemTime::now();
171
172 stats.record_file(PathBuf::from("/test/file.txt"), 1024, now, 2);
173
174 assert_eq!(stats.total_files, 1);
175 assert_eq!(stats.total_size, 1024);
176 assert_eq!(stats.max_depth, 2);
177 assert!(stats.largest_file.is_some());
178 }
179}