ccsync_core/comparison/
timestamp.rs

1//! File timestamp comparison for determining recency
2
3use std::fs;
4use std::path::Path;
5use std::time::SystemTime;
6
7use anyhow::Context;
8
9use crate::error::Result;
10
11/// Timestamp comparator
12pub struct TimestampComparator;
13
14impl Default for TimestampComparator {
15    fn default() -> Self {
16        Self::new()
17    }
18}
19
20impl TimestampComparator {
21    /// Create a new timestamp comparator
22    #[must_use]
23    pub const fn new() -> Self {
24        Self
25    }
26
27    /// Check if source file is newer than destination file
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if file metadata cannot be read.
32    pub fn is_newer(source: &Path, destination: &Path) -> Result<bool> {
33        let source_time = Self::get_modified_time(source)?;
34        let dest_time = Self::get_modified_time(destination)?;
35
36        Ok(source_time > dest_time)
37    }
38
39    /// Get the modification time of a file
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if file metadata cannot be read.
44    pub fn get_modified_time(path: &Path) -> Result<SystemTime> {
45        let metadata = fs::metadata(path)
46            .with_context(|| format!("Failed to read metadata for: {}", path.display()))?;
47
48        metadata
49            .modified()
50            .with_context(|| format!("Failed to get modification time for: {}", path.display()))
51    }
52
53    /// Compare modification times and return ordering
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if file metadata cannot be read.
58    pub fn compare_times(source: &Path, destination: &Path) -> Result<std::cmp::Ordering> {
59        let source_time = Self::get_modified_time(source)?;
60        let dest_time = Self::get_modified_time(destination)?;
61
62        Ok(source_time.cmp(&dest_time))
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use std::fs;
70    use std::thread;
71    use std::time::Duration;
72    use tempfile::TempDir;
73
74    #[test]
75    fn test_identical_timestamps() {
76        let tmp = TempDir::new().unwrap();
77        let file1 = tmp.path().join("file1.txt");
78        let file2 = tmp.path().join("file2.txt");
79
80        fs::write(&file1, "content").unwrap();
81
82        // Copy file - this may create identical or newer timestamps depending on filesystem
83        fs::copy(&file1, &file2).unwrap();
84
85        let _comparator = TimestampComparator::new();
86        let ordering = TimestampComparator::compare_times(&file1, &file2).unwrap();
87
88        // On some filesystems, copy creates identical timestamps (Equal)
89        // On others, copy creates a newer file (Less, because file1 < file2)
90        // We verify both files exist and comparison works without panicking
91        assert!(
92            matches!(
93                ordering,
94                std::cmp::Ordering::Equal | std::cmp::Ordering::Less
95            ),
96            "Expected Equal or Less for copied file timestamps, got {:?}",
97            ordering
98        );
99    }
100
101    #[test]
102    fn test_source_newer() {
103        let tmp = TempDir::new().unwrap();
104        let file1 = tmp.path().join("old.txt");
105        let file2 = tmp.path().join("new.txt");
106
107        // Create old file
108        fs::write(&file1, "old content").unwrap();
109
110        // Sleep to ensure time difference
111        thread::sleep(Duration::from_millis(10));
112
113        // Create new file
114        fs::write(&file2, "new content").unwrap();
115
116        let _comparator = TimestampComparator::new();
117        let is_newer = TimestampComparator::is_newer(&file2, &file1).unwrap();
118
119        assert!(is_newer, "file2 should be newer than file1");
120    }
121
122    #[test]
123    fn test_destination_newer() {
124        let tmp = TempDir::new().unwrap();
125        let file1 = tmp.path().join("new.txt");
126        let file2 = tmp.path().join("old.txt");
127
128        // Create old file
129        fs::write(&file2, "old content").unwrap();
130
131        // Sleep to ensure time difference
132        thread::sleep(Duration::from_millis(10));
133
134        // Create new file
135        fs::write(&file1, "new content").unwrap();
136
137        let _comparator = TimestampComparator::new();
138        let is_newer = TimestampComparator::is_newer(&file1, &file2).unwrap();
139
140        assert!(is_newer, "file1 should be newer than file2");
141    }
142
143    #[test]
144    fn test_get_modified_time() {
145        let tmp = TempDir::new().unwrap();
146        let file = tmp.path().join("file.txt");
147        fs::write(&file, "content").unwrap();
148
149        let time = TimestampComparator::get_modified_time(&file);
150        assert!(time.is_ok());
151    }
152
153    #[test]
154    fn test_nonexistent_file() {
155        let tmp = TempDir::new().unwrap();
156        let file = tmp.path().join("nonexistent.txt");
157
158        let result = TimestampComparator::get_modified_time(&file);
159        assert!(result.is_err());
160    }
161}