metis_core/application/services/
filesystem.rs

1use crate::{MetisError, Result};
2use sha2::{Digest, Sha256};
3use std::fs;
4use std::path::Path;
5
6/// Filesystem operations service
7/// Handles reading/writing documents to disk and computing file hashes
8pub struct FilesystemService;
9
10impl FilesystemService {
11    /// Read file contents from disk
12    pub fn read_file<P: AsRef<Path>>(path: P) -> Result<String> {
13        fs::read_to_string(path).map_err(MetisError::Io)
14    }
15
16    /// Write file contents to disk
17    pub fn write_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> {
18        // Ensure parent directory exists
19        if let Some(parent) = path.as_ref().parent() {
20            fs::create_dir_all(parent).map_err(MetisError::Io)?;
21        }
22
23        fs::write(path, content).map_err(MetisError::Io)
24    }
25
26    /// Check if file exists
27    pub fn file_exists<P: AsRef<Path>>(path: P) -> bool {
28        path.as_ref().exists()
29    }
30
31    /// Compute SHA256 hash of file contents
32    pub fn compute_file_hash<P: AsRef<Path>>(path: P) -> Result<String> {
33        let contents = Self::read_file(path)?;
34        let mut hasher = Sha256::new();
35        hasher.update(contents.as_bytes());
36        Ok(format!("{:x}", hasher.finalize()))
37    }
38
39    /// Compute SHA256 hash of string content
40    pub fn compute_content_hash(content: &str) -> String {
41        let mut hasher = Sha256::new();
42        hasher.update(content.as_bytes());
43        format!("{:x}", hasher.finalize())
44    }
45
46    /// Get file modification time as Unix timestamp
47    pub fn get_file_mtime<P: AsRef<Path>>(path: P) -> Result<f64> {
48        let metadata = fs::metadata(path).map_err(MetisError::Io)?;
49        let mtime = metadata
50            .modified()
51            .map_err(MetisError::Io)?
52            .duration_since(std::time::UNIX_EPOCH)
53            .map_err(|_| MetisError::ValidationFailed {
54                message: "Invalid file modification time".to_string(),
55            })?;
56        Ok(mtime.as_secs_f64())
57    }
58
59    /// Delete a file
60    pub fn delete_file<P: AsRef<Path>>(path: P) -> Result<()> {
61        fs::remove_file(path).map_err(MetisError::Io)
62    }
63
64    /// List all markdown files in a directory recursively
65    pub fn find_markdown_files<P: AsRef<Path>>(dir: P) -> Result<Vec<String>> {
66        use walkdir::WalkDir;
67
68        let mut files = Vec::new();
69
70        for entry in WalkDir::new(dir).follow_links(true) {
71            let entry = entry
72                .map_err(|e| MetisError::Io(std::io::Error::other(format!("Walk error: {}", e))))?;
73
74            if entry.file_type().is_file() {
75                if let Some(path_str) = entry.path().to_str() {
76                    if path_str.ends_with(".md") {
77                        files.push(path_str.to_string());
78                    }
79                }
80            }
81        }
82
83        Ok(files)
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use tempfile::tempdir;
91
92    #[test]
93    fn test_write_and_read_file() {
94        let temp_dir = tempdir().expect("Failed to create temp dir");
95        let file_path = temp_dir.path().join("test.md");
96
97        let content = "# Test Document\n\nThis is test content.";
98
99        // Write file
100        FilesystemService::write_file(&file_path, content).expect("Failed to write file");
101
102        // Read file
103        let read_content = FilesystemService::read_file(&file_path).expect("Failed to read file");
104        assert_eq!(content, read_content);
105
106        // Check if file exists
107        assert!(FilesystemService::file_exists(&file_path));
108    }
109
110    #[test]
111    fn test_compute_hashes() {
112        let content = "# Test Document\n\nThis is test content.";
113
114        // Test content hash
115        let hash1 = FilesystemService::compute_content_hash(content);
116        let hash2 = FilesystemService::compute_content_hash(content);
117        assert_eq!(hash1, hash2); // Same content should produce same hash
118
119        let different_content = "# Different Document\n\nThis is different content.";
120        let hash3 = FilesystemService::compute_content_hash(different_content);
121        assert_ne!(hash1, hash3); // Different content should produce different hash
122
123        // Test file hash
124        let temp_dir = tempdir().expect("Failed to create temp dir");
125        let file_path = temp_dir.path().join("test.md");
126        FilesystemService::write_file(&file_path, content).expect("Failed to write file");
127
128        let file_hash =
129            FilesystemService::compute_file_hash(&file_path).expect("Failed to compute file hash");
130        assert_eq!(hash1, file_hash); // File hash should match content hash
131    }
132
133    #[test]
134    fn test_file_operations() {
135        let temp_dir = tempdir().expect("Failed to create temp dir");
136        let file_path = temp_dir.path().join("subdir").join("test.md");
137
138        let content = "# Test Document";
139
140        // Write file (should create subdirectory)
141        FilesystemService::write_file(&file_path, content).expect("Failed to write file");
142        assert!(FilesystemService::file_exists(&file_path));
143
144        // Get modification time
145        let mtime = FilesystemService::get_file_mtime(&file_path).expect("Failed to get mtime");
146        assert!(mtime > 0.0);
147
148        // Delete file
149        FilesystemService::delete_file(&file_path).expect("Failed to delete file");
150        assert!(!FilesystemService::file_exists(&file_path));
151    }
152
153    #[test]
154    fn test_find_markdown_files() {
155        let temp_dir = tempdir().expect("Failed to create temp dir");
156        let base_path = temp_dir.path();
157
158        // Create some test files
159        let files = vec![
160            "doc1.md",
161            "subdir/doc2.md",
162            "subdir/nested/doc3.md",
163            "not_markdown.txt",
164        ];
165
166        for file in &files {
167            let file_path = base_path.join(file);
168            FilesystemService::write_file(&file_path, "# Test").expect("Failed to write file");
169        }
170
171        // Find markdown files
172        let found_files = FilesystemService::find_markdown_files(base_path)
173            .expect("Failed to find markdown files");
174
175        // Should find 3 .md files, not the .txt file
176        assert_eq!(found_files.len(), 3);
177
178        // All found files should end with .md
179        for file in &found_files {
180            assert!(file.ends_with(".md"));
181        }
182    }
183}