rusty_files/indexer/
metadata.rs1use 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}