gravityfile_scan/
inode.rs

1//! Inode tracking for hardlink deduplication.
2
3use dashmap::DashSet;
4use gravityfile_core::InodeInfo;
5
6/// Tracks seen inodes to prevent double-counting hardlinks.
7///
8/// When a file has multiple hardlinks, we only want to count its size once.
9/// This tracker uses a concurrent set to track (inode, device) pairs.
10#[derive(Debug, Default)]
11pub struct InodeTracker {
12    seen: DashSet<InodeInfo>,
13}
14
15impl InodeTracker {
16    /// Create a new inode tracker.
17    pub fn new() -> Self {
18        Self {
19            seen: DashSet::new(),
20        }
21    }
22
23    /// Track an inode. Returns `true` if this is the first time seeing it.
24    ///
25    /// If the inode was already tracked, returns `false` indicating this
26    /// is a hardlink to an already-counted file.
27    pub fn track(&self, info: InodeInfo) -> bool {
28        self.seen.insert(info)
29    }
30
31    /// Check if an inode has been seen (without tracking).
32    pub fn has_seen(&self, info: &InodeInfo) -> bool {
33        self.seen.contains(info)
34    }
35
36    /// Get the number of unique inodes tracked.
37    pub fn len(&self) -> usize {
38        self.seen.len()
39    }
40
41    /// Check if no inodes have been tracked.
42    pub fn is_empty(&self) -> bool {
43        self.seen.is_empty()
44    }
45
46    /// Clear all tracked inodes.
47    pub fn clear(&self) {
48        self.seen.clear();
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn test_track_new_inode() {
58        let tracker = InodeTracker::new();
59        let info = InodeInfo::new(12345, 1);
60
61        assert!(tracker.track(info));
62        assert!(!tracker.track(info)); // Second time returns false
63    }
64
65    #[test]
66    fn test_has_seen() {
67        let tracker = InodeTracker::new();
68        let info = InodeInfo::new(12345, 1);
69
70        assert!(!tracker.has_seen(&info));
71        tracker.track(info);
72        assert!(tracker.has_seen(&info));
73    }
74
75    #[test]
76    fn test_different_devices() {
77        let tracker = InodeTracker::new();
78        let info1 = InodeInfo::new(12345, 1);
79        let info2 = InodeInfo::new(12345, 2); // Same inode, different device
80
81        assert!(tracker.track(info1));
82        assert!(tracker.track(info2)); // Different device, so it's new
83    }
84}