gravityfile_core/
node.rs

1//! File and directory node types.
2
3use std::time::SystemTime;
4
5use compact_str::CompactString;
6use serde::{Deserialize, Serialize};
7
8/// Unique identifier for a node within a tree.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct NodeId(pub u64);
11
12impl NodeId {
13    /// Create a new NodeId from a u64.
14    pub fn new(id: u64) -> Self {
15        Self(id)
16    }
17}
18
19/// BLAKE3 content hash for duplicate detection.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub struct ContentHash(pub [u8; 32]);
22
23impl ContentHash {
24    /// Create a new ContentHash from raw bytes.
25    pub fn new(bytes: [u8; 32]) -> Self {
26        Self(bytes)
27    }
28
29    /// Get the hash as a hex string.
30    pub fn to_hex(&self) -> String {
31        self.0.iter().map(|b| format!("{b:02x}")).collect()
32    }
33}
34
35/// Inode information for hardlink detection.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub struct InodeInfo {
38    /// Inode number.
39    pub inode: u64,
40    /// Device ID.
41    pub device: u64,
42}
43
44impl InodeInfo {
45    /// Create new inode info.
46    pub fn new(inode: u64, device: u64) -> Self {
47        Self { inode, device }
48    }
49}
50
51/// File metadata timestamps.
52#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
53pub struct Timestamps {
54    /// Last modification time.
55    pub modified: SystemTime,
56    /// Last access time (if available).
57    pub accessed: Option<SystemTime>,
58    /// Creation time (if available, platform-dependent).
59    pub created: Option<SystemTime>,
60}
61
62impl Timestamps {
63    /// Create timestamps with only modified time.
64    pub fn with_modified(modified: SystemTime) -> Self {
65        Self {
66            modified,
67            accessed: None,
68            created: None,
69        }
70    }
71
72    /// Create timestamps with all available times.
73    pub fn new(
74        modified: SystemTime,
75        accessed: Option<SystemTime>,
76        created: Option<SystemTime>,
77    ) -> Self {
78        Self {
79            modified,
80            accessed,
81            created,
82        }
83    }
84}
85
86/// Type of file system node.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub enum NodeKind {
89    /// Regular file.
90    File {
91        /// Whether the file is executable.
92        executable: bool,
93    },
94    /// Directory.
95    Directory {
96        /// Total number of files in this subtree.
97        file_count: u64,
98        /// Total number of directories in this subtree.
99        dir_count: u64,
100    },
101    /// Symbolic link.
102    Symlink {
103        /// Link target path.
104        target: CompactString,
105        /// Whether the link target exists.
106        broken: bool,
107    },
108    /// Other file types (sockets, devices, etc.).
109    Other,
110}
111
112impl NodeKind {
113    /// Check if this is a directory.
114    pub fn is_dir(&self) -> bool {
115        matches!(self, NodeKind::Directory { .. })
116    }
117
118    /// Check if this is a regular file.
119    pub fn is_file(&self) -> bool {
120        matches!(self, NodeKind::File { .. })
121    }
122
123    /// Check if this is a symlink.
124    pub fn is_symlink(&self) -> bool {
125        matches!(self, NodeKind::Symlink { .. })
126    }
127}
128
129/// A single file or directory in the tree.
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct FileNode {
132    /// Unique identifier for this node.
133    pub id: NodeId,
134
135    /// File/directory name (not full path).
136    pub name: CompactString,
137
138    /// Node type and associated metadata.
139    pub kind: NodeKind,
140
141    /// Size in bytes (aggregate for directories).
142    pub size: u64,
143
144    /// Disk blocks actually used.
145    pub blocks: u64,
146
147    /// File metadata timestamps.
148    pub timestamps: Timestamps,
149
150    /// Inode info for hardlink detection.
151    pub inode: Option<InodeInfo>,
152
153    /// Content hash (computed on demand).
154    pub content_hash: Option<ContentHash>,
155
156    /// Children nodes (directories only), sorted by size descending.
157    pub children: Vec<FileNode>,
158}
159
160impl FileNode {
161    /// Create a new file node.
162    pub fn new_file(
163        id: NodeId,
164        name: impl Into<CompactString>,
165        size: u64,
166        blocks: u64,
167        timestamps: Timestamps,
168        executable: bool,
169    ) -> Self {
170        Self {
171            id,
172            name: name.into(),
173            kind: NodeKind::File { executable },
174            size,
175            blocks,
176            timestamps,
177            inode: None,
178            content_hash: None,
179            children: Vec::new(),
180        }
181    }
182
183    /// Create a new directory node.
184    pub fn new_directory(
185        id: NodeId,
186        name: impl Into<CompactString>,
187        timestamps: Timestamps,
188    ) -> Self {
189        Self {
190            id,
191            name: name.into(),
192            kind: NodeKind::Directory {
193                file_count: 0,
194                dir_count: 0,
195            },
196            size: 0,
197            blocks: 0,
198            timestamps,
199            inode: None,
200            content_hash: None,
201            children: Vec::new(),
202        }
203    }
204
205    /// Check if this node is a directory.
206    pub fn is_dir(&self) -> bool {
207        self.kind.is_dir()
208    }
209
210    /// Check if this node is a file.
211    pub fn is_file(&self) -> bool {
212        self.kind.is_file()
213    }
214
215    /// Get the number of direct children.
216    pub fn child_count(&self) -> usize {
217        self.children.len()
218    }
219
220    /// Get file count for directories, 1 for files.
221    pub fn file_count(&self) -> u64 {
222        match &self.kind {
223            NodeKind::Directory { file_count, .. } => *file_count,
224            NodeKind::File { .. } => 1,
225            _ => 0,
226        }
227    }
228
229    /// Get directory count for directories.
230    pub fn dir_count(&self) -> u64 {
231        match &self.kind {
232            NodeKind::Directory { dir_count, .. } => *dir_count,
233            _ => 0,
234        }
235    }
236
237    /// Sort children by size in descending order.
238    pub fn sort_children_by_size(&mut self) {
239        self.children.sort_by(|a, b| b.size.cmp(&a.size));
240        for child in &mut self.children {
241            child.sort_children_by_size();
242        }
243    }
244
245    /// Update directory counts based on children.
246    pub fn update_counts(&mut self) {
247        if let NodeKind::Directory {
248            ref mut file_count,
249            ref mut dir_count,
250        } = self.kind
251        {
252            *file_count = 0;
253            *dir_count = 0;
254
255            for child in &self.children {
256                match &child.kind {
257                    NodeKind::File { .. } => *file_count += 1,
258                    NodeKind::Directory {
259                        file_count: fc,
260                        dir_count: dc,
261                    } => {
262                        *file_count += fc;
263                        *dir_count += dc + 1;
264                    }
265                    _ => {}
266                }
267            }
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_node_id() {
278        let id = NodeId::new(42);
279        assert_eq!(id.0, 42);
280    }
281
282    #[test]
283    fn test_content_hash_hex() {
284        let hash = ContentHash::new([0xab; 32]);
285        assert_eq!(hash.to_hex().len(), 64);
286        assert!(hash.to_hex().starts_with("abab"));
287    }
288
289    #[test]
290    fn test_file_node_creation() {
291        let node = FileNode::new_file(
292            NodeId::new(1),
293            "test.txt",
294            1024,
295            2,
296            Timestamps::with_modified(SystemTime::now()),
297            false,
298        );
299        assert!(node.is_file());
300        assert!(!node.is_dir());
301        assert_eq!(node.size, 1024);
302    }
303
304    #[test]
305    fn test_directory_node_creation() {
306        let node = FileNode::new_directory(
307            NodeId::new(1),
308            "test_dir",
309            Timestamps::with_modified(SystemTime::now()),
310        );
311        assert!(node.is_dir());
312        assert!(!node.is_file());
313    }
314}