vibetunnel/tauri/src-tauri/src/fs_api.rs
2025-06-23 14:58:10 +02:00

464 lines
12 KiB
Rust

use axum::{
extract::Query,
http::{header, StatusCode},
response::{IntoResponse, Response},
Json,
};
use base64::Engine;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tokio::fs;
use tokio::io::AsyncReadExt;
#[derive(Debug, Deserialize)]
pub struct FileQuery {
pub path: String,
}
#[derive(Debug, Serialize)]
pub struct FileMetadata {
pub name: String,
pub path: String,
pub size: u64,
pub is_dir: bool,
pub is_file: bool,
pub is_symlink: bool,
pub readonly: bool,
pub hidden: bool,
pub created: Option<String>,
pub modified: Option<String>,
pub accessed: Option<String>,
#[cfg(unix)]
pub permissions: Option<String>,
pub mime_type: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct MoveRequest {
pub from: String,
pub to: String,
}
#[derive(Debug, Deserialize)]
pub struct CopyRequest {
pub from: String,
pub to: String,
pub overwrite: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub struct WriteFileRequest {
pub path: String,
pub content: String,
pub encoding: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct OperationResult {
pub success: bool,
pub message: String,
}
/// Expand tilde to home directory
fn expand_path(path: &str) -> Result<PathBuf, StatusCode> {
if path.starts_with('~') {
let home = dirs::home_dir().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(home.join(path.strip_prefix("~/").unwrap_or("")))
} else {
Ok(PathBuf::from(path))
}
}
/// Get detailed file metadata
pub async fn get_file_info(
Query(params): Query<FileQuery>,
) -> Result<Json<FileMetadata>, StatusCode> {
let path = expand_path(&params.path)?;
let metadata = fs::metadata(&path)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
let name = path
.file_name().map_or_else(|| path.to_string_lossy().to_string(), |n| n.to_string_lossy().to_string());
let is_symlink = fs::symlink_metadata(&path)
.await
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
let hidden = name.starts_with('.');
let created = metadata
.created()
.map(|t| {
let datetime: chrono::DateTime<chrono::Utc> = t.into();
datetime.to_rfc3339()
})
.ok();
let modified = metadata
.modified()
.map(|t| {
let datetime: chrono::DateTime<chrono::Utc> = t.into();
datetime.to_rfc3339()
})
.ok();
let accessed = metadata
.accessed()
.map(|t| {
let datetime: chrono::DateTime<chrono::Utc> = t.into();
datetime.to_rfc3339()
})
.ok();
#[cfg(unix)]
let permissions = {
use std::os::unix::fs::PermissionsExt;
Some(format!("{:o}", metadata.permissions().mode() & 0o777))
};
let mime_type = if metadata.is_file() {
// Simple MIME type detection based on extension
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
Some(
match ext {
"txt" => "text/plain",
"html" | "htm" => "text/html",
"css" => "text/css",
"js" => "application/javascript",
"json" => "application/json",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"pdf" => "application/pdf",
"zip" => "application/zip",
_ => "application/octet-stream",
}
.to_string(),
)
} else {
None
};
Ok(Json(FileMetadata {
name,
path: path.to_string_lossy().to_string(),
size: metadata.len(),
is_dir: metadata.is_dir(),
is_file: metadata.is_file(),
is_symlink,
readonly: metadata.permissions().readonly(),
hidden,
created,
modified,
accessed,
#[cfg(unix)]
permissions,
#[cfg(not(unix))]
permissions: None,
mime_type,
}))
}
/// Read file contents
pub async fn read_file(Query(params): Query<FileQuery>) -> Result<Response, StatusCode> {
let path = expand_path(&params.path)?;
// Check if file exists and is a file
let metadata = fs::metadata(&path)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
if !metadata.is_file() {
return Err(StatusCode::BAD_REQUEST);
}
// Read file contents
let mut file = fs::File::open(&path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Determine content type
let content_type = path
.extension()
.and_then(|e| e.to_str())
.and_then(|ext| match ext {
"txt" => Some("text/plain"),
"html" | "htm" => Some("text/html"),
"css" => Some("text/css"),
"js" => Some("application/javascript"),
"json" => Some("application/json"),
"png" => Some("image/png"),
"jpg" | "jpeg" => Some("image/jpeg"),
"gif" => Some("image/gif"),
"pdf" => Some("application/pdf"),
_ => None,
})
.unwrap_or("application/octet-stream");
Ok(([(header::CONTENT_TYPE, content_type)], contents).into_response())
}
/// Write file contents
pub async fn write_file(
Json(req): Json<WriteFileRequest>,
) -> Result<Json<OperationResult>, StatusCode> {
let path = expand_path(&req.path)?;
// Ensure parent directory exists
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
// Write file
let content = if req.encoding.as_deref() == Some("base64") {
base64::engine::general_purpose::STANDARD
.decode(&req.content)
.map_err(|_| StatusCode::BAD_REQUEST)?
} else {
req.content.into_bytes()
};
fs::write(&path, content)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(OperationResult {
success: true,
message: format!("File written successfully: {}", path.display()),
}))
}
/// Delete file or directory
pub async fn delete_file(
Query(params): Query<FileQuery>,
) -> Result<Json<OperationResult>, StatusCode> {
let path = expand_path(&params.path)?;
// Check if path exists
let metadata = fs::metadata(&path)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
// Delete based on type
if metadata.is_dir() {
fs::remove_dir_all(&path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
} else {
fs::remove_file(&path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
Ok(Json(OperationResult {
success: true,
message: format!("Deleted: {}", path.display()),
}))
}
/// Move/rename file or directory
pub async fn move_file(Json(req): Json<MoveRequest>) -> Result<Json<OperationResult>, StatusCode> {
let from_path = expand_path(&req.from)?;
let to_path = expand_path(&req.to)?;
// Check if source exists
if !from_path.exists() {
return Err(StatusCode::NOT_FOUND);
}
// Check if destination already exists
if to_path.exists() {
return Err(StatusCode::CONFLICT);
}
// Ensure destination parent directory exists
if let Some(parent) = to_path.parent() {
fs::create_dir_all(parent)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
// Move the file/directory
fs::rename(&from_path, &to_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(OperationResult {
success: true,
message: format!(
"Moved from {} to {}",
from_path.display(),
to_path.display()
),
}))
}
/// Copy file or directory
pub async fn copy_file(Json(req): Json<CopyRequest>) -> Result<Json<OperationResult>, StatusCode> {
let from_path = expand_path(&req.from)?;
let to_path = expand_path(&req.to)?;
// Check if source exists
let metadata = fs::metadata(&from_path)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
// Check if destination already exists
if to_path.exists() && !req.overwrite.unwrap_or(false) {
return Err(StatusCode::CONFLICT);
}
// Ensure destination parent directory exists
if let Some(parent) = to_path.parent() {
fs::create_dir_all(parent)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
// Copy based on type
if metadata.is_file() {
fs::copy(&from_path, &to_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
} else if metadata.is_dir() {
// Recursive directory copy
copy_dir_recursive(&from_path, &to_path).await?;
}
Ok(Json(OperationResult {
success: true,
message: format!(
"Copied from {} to {}",
from_path.display(),
to_path.display()
),
}))
}
/// Recursively copy a directory
async fn copy_dir_recursive(from: &PathBuf, to: &PathBuf) -> Result<(), StatusCode> {
fs::create_dir_all(to)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut entries = fs::read_dir(from)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
while let Some(entry) = entries
.next_entry()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
{
let from_path = entry.path();
let to_path = to.join(entry.file_name());
let metadata = entry
.metadata()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if metadata.is_file() {
fs::copy(&from_path, &to_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
} else if metadata.is_dir() {
Box::pin(copy_dir_recursive(&from_path, &to_path)).await?;
}
}
Ok(())
}
/// Search for files matching a pattern
#[derive(Debug, Deserialize)]
pub struct SearchQuery {
pub path: String,
pub pattern: String,
pub max_depth: Option<u32>,
}
#[derive(Debug, Serialize)]
pub struct SearchResult {
pub path: String,
pub name: String,
pub is_dir: bool,
pub size: u64,
}
pub async fn search_files(
Query(params): Query<SearchQuery>,
) -> Result<Json<Vec<SearchResult>>, StatusCode> {
let base_path = expand_path(&params.path)?;
let pattern = params.pattern.to_lowercase();
let max_depth = params.max_depth.unwrap_or(5);
let mut results = Vec::new();
search_recursive(&base_path, &pattern, 0, max_depth, &mut results).await?;
Ok(Json(results))
}
async fn search_recursive(
path: &PathBuf,
pattern: &str,
depth: u32,
max_depth: u32,
results: &mut Vec<SearchResult>,
) -> Result<(), StatusCode> {
if depth > max_depth {
return Ok(());
}
let mut entries = fs::read_dir(path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
while let Some(entry) = entries
.next_entry()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
{
let entry_path = entry.path();
let file_name = entry.file_name().to_string_lossy().to_string();
if file_name.to_lowercase().contains(pattern) {
let metadata = entry
.metadata()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
results.push(SearchResult {
path: entry_path.to_string_lossy().to_string(),
name: file_name,
is_dir: metadata.is_dir(),
size: metadata.len(),
});
}
// Recurse into directories
if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
Box::pin(search_recursive(
&entry_path,
pattern,
depth + 1,
max_depth,
results,
))
.await?;
}
}
Ok(())
}