graphrag_cli/handlers/
file_ops.rs1use color_eyre::eyre::{eyre, Result};
6use std::path::{Path, PathBuf};
7use tokio::fs;
8
9pub struct FileOperations;
11
12impl FileOperations {
13 pub async fn exists(path: &Path) -> bool {
15 fs::metadata(path).await.is_ok()
16 }
17
18 pub async fn validate_file(path: &Path) -> Result<()> {
20 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 fs::metadata(path)
35 .await
36 .map_err(|e| eyre!("Cannot read file: {}", e))?;
37
38 Ok(())
39 }
40
41 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 #[allow(dead_code)]
52 pub async fn write_string(path: &Path, content: &str) -> Result<()> {
53 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 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 #[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 #[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 #[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}