gravityfile_core/
tree.rs

1//! File tree container and statistics.
2
3use 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/// Summary statistics for a scanned tree.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TreeStats {
15    /// Total size in bytes.
16    pub total_size: u64,
17    /// Total number of files.
18    pub total_files: u64,
19    /// Total number of directories.
20    pub total_dirs: u64,
21    /// Total number of symbolic links.
22    pub total_symlinks: u64,
23    /// Maximum depth reached.
24    pub max_depth: u32,
25    /// Largest file (path, size).
26    pub largest_file: Option<(PathBuf, u64)>,
27    /// Oldest file (path, time).
28    pub oldest_file: Option<(PathBuf, SystemTime)>,
29    /// Newest file (path, time).
30    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    /// Create new empty stats.
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Update stats with a file entry.
55    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        // Track largest file
61        if self.largest_file.as_ref().is_none_or(|(_, s)| size > *s) {
62            self.largest_file = Some((path.clone(), size));
63        }
64
65        // Track oldest file
66        if self.oldest_file.as_ref().is_none_or(|(_, t)| modified < *t) {
67            self.oldest_file = Some((path.clone(), modified));
68        }
69
70        // Track newest file
71        if self.newest_file.as_ref().is_none_or(|(_, t)| modified > *t) {
72            self.newest_file = Some((path, modified));
73        }
74    }
75
76    /// Record a directory.
77    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    /// Record a symlink.
83    pub fn record_symlink(&mut self) {
84        self.total_symlinks += 1;
85    }
86}
87
88/// Complete scanned file tree with metadata.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct FileTree {
91    /// Root node of the tree.
92    pub root: FileNode,
93
94    /// Root path that was scanned.
95    pub root_path: PathBuf,
96
97    /// When this scan was performed.
98    pub scanned_at: SystemTime,
99
100    /// Duration of the scan.
101    pub scan_duration: Duration,
102
103    /// Scan configuration used.
104    pub config: ScanConfig,
105
106    /// Summary statistics.
107    pub stats: TreeStats,
108
109    /// Warnings encountered during scan.
110    pub warnings: Vec<ScanWarning>,
111}
112
113impl FileTree {
114    /// Create a new file tree.
115    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    /// Get the total size of the tree.
135    pub fn total_size(&self) -> u64 {
136        self.root.size
137    }
138
139    /// Get the total number of files.
140    pub fn total_files(&self) -> u64 {
141        self.stats.total_files
142    }
143
144    /// Get the total number of directories.
145    pub fn total_dirs(&self) -> u64 {
146        self.stats.total_dirs
147    }
148
149    /// Check if there were any warnings during scanning.
150    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}