269 lines
8.4 KiB
Rust
269 lines
8.4 KiB
Rust
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<P: AsRef<Path>>(&self, path: P) -> Result<String> {
|
|
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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&self, dir: P) -> Result<Vec<FileInfo>> {
|
|
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<P: AsRef<Path>>(&self, path: P) -> bool {
|
|
path.as_ref().exists()
|
|
}
|
|
|
|
/// Get file info
|
|
pub fn get_file_info<P: AsRef<Path>>(&self, path: P) -> Result<FileInfo> {
|
|
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<P: AsRef<Path>>(&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<Option<String>> {
|
|
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<PathBuf> {
|
|
std::env::current_dir()
|
|
.context("Failed to get current directory")
|
|
}
|
|
|
|
/// Expand tilde in path
|
|
pub fn expand_path<P: AsRef<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<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
|
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<P: AsRef<Path>>(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])
|
|
}
|
|
}
|
|
} |