tycode_core/file/
access.rs1use 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 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 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}