Skip to main content

tycode_core/file/
access.rs

1use crate::file::resolver::Resolver;
2use anyhow::{Context, Result};
3use ignore::WalkBuilder;
4use std::path::PathBuf;
5use tokio::fs;
6
7#[derive(Clone)]
8pub struct FileAccessManager {
9    pub roots: Vec<String>,
10    resolver: Resolver,
11}
12
13impl FileAccessManager {
14    pub fn new(workspace_roots: Vec<PathBuf>) -> anyhow::Result<Self> {
15        let resolver = Resolver::new(workspace_roots)?;
16        let roots = resolver.roots();
17
18        Ok(Self { resolver, roots })
19    }
20
21    pub async fn read_file(&self, file_path: &str) -> Result<String> {
22        let path = self.resolve(file_path)?;
23
24        if !path.exists() {
25            anyhow::bail!("File not found: {}", file_path);
26        }
27
28        if !path.is_file() {
29            anyhow::bail!("Path is not a file: {}", file_path);
30        }
31
32        fs::read_to_string(&path)
33            .await
34            .with_context(|| format!("Failed to read file: {file_path}"))
35    }
36
37    pub async fn write_file(&self, file_path: &str, content: &str) -> Result<()> {
38        let path = self.resolve(file_path)?;
39
40        if let Some(parent) = path.parent() {
41            fs::create_dir_all(parent)
42                .await
43                .with_context(|| format!("Failed to create parent directories for: {file_path}"))?;
44        }
45
46        fs::write(&path, content)
47            .await
48            .with_context(|| format!("Failed to write file: {file_path}"))
49    }
50
51    pub async fn delete_file(&self, file_path: &str) -> Result<()> {
52        let path = self.resolve(file_path)?;
53
54        let metadata = fs::metadata(&path)
55            .await
56            .with_context(|| format!("Failed to get metadata for: {file_path}"))?;
57
58        if metadata.is_dir() {
59            fs::remove_dir(&path)
60                .await
61                .with_context(|| format!("Failed to delete directory: {file_path}"))?;
62        } else {
63            fs::remove_file(&path)
64                .await
65                .with_context(|| format!("Failed to delete file: {file_path}"))?;
66        }
67
68        Ok(())
69    }
70
71    pub async fn list_directory(&self, directory_path: &str) -> Result<Vec<PathBuf>> {
72        let dir_path = self.resolve(directory_path)?;
73
74        if !dir_path.exists() {
75            anyhow::bail!("Directory not found: {}", dir_path.display());
76        }
77
78        if !dir_path.is_dir() {
79            anyhow::bail!("Path is not a directory: {}", dir_path.display());
80        }
81
82        let mut paths = Vec::new();
83
84        for result in WalkBuilder::new(&dir_path)
85            .hidden(false)
86            .filter_entry(|entry| entry.file_name().to_string_lossy() != ".git")
87            .max_depth(Some(1))
88            .build()
89            .skip(1)
90        {
91            let entry = result?;
92            let path = entry.path();
93
94            let Ok(resolved) = self.resolver.canonicalize(path) else {
95                // Likely a sym link outside of the working directory (or a bug)
96                continue;
97            };
98
99            paths.push(resolved.virtual_path);
100        }
101
102        Ok(paths)
103    }
104
105    pub async fn file_exists(&self, file_path: &str) -> Result<bool> {
106        let path = self.resolve(file_path)?;
107        Ok(path.exists())
108    }
109
110    pub fn resolve(&self, virtual_path: &str) -> Result<PathBuf> {
111        let path = self.resolver.resolve_path(virtual_path)?;
112        Ok(path.real_path)
113    }
114
115    pub fn real_root(&self, workspace: &str) -> Option<PathBuf> {
116        self.resolver.root(workspace)
117    }
118
119    pub async fn list_all_files_recursive(
120        &self,
121        workspace: &str,
122        max_bytes: Option<usize>,
123    ) -> Result<Vec<PathBuf>> {
124        let real_root = self
125            .real_root(workspace)
126            .ok_or_else(|| anyhow::anyhow!("No real path found for workspace: {}", workspace))?;
127
128        let mut files = Vec::new();
129        let root_for_filter = real_root.clone();
130        let root_is_git_repo = real_root.join(".git").exists();
131
132        for result in WalkBuilder::new(&real_root)
133            .hidden(false)
134            .filter_entry(move |entry| {
135                if entry.file_name().to_string_lossy() == ".git" {
136                    return false;
137                }
138
139                if root_is_git_repo && entry.file_type().map_or(false, |ft| ft.is_dir()) {
140                    let is_root = entry.path() == root_for_filter;
141                    if !is_root && entry.path().join(".git").exists() {
142                        return false;
143                    }
144                }
145                true
146            })
147            .build()
148        {
149            let entry = result?;
150            let path = entry.path();
151
152            if !path.is_file() {
153                continue;
154            }
155
156            let Ok(resolved) = self.resolver.canonicalize(path) else {
157                // Likely a sym link outside of the working directory (or a bug)
158                continue;
159            };
160
161            files.push(resolved.virtual_path);
162        }
163
164        if let Some(limit) = max_bytes {
165            Ok(Self::truncate_by_bytes(files, limit))
166        } else {
167            Ok(files)
168        }
169    }
170
171    fn truncate_by_bytes(files: Vec<PathBuf>, max_bytes: usize) -> Vec<PathBuf> {
172        let mut result = Vec::new();
173        let mut current_bytes = 0;
174
175        for file in files {
176            let file_bytes = file.to_string_lossy().len() + 1;
177            if current_bytes + file_bytes > max_bytes {
178                break;
179            }
180            current_bytes += file_bytes;
181            result.push(file);
182        }
183
184        result
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use std::fs as std_fs;
192    use std::path::PathBuf;
193    use tempfile::tempdir;
194
195    #[tokio::test]
196    async fn test_new() {
197        let roots = vec![std::env::current_dir().unwrap()];
198        let manager = FileAccessManager::new(roots.clone()).unwrap();
199        assert_eq!(manager.roots.len(), 1);
200    }
201
202    #[tokio::test]
203    async fn test_read_file_success() {
204        let temp = tempdir().unwrap();
205        let workspace = temp.path().join("workspace");
206        std_fs::create_dir(&workspace).unwrap();
207        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
208
209        std_fs::write(workspace.join("test.txt"), "content").unwrap();
210        let content = manager.read_file("/workspace/test.txt").await.unwrap();
211        assert_eq!(content, "content");
212    }
213
214    #[tokio::test]
215    async fn test_read_file_not_found() {
216        let temp = tempdir().unwrap();
217        let workspace = temp.path().join("workspace");
218        std_fs::create_dir(&workspace).unwrap();
219        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
220
221        let err = manager
222            .read_file("/workspace/nonexistent.txt")
223            .await
224            .unwrap_err();
225        assert!(err.to_string().contains("File not found"));
226    }
227
228    #[tokio::test]
229    async fn test_read_file_not_file() {
230        let temp = tempdir().unwrap();
231        let workspace = temp.path().join("workspace");
232        std_fs::create_dir(&workspace).unwrap();
233        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
234
235        std_fs::create_dir(workspace.join("dir")).unwrap();
236        let err = manager.read_file("/workspace/dir").await.unwrap_err();
237        assert!(err.to_string().contains("Path is not a file"));
238    }
239
240    #[tokio::test]
241    async fn test_write_file_success() {
242        let temp = tempdir().unwrap();
243        let workspace = temp.path().join("workspace");
244        std_fs::create_dir(&workspace).unwrap();
245        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
246
247        manager
248            .write_file("/workspace/subdir/test.txt", "content")
249            .await
250            .unwrap();
251        let path = workspace.join("subdir/test.txt");
252        assert!(path.exists());
253        assert_eq!(std_fs::read_to_string(path).unwrap(), "content");
254    }
255
256    #[tokio::test]
257    async fn test_delete_file_success() {
258        let temp = tempdir().unwrap();
259        let workspace = temp.path().join("workspace");
260        std_fs::create_dir(&workspace).unwrap();
261        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
262
263        let path = workspace.join("test.txt");
264        std_fs::write(&path, "content").unwrap();
265        manager.delete_file("/workspace/test.txt").await.unwrap();
266        assert!(!path.exists());
267    }
268
269    #[tokio::test]
270    async fn test_delete_directory_success() {
271        let temp = tempdir().unwrap();
272        let workspace = temp.path().join("workspace");
273        std_fs::create_dir(&workspace).unwrap();
274        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
275
276        let dir_path = workspace.join("testdir");
277        std_fs::create_dir(&dir_path).unwrap();
278        manager.delete_file("/workspace/testdir").await.unwrap();
279        assert!(!dir_path.exists());
280    }
281
282    #[tokio::test]
283    async fn test_delete_file_not_found() {
284        let temp = tempdir().unwrap();
285        let workspace = temp.path().join("workspace");
286        std_fs::create_dir(&workspace).unwrap();
287        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
288
289        let err = manager
290            .delete_file("/workspace/nonexistent.txt")
291            .await
292            .unwrap_err();
293        assert!(err.to_string().contains("Failed to get metadata"));
294    }
295
296    #[tokio::test]
297    async fn test_list_directory_success() {
298        let temp = tempdir().unwrap();
299        let workspace = temp.path().join("workspace");
300        std_fs::create_dir(&workspace).unwrap();
301        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
302
303        std_fs::write(workspace.join("a.txt"), "content").unwrap();
304        std_fs::write(workspace.join("b.txt"), "content").unwrap();
305
306        let list = manager.list_directory("/workspace").await.unwrap();
307        assert_eq!(list.len(), 2);
308        assert!(list.contains(&PathBuf::from("/workspace/a.txt")));
309        assert!(list.contains(&PathBuf::from("/workspace/b.txt")));
310    }
311
312    #[tokio::test]
313    async fn test_list_directory_not_found() {
314        let temp = tempdir().unwrap();
315        let workspace = temp.path().join("workspace");
316        std_fs::create_dir(&workspace).unwrap();
317        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
318
319        let err = manager
320            .list_directory("/workspace/nonexistent")
321            .await
322            .unwrap_err();
323        assert!(err.to_string().contains("Directory not found"));
324    }
325
326    #[tokio::test]
327    async fn test_list_directory_not_dir() {
328        let temp = tempdir().unwrap();
329        let workspace = temp.path().join("workspace");
330        std_fs::create_dir(&workspace).unwrap();
331        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
332
333        std_fs::write(workspace.join("file.txt"), "content").unwrap();
334
335        let err = manager
336            .list_directory("/workspace/file.txt")
337            .await
338            .unwrap_err();
339        assert!(err.to_string().contains("Path is not a directory"));
340    }
341
342    #[tokio::test]
343    async fn test_file_exists_true() {
344        let temp = tempdir().unwrap();
345        let workspace = temp.path().join("workspace");
346        std_fs::create_dir(&workspace).unwrap();
347        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
348
349        std_fs::write(workspace.join("test.txt"), "content").unwrap();
350        let exists = manager.file_exists("/workspace/test.txt").await.unwrap();
351        assert!(exists);
352    }
353
354    #[tokio::test]
355    async fn test_file_exists_false() {
356        let temp = tempdir().unwrap();
357        let workspace = temp.path().join("workspace");
358        std_fs::create_dir(&workspace).unwrap();
359        let manager = FileAccessManager::new(vec![workspace.clone()]).unwrap();
360
361        let exists = manager.file_exists("/workspace/test.txt").await.unwrap();
362        assert!(!exists);
363    }
364}