Skip to main content

graphrag_cli/handlers/
file_ops.rs

1//! File operations utilities
2//!
3//! Provides helpers for loading and validating files.
4
5use color_eyre::eyre::{eyre, Result};
6use std::path::{Path, PathBuf};
7use tokio::fs;
8
9/// File operations utility
10pub struct FileOperations;
11
12impl FileOperations {
13    /// Check if a file exists
14    pub async fn exists(path: &Path) -> bool {
15        fs::metadata(path).await.is_ok()
16    }
17
18    /// Validate that a file exists and is readable
19    pub async fn validate_file(path: &Path) -> Result<()> {
20        // Debug log the exact path being checked
21        tracing::debug!("Validating file path: {:?}", path);
22        tracing::debug!("Path as string: {}", path.display());
23        tracing::debug!("Path extension: {:?}", path.extension());
24
25        if !Self::exists(path).await {
26            return Err(eyre!("File not found: {}", path.display()));
27        }
28
29        if !path.is_file() {
30            return Err(eyre!("Path is not a file: {}", path.display()));
31        }
32
33        // Try to read metadata to check permissions
34        fs::metadata(path)
35            .await
36            .map_err(|e| eyre!("Cannot read file: {}", e))?;
37
38        Ok(())
39    }
40
41    /// Read a file as string
42    pub async fn read_to_string(path: &Path) -> Result<String> {
43        Self::validate_file(path).await?;
44
45        fs::read_to_string(path)
46            .await
47            .map_err(|e| eyre!("Failed to read file {}: {}", path.display(), e))
48    }
49
50    /// Write string to file
51    #[allow(dead_code)]
52    pub async fn write_string(path: &Path, content: &str) -> Result<()> {
53        // Create parent directory if it doesn't exist
54        if let Some(parent) = path.parent() {
55            fs::create_dir_all(parent)
56                .await
57                .map_err(|e| eyre!("Failed to create directory {}: {}", parent.display(), e))?;
58        }
59
60        fs::write(path, content)
61            .await
62            .map_err(|e| eyre!("Failed to write file {}: {}", path.display(), e))
63    }
64
65    /// Expand tilde (~) in path
66    pub fn expand_tilde(path: &Path) -> PathBuf {
67        if path.starts_with("~") {
68            if let Some(home) = dirs::home_dir() {
69                return home.join(path.strip_prefix("~").unwrap());
70            }
71        }
72        path.to_path_buf()
73    }
74
75    /// Resolve relative path to absolute
76    #[allow(dead_code)]
77    pub fn canonicalize(path: &Path) -> Result<PathBuf> {
78        let expanded = Self::expand_tilde(path);
79
80        if expanded.is_absolute() {
81            Ok(expanded)
82        } else {
83            std::env::current_dir()
84                .map(|cwd| cwd.join(expanded))
85                .map_err(|e| eyre!("Failed to get current directory: {}", e))
86        }
87    }
88
89    /// Get file extension
90    #[allow(dead_code)]
91    pub fn get_extension(path: &Path) -> Option<String> {
92        path.extension()
93            .and_then(|ext| ext.to_str())
94            .map(|s| s.to_lowercase())
95    }
96
97    /// Check if file is a supported document format
98    #[allow(dead_code)]
99    pub fn is_supported_document(path: &Path) -> bool {
100        if let Some(ext) = Self::get_extension(path) {
101            matches!(ext.as_str(), "txt" | "md" | "rst" | "log")
102        } else {
103            false
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_expand_tilde() {
114        let path = Path::new("~/test.txt");
115        let expanded = FileOperations::expand_tilde(path);
116
117        if let Some(home) = dirs::home_dir() {
118            assert_eq!(expanded, home.join("test.txt"));
119        }
120    }
121
122    #[test]
123    fn test_get_extension() {
124        assert_eq!(
125            FileOperations::get_extension(Path::new("test.txt")),
126            Some("txt".to_string())
127        );
128        assert_eq!(
129            FileOperations::get_extension(Path::new("test.TXT")),
130            Some("txt".to_string())
131        );
132        assert_eq!(FileOperations::get_extension(Path::new("test")), None);
133    }
134
135    #[test]
136    fn test_is_supported_document() {
137        assert!(FileOperations::is_supported_document(Path::new("test.txt")));
138        assert!(FileOperations::is_supported_document(Path::new("test.md")));
139        assert!(!FileOperations::is_supported_document(Path::new(
140            "test.pdf"
141        )));
142    }
143}