2025-11-22 12:26:16 -03:00
|
|
|
//! Drive Module - S3-based File Storage
|
|
|
|
|
//!
|
|
|
|
|
//! Provides file management operations using S3 as backend storage.
|
|
|
|
|
//! Supports bot storage and provides REST API endpoints for desktop frontend.
|
|
|
|
|
//!
|
|
|
|
|
//! API Endpoints:
|
|
|
|
|
//! - GET /files/list - List files and folders
|
|
|
|
|
//! - POST /files/read - Read file content
|
|
|
|
|
//! - POST /files/write - Write file content
|
|
|
|
|
//! - POST /files/delete - Delete file/folder
|
|
|
|
|
//! - POST /files/create-folder - Create new folder
|
|
|
|
|
|
2025-11-21 09:28:35 -03:00
|
|
|
use crate::shared::state::AppState;
|
|
|
|
|
use crate::ui_tree::file_tree::{FileTree, TreeNode};
|
2025-11-22 12:26:16 -03:00
|
|
|
use axum::{
|
|
|
|
|
extract::{Query, State},
|
|
|
|
|
http::StatusCode,
|
|
|
|
|
response::Json,
|
|
|
|
|
routing::{get, post},
|
|
|
|
|
Router,
|
|
|
|
|
};
|
2025-11-21 09:28:35 -03:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
pub mod vectordb;
|
|
|
|
|
|
|
|
|
|
// ===== Request/Response Structures =====
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
2025-11-21 09:28:35 -03:00
|
|
|
pub struct FileItem {
|
2025-11-22 12:26:16 -03:00
|
|
|
pub name: String,
|
|
|
|
|
pub path: String,
|
|
|
|
|
pub is_dir: bool,
|
|
|
|
|
pub size: Option<i64>,
|
|
|
|
|
pub modified: Option<String>,
|
|
|
|
|
pub icon: String,
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
#[derive(Debug, Deserialize)]
|
2025-11-21 09:28:35 -03:00
|
|
|
pub struct ListQuery {
|
2025-11-22 12:26:16 -03:00
|
|
|
pub path: Option<String>,
|
|
|
|
|
pub bucket: Option<String>,
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
#[derive(Debug, Deserialize)]
|
2025-11-21 09:28:35 -03:00
|
|
|
pub struct ReadRequest {
|
2025-11-22 12:26:16 -03:00
|
|
|
pub bucket: String,
|
|
|
|
|
pub path: String,
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
pub struct ReadResponse {
|
|
|
|
|
pub content: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
2025-11-21 09:28:35 -03:00
|
|
|
pub struct WriteRequest {
|
2025-11-22 12:26:16 -03:00
|
|
|
pub bucket: String,
|
|
|
|
|
pub path: String,
|
|
|
|
|
pub content: String,
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
#[derive(Debug, Deserialize)]
|
2025-11-21 09:28:35 -03:00
|
|
|
pub struct DeleteRequest {
|
2025-11-22 12:26:16 -03:00
|
|
|
pub bucket: String,
|
|
|
|
|
pub path: String,
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
#[derive(Debug, Deserialize)]
|
2025-11-21 09:28:35 -03:00
|
|
|
pub struct CreateFolderRequest {
|
2025-11-22 12:26:16 -03:00
|
|
|
pub bucket: String,
|
|
|
|
|
pub path: String,
|
|
|
|
|
pub name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
pub struct SuccessResponse {
|
|
|
|
|
pub success: bool,
|
|
|
|
|
pub message: Option<String>,
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
// ===== API Configuration =====
|
2025-11-21 09:28:35 -03:00
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
/// Configure drive API routes
|
|
|
|
|
pub fn configure() -> Router<Arc<AppState>> {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/files/list", get(list_files))
|
|
|
|
|
.route("/files/read", post(read_file))
|
|
|
|
|
.route("/files/write", post(write_file))
|
|
|
|
|
.route("/files/delete", post(delete_file))
|
|
|
|
|
.route("/files/create-folder", post(create_folder))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== API Handlers =====
|
|
|
|
|
|
|
|
|
|
/// GET /files/list - List files and folders in S3 bucket
|
|
|
|
|
pub async fn list_files(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Query(params): Query<ListQuery>,
|
|
|
|
|
) -> Result<Json<Vec<FileItem>>, (StatusCode, Json<serde_json::Value>)> {
|
|
|
|
|
// Use FileTree for hierarchical navigation
|
|
|
|
|
let mut tree = FileTree::new(state.clone());
|
|
|
|
|
|
|
|
|
|
let result = if let Some(bucket) = ¶ms.bucket {
|
|
|
|
|
if let Some(path) = ¶ms.path {
|
2025-11-21 09:28:35 -03:00
|
|
|
tree.enter_folder(bucket.clone(), path.clone()).await
|
|
|
|
|
} else {
|
|
|
|
|
tree.enter_bucket(bucket.clone()).await
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
tree.load_root().await
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Err(e) = result {
|
2025-11-22 12:26:16 -03:00
|
|
|
return Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({ "error": e.to_string() })),
|
|
|
|
|
));
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let items: Vec<FileItem> = tree
|
|
|
|
|
.render_items()
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(display, node)| {
|
|
|
|
|
let (name, path, is_dir, icon) = match node {
|
|
|
|
|
TreeNode::Bucket { name } => {
|
|
|
|
|
let icon = if name.ends_with(".gbai") {
|
|
|
|
|
"🤖"
|
|
|
|
|
} else {
|
|
|
|
|
"📦"
|
|
|
|
|
};
|
|
|
|
|
(name.clone(), name.clone(), true, icon.to_string())
|
|
|
|
|
}
|
|
|
|
|
TreeNode::Folder { bucket, path } => {
|
|
|
|
|
let name = path.split('/').last().unwrap_or(path).to_string();
|
|
|
|
|
(name, path.clone(), true, "📁".to_string())
|
|
|
|
|
}
|
|
|
|
|
TreeNode::File { bucket, path } => {
|
|
|
|
|
let name = path.split('/').last().unwrap_or(path).to_string();
|
2025-11-22 12:26:16 -03:00
|
|
|
let icon = get_file_icon(path);
|
|
|
|
|
(name, path.clone(), false, icon)
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
FileItem {
|
|
|
|
|
name,
|
|
|
|
|
path,
|
|
|
|
|
is_dir,
|
2025-11-22 12:26:16 -03:00
|
|
|
size: None,
|
|
|
|
|
modified: None,
|
2025-11-21 09:28:35 -03:00
|
|
|
icon,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
Ok(Json(items))
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
/// POST /files/read - Read file content from S3
|
|
|
|
|
pub async fn read_file(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Json(req): Json<ReadRequest>,
|
|
|
|
|
) -> Result<Json<ReadResponse>, (StatusCode, Json<serde_json::Value>)> {
|
|
|
|
|
let s3_client = state.drive.as_ref().ok_or_else(|| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
|
|
|
Json(serde_json::json!({ "error": "S3 service not available" })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let result = s3_client
|
|
|
|
|
.get_object()
|
|
|
|
|
.bucket(&req.bucket)
|
|
|
|
|
.key(&req.path)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({ "error": format!("Failed to read file: {}", e) })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let bytes = result
|
|
|
|
|
.body
|
|
|
|
|
.collect()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({ "error": format!("Failed to read file body: {}", e) })),
|
|
|
|
|
)
|
|
|
|
|
})?
|
|
|
|
|
.into_bytes();
|
|
|
|
|
|
|
|
|
|
let content = String::from_utf8(bytes.to_vec()).map_err(|e| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({ "error": format!("File is not valid UTF-8: {}", e) })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(ReadResponse { content }))
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
/// POST /files/write - Write file content to S3
|
|
|
|
|
pub async fn write_file(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Json(req): Json<WriteRequest>,
|
|
|
|
|
) -> Result<Json<SuccessResponse>, (StatusCode, Json<serde_json::Value>)> {
|
|
|
|
|
let s3_client = state.drive.as_ref().ok_or_else(|| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
|
|
|
Json(serde_json::json!({ "error": "S3 service not available" })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
s3_client
|
|
|
|
|
.put_object()
|
|
|
|
|
.bucket(&req.bucket)
|
|
|
|
|
.key(&req.path)
|
|
|
|
|
.body(req.content.into_bytes().into())
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({ "error": format!("Failed to write file: {}", e) })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(SuccessResponse {
|
|
|
|
|
success: true,
|
|
|
|
|
message: Some("File written successfully".to_string()),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// POST /files/delete - Delete file or folder from S3
|
|
|
|
|
pub async fn delete_file(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Json(req): Json<DeleteRequest>,
|
|
|
|
|
) -> Result<Json<SuccessResponse>, (StatusCode, Json<serde_json::Value>)> {
|
|
|
|
|
let s3_client = state.drive.as_ref().ok_or_else(|| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
|
|
|
Json(serde_json::json!({ "error": "S3 service not available" })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
// If path ends with /, it's a folder - delete all objects with this prefix
|
|
|
|
|
if req.path.ends_with('/') {
|
|
|
|
|
let result = s3_client
|
|
|
|
|
.list_objects_v2()
|
2025-11-21 09:28:35 -03:00
|
|
|
.bucket(&req.bucket)
|
2025-11-22 12:26:16 -03:00
|
|
|
.prefix(&req.path)
|
2025-11-21 09:28:35 -03:00
|
|
|
.send()
|
|
|
|
|
.await
|
2025-11-22 12:26:16 -03:00
|
|
|
.map_err(|e| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({ "error": format!("Failed to list objects for deletion: {}", e) })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
for obj in result.contents() {
|
|
|
|
|
if let Some(key) = obj.key() {
|
|
|
|
|
s3_client
|
|
|
|
|
.delete_object()
|
|
|
|
|
.bucket(&req.bucket)
|
|
|
|
|
.key(key)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({ "error": format!("Failed to delete object: {}", e) })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
}
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
} else {
|
2025-11-22 12:26:16 -03:00
|
|
|
s3_client
|
2025-11-21 09:28:35 -03:00
|
|
|
.delete_object()
|
|
|
|
|
.bucket(&req.bucket)
|
|
|
|
|
.key(&req.path)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
2025-11-22 12:26:16 -03:00
|
|
|
.map_err(|e| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({ "error": format!("Failed to delete file: {}", e) })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
2025-11-22 12:26:16 -03:00
|
|
|
|
|
|
|
|
Ok(Json(SuccessResponse {
|
|
|
|
|
success: true,
|
|
|
|
|
message: Some("Deleted successfully".to_string()),
|
|
|
|
|
}))
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
/// POST /files/create-folder - Create new folder in S3
|
|
|
|
|
pub async fn create_folder(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Json(req): Json<CreateFolderRequest>,
|
|
|
|
|
) -> Result<Json<SuccessResponse>, (StatusCode, Json<serde_json::Value>)> {
|
|
|
|
|
let s3_client = state.drive.as_ref().ok_or_else(|| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
|
|
|
Json(serde_json::json!({ "error": "S3 service not available" })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
2025-11-21 09:28:35 -03:00
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
// S3 doesn't have real folders, create an empty object with trailing /
|
|
|
|
|
let folder_path = if req.path.is_empty() || req.path == "/" {
|
|
|
|
|
format!("{}/", req.name)
|
2025-11-21 09:28:35 -03:00
|
|
|
} else {
|
2025-11-22 12:26:16 -03:00
|
|
|
format!("{}/{}/", req.path.trim_end_matches('/'), req.name)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
s3_client
|
|
|
|
|
.put_object()
|
|
|
|
|
.bucket(&req.bucket)
|
|
|
|
|
.key(&folder_path)
|
|
|
|
|
.body(Vec::new().into())
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({ "error": format!("Failed to create folder: {}", e) })),
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(SuccessResponse {
|
|
|
|
|
success: true,
|
|
|
|
|
message: Some("Folder created successfully".to_string()),
|
|
|
|
|
}))
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
// ===== Helper Functions =====
|
|
|
|
|
|
|
|
|
|
/// Get appropriate icon for file based on extension
|
|
|
|
|
fn get_file_icon(path: &str) -> String {
|
|
|
|
|
if path.ends_with(".bas") {
|
|
|
|
|
"⚙️".to_string()
|
|
|
|
|
} else if path.ends_with(".ast") {
|
|
|
|
|
"🔧".to_string()
|
|
|
|
|
} else if path.ends_with(".csv") {
|
|
|
|
|
"📊".to_string()
|
|
|
|
|
} else if path.ends_with(".gbkb") {
|
|
|
|
|
"📚".to_string()
|
|
|
|
|
} else if path.ends_with(".json") {
|
|
|
|
|
"🔖".to_string()
|
|
|
|
|
} else if path.ends_with(".txt") || path.ends_with(".md") {
|
|
|
|
|
"📃".to_string()
|
|
|
|
|
} else if path.ends_with(".pdf") {
|
|
|
|
|
"📕".to_string()
|
|
|
|
|
} else if path.ends_with(".zip") || path.ends_with(".tar") || path.ends_with(".gz") {
|
|
|
|
|
"📦".to_string()
|
|
|
|
|
} else if path.ends_with(".jpg") || path.ends_with(".png") || path.ends_with(".gif") {
|
|
|
|
|
"🖼️".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
"📄".to_string()
|
|
|
|
|
}
|
2025-11-21 09:28:35 -03:00
|
|
|
}
|