rusty_files/indexer/
metadata.rs

1use crate::core::error::Result;
2use crate::core::types::FileEntry;
3use crate::utils::mime::detect_mime_type;
4use crate::utils::path::is_hidden;
5use chrono::{DateTime, TimeZone, Utc};
6use std::fs;
7use std::path::Path;
8
9pub struct MetadataExtractor;
10
11impl MetadataExtractor {
12    pub fn extract<P: AsRef<Path>>(path: P) -> Result<FileEntry> {
13        let path = path.as_ref();
14        let metadata = fs::metadata(path)?;
15
16        let mut entry = FileEntry::new(path.to_path_buf());
17
18        entry.size = metadata.len();
19        entry.is_directory = metadata.is_dir();
20        entry.is_hidden = is_hidden(path);
21
22        #[cfg(unix)]
23        {
24            entry.is_symlink = metadata.file_type().is_symlink();
25        }
26
27        #[cfg(windows)]
28        {
29            entry.is_symlink = metadata.file_type().is_symlink();
30        }
31
32        if let Ok(created) = metadata.created() {
33            entry.created_at = Self::system_time_to_datetime(created);
34        }
35
36        if let Ok(modified) = metadata.modified() {
37            entry.modified_at = Self::system_time_to_datetime(modified);
38        }
39
40        if let Ok(accessed) = metadata.accessed() {
41            entry.accessed_at = Self::system_time_to_datetime(accessed);
42        }
43
44        if !entry.is_directory {
45            entry.mime_type = detect_mime_type(path);
46        }
47
48        let now = Utc::now();
49        entry.indexed_at = now;
50        entry.last_verified = now;
51
52        Ok(entry)
53    }
54
55    pub fn extract_batch<P: AsRef<Path> + Sync>(paths: &[P]) -> Vec<Result<FileEntry>> {
56        use rayon::prelude::*;
57
58        paths
59            .par_iter()
60            .map(|path| Self::extract(path.as_ref()))
61            .collect()
62    }
63
64    fn system_time_to_datetime(time: std::time::SystemTime) -> Option<DateTime<Utc>> {
65        time.duration_since(std::time::UNIX_EPOCH)
66            .ok()
67            .and_then(|duration| {
68                Utc.timestamp_opt(duration.as_secs() as i64, duration.subsec_nanos())
69                    .single()
70            })
71    }
72
73    pub fn is_modified_since<P: AsRef<Path>>(
74        path: P,
75        since: DateTime<Utc>,
76    ) -> Result<bool> {
77        let metadata = fs::metadata(path)?;
78        if let Ok(modified) = metadata.modified() {
79            if let Some(modified_dt) = Self::system_time_to_datetime(modified) {
80                return Ok(modified_dt > since);
81            }
82        }
83        Ok(false)
84    }
85
86    pub fn get_file_size<P: AsRef<Path>>(path: P) -> Result<u64> {
87        let metadata = fs::metadata(path)?;
88        Ok(metadata.len())
89    }
90
91    pub fn is_readable<P: AsRef<Path>>(path: P) -> bool {
92        fs::metadata(path).is_ok()
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::fs;
100    use tempfile::TempDir;
101
102    #[test]
103    fn test_extract_file_metadata() {
104        let temp_dir = TempDir::new().unwrap();
105        let file_path = temp_dir.path().join("test.txt");
106        fs::write(&file_path, "Hello, world!").unwrap();
107
108        let entry = MetadataExtractor::extract(&file_path).unwrap();
109
110        assert_eq!(entry.name, "test.txt");
111        assert_eq!(entry.extension, Some("txt".to_string()));
112        assert_eq!(entry.size, 13);
113        assert!(!entry.is_directory);
114    }
115
116    #[test]
117    fn test_extract_directory_metadata() {
118        let temp_dir = TempDir::new().unwrap();
119        let dir_path = temp_dir.path().join("subdir");
120        fs::create_dir(&dir_path).unwrap();
121
122        let entry = MetadataExtractor::extract(&dir_path).unwrap();
123
124        assert_eq!(entry.name, "subdir");
125        assert!(entry.is_directory);
126    }
127
128    #[test]
129    fn test_extract_batch() {
130        let temp_dir = TempDir::new().unwrap();
131        let file1 = temp_dir.path().join("file1.txt");
132        let file2 = temp_dir.path().join("file2.txt");
133
134        fs::write(&file1, "content1").unwrap();
135        fs::write(&file2, "content2").unwrap();
136
137        let paths = vec![file1, file2];
138        let results = MetadataExtractor::extract_batch(&paths);
139
140        assert_eq!(results.len(), 2);
141        assert!(results.iter().all(|r| r.is_ok()));
142    }
143}