Skip to main content

kanban_persistence_json/
conflict.rs

1use std::fs;
2use std::hash::{Hash, Hasher};
3use std::path::Path;
4use std::time::SystemTime;
5
6/// Metadata about a file for conflict detection
7///
8/// Uses modification time, size, and content hash for comprehensive change detection.
9/// The hash provides additional protection against edge cases where timestamp and size
10/// might be identical but content differs.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct FileMetadata {
13    /// Last modified time of the file
14    pub modified_time: SystemTime,
15    /// File size in bytes for additional verification
16    pub size: u64,
17    /// Content hash for detecting changes even when timestamp/size are identical
18    pub content_hash: u64,
19}
20
21impl FileMetadata {
22    /// Create FileMetadata from a file on disk
23    ///
24    /// Computes content hash by reading and hashing the entire file.
25    /// For typical kanban files (< 1MB), this adds minimal overhead (1-5ms).
26    pub fn from_file(path: &Path) -> std::io::Result<Self> {
27        let metadata = fs::metadata(path)?;
28        let modified_time = metadata.modified()?;
29        let size = metadata.len();
30
31        // Always compute content hash for comprehensive change detection
32        let content = fs::read(path)?;
33        let mut hasher = std::collections::hash_map::DefaultHasher::new();
34        content.hash(&mut hasher);
35        let content_hash = hasher.finish();
36
37        Ok(Self {
38            modified_time,
39            size,
40            content_hash,
41        })
42    }
43
44    /// Check if file has changed since this metadata was captured
45    pub fn has_changed(&self, path: &Path) -> std::io::Result<bool> {
46        let current = Self::from_file(path)?;
47        Ok(current != *self)
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use tempfile::tempdir;
55
56    #[test]
57    fn test_metadata_from_file() {
58        let dir = tempdir().unwrap();
59        let file_path = dir.path().join("test.json");
60        let content = b"test content";
61        fs::write(&file_path, content).unwrap();
62
63        let metadata = FileMetadata::from_file(&file_path).unwrap();
64        assert_eq!(metadata.size, content.len() as u64);
65    }
66
67    #[test]
68    fn test_has_not_changed() {
69        let dir = tempdir().unwrap();
70        let file_path = dir.path().join("test.json");
71        fs::write(&file_path, b"test content").unwrap();
72
73        let metadata = FileMetadata::from_file(&file_path).unwrap();
74        assert!(!metadata.has_changed(&file_path).unwrap());
75    }
76
77    #[test]
78    fn test_has_changed_on_content_modification() {
79        let dir = tempdir().unwrap();
80        let file_path = dir.path().join("test.json");
81        fs::write(&file_path, b"test content").unwrap();
82
83        let metadata = FileMetadata::from_file(&file_path).unwrap();
84        fs::write(&file_path, b"modified content").unwrap();
85
86        assert!(metadata.has_changed(&file_path).unwrap());
87    }
88
89    #[test]
90    fn test_size_change_detected() {
91        let dir = tempdir().unwrap();
92        let file_path = dir.path().join("test.json");
93        fs::write(&file_path, b"content1").unwrap();
94
95        let metadata = FileMetadata::from_file(&file_path).unwrap();
96        assert_eq!(metadata.size, 8);
97
98        fs::write(&file_path, b"content1_longer").unwrap();
99        assert!(metadata.has_changed(&file_path).unwrap());
100    }
101}