307 lines
8.9 KiB
Rust
307 lines
8.9 KiB
Rust
use crate::file_watcher::FileWatcherState;
|
||
use serde::{Deserialize, Serialize};
|
||
use std::fs;
|
||
use std::path::Path;
|
||
use tauri::State;
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct FileNode {
|
||
pub name: String,
|
||
pub path: String,
|
||
pub file_type: String, // "file" or "directory"
|
||
pub children: Option<Vec<FileNode>>,
|
||
pub size: Option<u64>,
|
||
pub modified: Option<u64>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||
pub struct FileSystemChange {
|
||
pub path: String,
|
||
pub change_type: String, // "created", "modified", "deleted"
|
||
}
|
||
|
||
/// 读取文件内容
|
||
#[tauri::command]
|
||
pub async fn read_file(path: String) -> Result<String, String> {
|
||
fs::read_to_string(&path).map_err(|e| format!("Failed to read file: {}", e))
|
||
}
|
||
|
||
/// 写入文件内容
|
||
#[tauri::command]
|
||
pub async fn write_file(path: String, content: String) -> Result<(), String> {
|
||
fs::write(&path, content).map_err(|e| format!("Failed to write file: {}", e))
|
||
}
|
||
|
||
/// 读取目录树结构
|
||
#[tauri::command]
|
||
pub async fn read_directory_tree(
|
||
path: String,
|
||
max_depth: Option<u32>,
|
||
ignore_patterns: Option<Vec<String>>,
|
||
) -> Result<FileNode, String> {
|
||
let path = Path::new(&path);
|
||
if !path.exists() {
|
||
return Err(format!("Path does not exist: {}", path.display()));
|
||
}
|
||
|
||
let max_depth = max_depth.unwrap_or(5);
|
||
let ignore_patterns = ignore_patterns.unwrap_or_else(|| {
|
||
vec![
|
||
String::from("node_modules"),
|
||
String::from(".git"),
|
||
String::from("target"),
|
||
String::from("dist"),
|
||
String::from("build"),
|
||
String::from(".idea"),
|
||
String::from(".vscode"),
|
||
String::from("__pycache__"),
|
||
String::from(".DS_Store"),
|
||
]
|
||
});
|
||
|
||
read_directory_recursive(path, 0, max_depth, &ignore_patterns).map_err(|e| e.to_string())
|
||
}
|
||
|
||
fn read_directory_recursive(
|
||
path: &Path,
|
||
current_depth: u32,
|
||
max_depth: u32,
|
||
ignore_patterns: &[String],
|
||
) -> std::io::Result<FileNode> {
|
||
let name = path
|
||
.file_name()
|
||
.and_then(|n| n.to_str())
|
||
.unwrap_or("")
|
||
.to_string();
|
||
|
||
let metadata = fs::metadata(path)?;
|
||
|
||
let node = if metadata.is_dir() {
|
||
let mut children = Vec::new();
|
||
|
||
if current_depth < max_depth {
|
||
// Check if directory should be ignored
|
||
let should_ignore = ignore_patterns
|
||
.iter()
|
||
.any(|pattern| &name == pattern || name.starts_with('.'));
|
||
|
||
if !should_ignore {
|
||
let entries = fs::read_dir(path)?;
|
||
for entry in entries {
|
||
let entry = entry?;
|
||
let child_path = entry.path();
|
||
|
||
// Skip symlinks to avoid infinite loops
|
||
if let Ok(meta) = entry.metadata() {
|
||
if !meta.file_type().is_symlink() {
|
||
if let Ok(child_node) = read_directory_recursive(
|
||
&child_path,
|
||
current_depth + 1,
|
||
max_depth,
|
||
ignore_patterns,
|
||
) {
|
||
children.push(child_node);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Sort children: directories first, then files, alphabetically
|
||
children.sort_by(|a, b| match (a.file_type.as_str(), b.file_type.as_str()) {
|
||
("directory", "file") => std::cmp::Ordering::Less,
|
||
("file", "directory") => std::cmp::Ordering::Greater,
|
||
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
||
});
|
||
}
|
||
}
|
||
|
||
FileNode {
|
||
name,
|
||
path: path.to_string_lossy().to_string(),
|
||
file_type: String::from("directory"),
|
||
children: Some(children),
|
||
size: None,
|
||
modified: metadata
|
||
.modified()
|
||
.ok()
|
||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||
.map(|d| d.as_secs()),
|
||
}
|
||
} else {
|
||
FileNode {
|
||
name,
|
||
path: path.to_string_lossy().to_string(),
|
||
file_type: String::from("file"),
|
||
children: None,
|
||
size: Some(metadata.len()),
|
||
modified: metadata
|
||
.modified()
|
||
.ok()
|
||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||
.map(|d| d.as_secs()),
|
||
}
|
||
};
|
||
|
||
Ok(node)
|
||
}
|
||
|
||
/// 搜索文件
|
||
#[tauri::command]
|
||
pub async fn search_files_by_name(
|
||
base_path: String,
|
||
query: String,
|
||
max_results: Option<usize>,
|
||
) -> Result<Vec<String>, String> {
|
||
let base_path = Path::new(&base_path);
|
||
if !base_path.exists() {
|
||
return Err(format!("Path does not exist: {}", base_path.display()));
|
||
}
|
||
|
||
let query_lower = query.to_lowercase();
|
||
let max_results = max_results.unwrap_or(100);
|
||
let mut results = Vec::new();
|
||
|
||
search_recursive(base_path, &query_lower, &mut results, max_results)?;
|
||
|
||
Ok(results)
|
||
}
|
||
|
||
fn search_recursive(
|
||
dir: &Path,
|
||
query: &str,
|
||
results: &mut Vec<String>,
|
||
max_results: usize,
|
||
) -> Result<(), String> {
|
||
if results.len() >= max_results {
|
||
return Ok(());
|
||
}
|
||
|
||
let entries = fs::read_dir(dir).map_err(|e| format!("Failed to read directory: {}", e))?;
|
||
|
||
for entry in entries {
|
||
if results.len() >= max_results {
|
||
break;
|
||
}
|
||
|
||
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
|
||
let path = entry.path();
|
||
let file_name = path
|
||
.file_name()
|
||
.and_then(|n| n.to_str())
|
||
.unwrap_or("")
|
||
.to_lowercase();
|
||
|
||
if file_name.contains(query) {
|
||
results.push(path.to_string_lossy().to_string());
|
||
}
|
||
|
||
if path.is_dir() {
|
||
// Skip hidden directories and common ignore patterns
|
||
if !file_name.starts_with('.')
|
||
&& file_name != "node_modules"
|
||
&& file_name != "target"
|
||
&& file_name != "dist"
|
||
{
|
||
let _ = search_recursive(&path, query, results, max_results);
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 获取文件信息
|
||
#[tauri::command]
|
||
pub async fn get_file_info(path: String) -> Result<FileNode, String> {
|
||
let path = Path::new(&path);
|
||
if !path.exists() {
|
||
return Err(format!("Path does not exist: {}", path.display()));
|
||
}
|
||
|
||
let metadata = fs::metadata(path).map_err(|e| format!("Failed to get metadata: {}", e))?;
|
||
|
||
let name = path
|
||
.file_name()
|
||
.and_then(|n| n.to_str())
|
||
.unwrap_or("")
|
||
.to_string();
|
||
|
||
Ok(FileNode {
|
||
name,
|
||
path: path.to_string_lossy().to_string(),
|
||
file_type: if metadata.is_dir() {
|
||
String::from("directory")
|
||
} else {
|
||
String::from("file")
|
||
},
|
||
children: None,
|
||
size: if metadata.is_file() {
|
||
Some(metadata.len())
|
||
} else {
|
||
None
|
||
},
|
||
modified: metadata
|
||
.modified()
|
||
.ok()
|
||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||
.map(|d| d.as_secs()),
|
||
})
|
||
}
|
||
|
||
/// 监听文件系统变化
|
||
#[tauri::command]
|
||
pub async fn watch_directory(
|
||
watcher_state: State<'_, FileWatcherState>,
|
||
path: String,
|
||
recursive: Option<bool>,
|
||
) -> Result<(), String> {
|
||
let recursive = recursive.unwrap_or(false);
|
||
|
||
watcher_state.with_manager(|manager| manager.watch_path(&path, recursive))
|
||
}
|
||
|
||
/// 停止监听指定路径
|
||
#[tauri::command]
|
||
pub async fn unwatch_directory(
|
||
watcher_state: State<'_, FileWatcherState>,
|
||
path: String,
|
||
) -> Result<(), String> {
|
||
watcher_state.with_manager(|manager| manager.unwatch_path(&path))
|
||
}
|
||
|
||
/// 获取当前监听的路径列表
|
||
#[tauri::command]
|
||
pub async fn get_watched_paths(
|
||
watcher_state: State<'_, FileWatcherState>,
|
||
) -> Result<Vec<String>, String> {
|
||
watcher_state.with_manager(|manager| Ok(manager.get_watched_paths()))
|
||
}
|
||
|
||
/// 获取文件树(简化版,供文件浏览器使用)
|
||
#[tauri::command]
|
||
pub async fn get_file_tree(project_path: String) -> Result<Vec<FileNode>, String> {
|
||
let path = Path::new(&project_path);
|
||
if !path.exists() {
|
||
return Err(format!("Path does not exist: {}", path.display()));
|
||
}
|
||
|
||
let ignore_patterns = vec![
|
||
String::from("node_modules"),
|
||
String::from(".git"),
|
||
String::from("target"),
|
||
String::from("dist"),
|
||
String::from("build"),
|
||
String::from(".idea"),
|
||
String::from(".vscode"),
|
||
String::from("__pycache__"),
|
||
String::from(".DS_Store"),
|
||
];
|
||
|
||
// 增加最大深度为 10,以支持更深的文件夹结构
|
||
let root_node =
|
||
read_directory_recursive(path, 0, 10, &ignore_patterns).map_err(|e| e.to_string())?;
|
||
|
||
// Return children of root node if it has any
|
||
Ok(root_node.children.unwrap_or_default())
|
||
}
|