1use anyhow::{Context, Result};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10pub struct VirtualFileSystem {
12 root: PathBuf,
14 current_dir: PathBuf,
16 #[allow(dead_code)]
18 session_id: String,
19}
20
21impl VirtualFileSystem {
22 pub fn new(lesson_id: &str, session_id: &str) -> Result<Self> {
24 let root = Self::create_temp_root(lesson_id, session_id)?;
25 let current_dir = PathBuf::from("/lesson-home");
26
27 let mut vfs = Self {
28 root,
29 current_dir,
30 session_id: session_id.to_string(),
31 };
32
33 vfs.initialize_structure()?;
35
36 Ok(vfs)
37 }
38
39 fn create_temp_root(lesson_id: &str, session_id: &str) -> Result<PathBuf> {
41 let temp_dir = std::env::temp_dir();
42 let root = temp_dir.join(format!("arct-lesson-{}-{}", lesson_id, session_id));
43
44 fs::create_dir_all(&root)
45 .context("Failed to create virtual filesystem root")?;
46
47 Ok(root)
48 }
49
50 fn initialize_structure(&mut self) -> Result<()> {
52 let home = self.root.join("lesson-home");
53 fs::create_dir_all(&home)?;
54
55 let dirs = [
57 "Documents",
58 "Documents/homework",
59 "Downloads",
60 "Pictures",
61 "Pictures/family",
62 "Music",
63 "Videos",
64 "projects",
65 "projects/website",
66 ];
67
68 for dir in &dirs {
69 fs::create_dir_all(home.join(dir))?;
70 }
71
72 self.create_file("Documents/report.txt", "# Quarterly Report\n\nThis is a sample document.\n")?;
74 self.create_file("Documents/notes.md", "# Notes\n\n- Learn Linux commands\n- Practice navigation\n")?;
75 self.create_file("Downloads/software.zip", "[Binary data - sample file]\n")?;
76 self.create_file("Pictures/vacation.jpg", "[Image data - sample file]\n")?;
77 self.create_file("Pictures/family/photo.png", "[Image data - sample file]\n")?;
78 self.create_file("projects/website/index.html", "<!DOCTYPE html>\n<html>\n<head><title>My Site</title></head>\n<body><h1>Hello World</h1></body>\n</html>\n")?;
79 self.create_file("projects/website/style.css", "body { font-family: Arial; }\n")?;
80 self.create_file(".bashrc", "# Bash configuration\nexport PS1='$ '\n")?;
81
82 Ok(())
83 }
84
85 fn create_file(&self, path: &str, content: &str) -> Result<()> {
87 let full_path = self.root.join("lesson-home").join(path);
88 fs::write(full_path, content)?;
89 Ok(())
90 }
91
92 pub fn resolve_path(&self, virtual_path: &str) -> PathBuf {
94 if virtual_path == "~" || virtual_path == "" {
95 return self.root.join("lesson-home");
96 }
97
98 let path = PathBuf::from(virtual_path);
99
100 if let Ok(stripped) = path.strip_prefix("/lesson-home") {
102 return self.root.join("lesson-home").join(stripped);
103 }
104
105 if path.is_absolute() {
107 return self.root.join("lesson-home").join(path.strip_prefix("/").unwrap_or(&path));
108 }
109
110 let current = self.get_current_dir_real();
112 current.join(virtual_path)
113 }
114
115 pub fn get_current_dir(&self) -> &Path {
117 &self.current_dir
118 }
119
120 pub fn get_current_dir_real(&self) -> PathBuf {
122 if self.current_dir == PathBuf::from("/lesson-home") {
123 self.root.join("lesson-home")
124 } else {
125 let relative = self.current_dir.strip_prefix("/lesson-home").unwrap_or(&self.current_dir);
126 self.root.join("lesson-home").join(relative)
127 }
128 }
129
130 pub fn change_directory(&mut self, path: &str) -> Result<String> {
132 let target = if path == "~" || path.is_empty() {
133 PathBuf::from("/lesson-home")
134 } else if path == ".." {
135 if self.current_dir == PathBuf::from("/lesson-home") {
137 PathBuf::from("/lesson-home")
139 } else {
140 self.current_dir.parent()
141 .map(|p| p.to_path_buf())
142 .unwrap_or_else(|| PathBuf::from("/lesson-home"))
143 }
144 } else if path.starts_with('/') {
145 if path.starts_with("/lesson-home") {
147 PathBuf::from(path)
148 } else {
149 PathBuf::from("/lesson-home").join(path.trim_start_matches('/'))
150 }
151 } else {
152 self.current_dir.join(path)
154 };
155
156 let real_path = self.resolve_path(target.to_str().unwrap_or("/lesson-home"));
158 if !real_path.exists() {
159 anyhow::bail!("Directory not found: {}", path);
160 }
161 if !real_path.is_dir() {
162 anyhow::bail!("Not a directory: {}", path);
163 }
164
165 self.current_dir = target.clone();
166 Ok(target.to_string_lossy().to_string())
167 }
168
169 pub fn list_directory(&self, path: Option<&str>) -> Result<Vec<DirEntry>> {
171 let real_path = if let Some(p) = path {
172 self.resolve_path(p)
173 } else {
174 self.get_current_dir_real()
175 };
176
177 let mut entries = Vec::new();
178
179 for entry in fs::read_dir(&real_path)
180 .context("Failed to read directory")?
181 {
182 let entry = entry?;
183 let metadata = entry.metadata()?;
184
185 entries.push(DirEntry {
186 name: entry.file_name().to_string_lossy().to_string(),
187 is_dir: metadata.is_dir(),
188 size: metadata.len(),
189 });
190 }
191
192 entries.sort_by(|a, b| {
194 match (a.is_dir, b.is_dir) {
195 (true, false) => std::cmp::Ordering::Less,
196 (false, true) => std::cmp::Ordering::Greater,
197 _ => a.name.cmp(&b.name),
198 }
199 });
200
201 Ok(entries)
202 }
203
204 pub fn get_tree(&self, max_depth: usize) -> Vec<TreeNode> {
206 let root = self.root.join("lesson-home");
207 self.build_tree(&root, 0, max_depth)
208 }
209
210 fn build_tree(&self, path: &Path, depth: usize, max_depth: usize) -> Vec<TreeNode> {
212 if depth >= max_depth {
213 return vec![];
214 }
215
216 let mut nodes = Vec::new();
217
218 if let Ok(entries) = fs::read_dir(path) {
219 for entry in entries.flatten() {
220 let metadata = entry.metadata().ok();
221 let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
222 let name = entry.file_name().to_string_lossy().to_string();
223
224 let children = if is_dir {
225 self.build_tree(&entry.path(), depth + 1, max_depth)
226 } else {
227 vec![]
228 };
229
230 nodes.push(TreeNode {
231 name,
232 is_dir,
233 children,
234 is_current: self.get_current_dir_real() == entry.path(),
235 });
236 }
237 }
238
239 nodes.sort_by(|a, b| {
241 match (a.is_dir, b.is_dir) {
242 (true, false) => std::cmp::Ordering::Less,
243 (false, true) => std::cmp::Ordering::Greater,
244 _ => a.name.cmp(&b.name),
245 }
246 });
247
248 nodes
249 }
250
251 pub fn read_file(&self, path: &str) -> Result<String> {
253 let real_path = self.resolve_path(path);
254
255 if !real_path.exists() {
256 anyhow::bail!("No such file or directory: {}", path);
257 }
258
259 if real_path.is_dir() {
260 anyhow::bail!("Is a directory: {}", path);
261 }
262
263 fs::read_to_string(&real_path)
264 .context(format!("Failed to read file: {}", path))
265 }
266
267 pub fn create_directory(&self, path: &str, parents: bool) -> Result<()> {
269 let real_path = self.resolve_path(path);
270
271 if real_path.exists() {
272 anyhow::bail!("File or directory already exists: {}", path);
273 }
274
275 if parents {
276 fs::create_dir_all(&real_path)
277 } else {
278 fs::create_dir(&real_path)
279 }
280 .context(format!("Failed to create directory: {}", path))
281 }
282
283 pub fn touch_file(&self, path: &str) -> Result<()> {
285 let real_path = self.resolve_path(path);
286
287 if real_path.exists() {
288 let metadata = fs::metadata(&real_path)?;
290 let permissions = metadata.permissions();
291 fs::set_permissions(&real_path, permissions)?;
292 } else {
293 fs::write(&real_path, "")?;
295 }
296
297 Ok(())
298 }
299
300 pub fn remove(&self, path: &str, recursive: bool, force: bool) -> Result<()> {
302 let real_path = self.resolve_path(path);
303
304 if !real_path.exists() {
305 if force {
306 return Ok(()); }
308 anyhow::bail!("No such file or directory: {}", path);
309 }
310
311 if real_path.is_dir() {
312 if !recursive {
313 anyhow::bail!("Is a directory (use -r to remove): {}", path);
314 }
315 fs::remove_dir_all(&real_path)
316 } else {
317 fs::remove_file(&real_path)
318 }
319 .context(format!("Failed to remove: {}", path))
320 }
321
322 pub fn move_item(&self, source: &str, destination: &str) -> Result<()> {
324 let source_path = self.resolve_path(source);
325 let dest_path = self.resolve_path(destination);
326
327 if !source_path.exists() {
328 anyhow::bail!("No such file or directory: {}", source);
329 }
330
331 let final_dest = if dest_path.exists() && dest_path.is_dir() {
333 dest_path.join(source_path.file_name().unwrap_or_default())
334 } else {
335 dest_path
336 };
337
338 fs::rename(&source_path, &final_dest)
339 .context(format!("Failed to move {} to {}", source, destination))
340 }
341
342 pub fn copy(&self, source: &str, destination: &str, recursive: bool) -> Result<()> {
344 let source_path = self.resolve_path(source);
345 let dest_path = self.resolve_path(destination);
346
347 if !source_path.exists() {
348 anyhow::bail!("No such file or directory: {}", source);
349 }
350
351 if source_path.is_dir() {
352 if !recursive {
353 anyhow::bail!("Is a directory (use -r to copy): {}", source);
354 }
355
356 self.copy_dir_recursive(&source_path, &dest_path)?;
358 } else {
359 let final_dest = if dest_path.exists() && dest_path.is_dir() {
361 dest_path.join(source_path.file_name().unwrap_or_default())
362 } else {
363 dest_path
364 };
365
366 fs::copy(&source_path, &final_dest)
367 .context(format!("Failed to copy {} to {}", source, destination))?;
368 }
369
370 Ok(())
371 }
372
373 fn copy_dir_recursive(&self, source: &Path, destination: &Path) -> Result<()> {
375 fs::create_dir_all(destination)?;
376
377 for entry in fs::read_dir(source)? {
378 let entry = entry?;
379 let file_type = entry.file_type()?;
380 let dest_path = destination.join(entry.file_name());
381
382 if file_type.is_dir() {
383 self.copy_dir_recursive(&entry.path(), &dest_path)?;
384 } else {
385 fs::copy(entry.path(), dest_path)?;
386 }
387 }
388
389 Ok(())
390 }
391
392 pub fn write_file(&self, path: &str, content: &str, append: bool) -> Result<()> {
394 let real_path = self.resolve_path(path);
395
396 if append {
397 use std::io::Write;
398 let mut file = fs::OpenOptions::new()
399 .create(true)
400 .append(true)
401 .open(&real_path)?;
402 file.write_all(content.as_bytes())?;
403 } else {
404 fs::write(&real_path, content)?;
405 }
406
407 Ok(())
408 }
409
410 pub fn cleanup(&self) -> Result<()> {
412 if self.root.exists() {
413 fs::remove_dir_all(&self.root)
414 .context("Failed to cleanup virtual filesystem")?;
415 }
416 Ok(())
417 }
418}
419
420impl Drop for VirtualFileSystem {
421 fn drop(&mut self) {
422 let _ = self.cleanup();
423 }
424}
425
426#[derive(Debug, Clone)]
428pub struct DirEntry {
429 pub name: String,
430 pub is_dir: bool,
431 pub size: u64,
432}
433
434#[derive(Debug, Clone)]
436pub struct TreeNode {
437 pub name: String,
438 pub is_dir: bool,
439 pub children: Vec<TreeNode>,
440 pub is_current: bool,
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_create_vfs() {
449 let vfs = VirtualFileSystem::new("test", "session-123").unwrap();
450 assert!(vfs.root.exists());
451 assert_eq!(vfs.get_current_dir(), Path::new("/lesson-home"));
452 }
453
454 #[test]
455 fn test_change_directory() {
456 let mut vfs = VirtualFileSystem::new("test", "session-456").unwrap();
457
458 let new_path = vfs.change_directory("Documents").unwrap();
460 assert_eq!(new_path, "/lesson-home/Documents");
461
462 let new_path = vfs.change_directory("..").unwrap();
464 assert_eq!(new_path, "/lesson-home");
465 }
466
467 #[test]
468 fn test_list_directory() {
469 let vfs = VirtualFileSystem::new("test", "session-789").unwrap();
470 let entries = vfs.list_directory(None).unwrap();
471
472 assert!(entries.len() >= 7);
474
475 assert!(entries[0].is_dir || entries[0].name == ".bashrc");
477 }
478}