use anyhow::{Result, Context}; use std::fs; use std::path::{Path, PathBuf}; use std::time::SystemTime; use crate::config::Config; #[derive(Debug, Clone)] pub struct FileInfo { pub path: PathBuf, pub name: String, pub size: u64, pub modified: SystemTime, pub is_readable: bool, pub is_writable: bool, } pub struct FileManager { config: Config, } impl FileManager { pub fn new(config: Config) -> Self { Self { config } } /// Read a file and return its contents pub fn read_file>(&self, path: P) -> Result { let path = path.as_ref(); let metadata = fs::metadata(path) .with_context(|| format!("Failed to get metadata for {}", path.display()))?; // Check file size limit let size_mb = metadata.len() / (1024 * 1024); if size_mb > self.config.files.max_file_size_mb { return Err(anyhow::anyhow!( "File {} is too large ({} MB > {} MB limit)", path.display(), size_mb, self.config.files.max_file_size_mb )); } let content = fs::read_to_string(path) .with_context(|| format!("Failed to read file {}", path.display()))?; Ok(content) } /// Write content to a file pub fn write_file>(&self, path: P, content: &str) -> Result<()> { let path = path.as_ref(); // Create backup if enabled if self.config.files.backup_files && path.exists() { self.create_backup(path)?; } // Ensure parent directory exists if let Some(parent) = path.parent() { fs::create_dir_all(parent) .with_context(|| format!("Failed to create directory {}", parent.display()))?; } fs::write(path, content) .with_context(|| format!("Failed to write file {}", path.display()))?; Ok(()) } /// Create a backup of the file fn create_backup>(&self, path: P) -> Result<()> { let path = path.as_ref(); let backup_path = path.with_extension(format!("{}.backup", path.extension().and_then(|s| s.to_str()).unwrap_or("txt"))); fs::copy(path, &backup_path) .with_context(|| format!("Failed to create backup at {}", backup_path.display()))?; Ok(()) } /// List files in a directory pub fn list_files>(&self, dir: P) -> Result> { let dir = dir.as_ref(); let entries = fs::read_dir(dir) .with_context(|| format!("Failed to read directory {}", dir.display()))?; let mut files = Vec::new(); for entry in entries { let entry = entry?; let path = entry.path(); if path.is_file() { let metadata = entry.metadata()?; let name = entry.file_name().to_string_lossy().to_string(); files.push(FileInfo { path: path.clone(), name, size: metadata.len(), modified: metadata.modified()?, is_readable: path.exists() && fs::File::open(&path).is_ok(), is_writable: !metadata.permissions().readonly(), }); } } // Sort by name files.sort_by(|a, b| a.name.cmp(&b.name)); Ok(files) } /// Check if a file exists pub fn file_exists>(&self, path: P) -> bool { path.as_ref().exists() } /// Get file info pub fn get_file_info>(&self, path: P) -> Result { let path = path.as_ref(); let metadata = fs::metadata(path) .with_context(|| format!("Failed to get metadata for {}", path.display()))?; Ok(FileInfo { path: path.to_path_buf(), name: path.file_name().unwrap_or_default().to_string_lossy().to_string(), size: metadata.len(), modified: metadata.modified()?, is_readable: path.exists() && fs::File::open(&path).is_ok(), is_writable: !metadata.permissions().readonly(), }) } /// Append content to a file pub fn append_file>(&self, path: P, content: &str) -> Result<()> { let path = path.as_ref(); use std::io::Write; let mut file = std::fs::OpenOptions::new() .create(true) .append(true) .open(path) .with_context(|| format!("Failed to open file for appending {}", path.display()))?; file.write_all(content.as_bytes()) .with_context(|| format!("Failed to append to file {}", path.display()))?; Ok(()) } /// Load project context file (OWLEN.md) pub fn load_project_context(&self) -> Result> { let context_file = &self.config.general.project_context_file; if self.file_exists(context_file) { match self.read_file(context_file) { Ok(content) => Ok(Some(content)), Err(_) => Ok(None), // File exists but can't read, return None instead of error } } else { Ok(None) } } /// Create a default project context file pub fn create_default_project_context(&self) -> Result<()> { let context_file = &self.config.general.project_context_file; if !self.file_exists(context_file) { let default_content = r#"# Project Context - OWLlama This file provides context about your project to the AI assistant. ## Project Description Describe your project here. ## Key Files and Structure List important files, directories, and their purposes. ## Technologies Used - Programming languages - Frameworks - Tools and dependencies ## Development Guidelines - Coding standards - Best practices - Testing approach ## Current Focus What you're currently working on or need help with. --- *This file is automatically loaded as context for AI conversations.* "#; self.write_file(context_file, default_content)?; } Ok(()) } } /// Utility functions for common file operations pub mod utils { use super::*; /// Get the current working directory pub fn get_current_dir() -> Result { std::env::current_dir() .context("Failed to get current directory") } /// Expand tilde in path pub fn expand_path>(path: P) -> PathBuf { let path_str = path.as_ref().to_string_lossy(); let expanded = shellexpand::tilde(&path_str); PathBuf::from(expanded.as_ref()) } /// Get relative path from current directory pub fn get_relative_path>(path: P) -> Result { let current_dir = get_current_dir()?; let absolute_path = path.as_ref().canonicalize() .context("Failed to canonicalize path")?; absolute_path.strip_prefix(¤t_dir) .map(|p| p.to_path_buf()) .or_else(|_| Ok(absolute_path)) } /// Check if path is a text file based on extension pub fn is_text_file>(path: P) -> bool { let path = path.as_ref(); if let Some(ext) = path.extension().and_then(|s| s.to_str()) { matches!(ext.to_lowercase().as_str(), "txt" | "md" | "rs" | "py" | "js" | "ts" | "html" | "css" | "json" | "toml" | "yaml" | "yml" | "xml" | "csv" | "log" | "sh" | "bash" | "c" | "cpp" | "h" | "hpp" | "java" | "go" | "php" | "rb" | "swift" | "kt" | "scala" | "r" | "sql" | "dockerfile" | "makefile" ) } else { // Files without extensions might be text (like Makefile, README, etc.) path.file_name() .and_then(|name| name.to_str()) .map(|name| name.chars().all(|c| c.is_ascii())) .unwrap_or(false) } } /// Format file size in human readable format pub fn format_file_size(size: u64) -> String { const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; let mut size = size as f64; let mut unit_index = 0; while size >= 1024.0 && unit_index < UNITS.len() - 1 { size /= 1024.0; unit_index += 1; } if unit_index == 0 { format!("{} {}", size as u64, UNITS[unit_index]) } else { format!("{:.1} {}", size, UNITS[unit_index]) } } }