botserver/src/drive/mod.rs
Rodrigo Rodriguez (Pragmatismo) d0563391b6 ``` Add comprehensive email account management and user settings
interface

Implements multi-user authentication system with email account
management, profile settings, drive configuration, and security
controls. Includes database migrations for user accounts, email
credentials, preferences, and session management. Frontend provides
intuitive UI for adding IMAP/SMTP accounts with provider presets and
connection testing. Backend supports per-user vector databases for email
and file indexing with Zitadel SSO integration and automatic workspace
initialization. ```
2025-11-21 09:28:35 -03:00

253 lines
7.3 KiB
Rust

use crate::shared::state::AppState;
use crate::ui_tree::file_tree::{FileTree, TreeNode};
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Serialize, Deserialize)]
pub struct FileItem {
name: String,
path: String,
is_dir: bool,
icon: String,
}
#[derive(Deserialize)]
pub struct ListQuery {
path: Option<String>,
bucket: Option<String>,
}
#[derive(Deserialize)]
pub struct ReadRequest {
bucket: String,
path: String,
}
#[derive(Deserialize)]
pub struct WriteRequest {
bucket: String,
path: String,
content: String,
}
#[derive(Deserialize)]
pub struct DeleteRequest {
bucket: String,
path: String,
}
#[derive(Deserialize)]
pub struct CreateFolderRequest {
bucket: String,
path: String,
name: String,
}
async fn list_files(
query: web::Query<ListQuery>,
app_state: web::Data<Arc<AppState>>,
) -> impl Responder {
let mut tree = FileTree::new(app_state.get_ref().clone());
let result = if let Some(bucket) = &query.bucket {
if let Some(path) = &query.path {
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 {
return HttpResponse::InternalServerError().json(serde_json::json!({
"error": e.to_string()
}));
}
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();
let icon = if path.ends_with(".bas") {
"⚙️"
} else if path.ends_with(".ast") {
"🔧"
} else if path.ends_with(".csv") {
"📊"
} else if path.ends_with(".gbkb") {
"📚"
} else if path.ends_with(".json") {
"🔖"
} else if path.ends_with(".txt") || path.ends_with(".md") {
"📃"
} else {
"📄"
};
(name, path.clone(), false, icon.to_string())
}
};
FileItem {
name,
path,
is_dir,
icon,
}
})
.collect();
HttpResponse::Ok().json(items)
}
async fn read_file(
req: web::Json<ReadRequest>,
app_state: web::Data<Arc<AppState>>,
) -> impl Responder {
if let Some(drive) = &app_state.drive {
match drive
.get_object()
.bucket(&req.bucket)
.key(&req.path)
.send()
.await
{
Ok(response) => match response.body.collect().await {
Ok(data) => {
let bytes = data.into_bytes();
match String::from_utf8(bytes.to_vec()) {
Ok(content) => HttpResponse::Ok().json(serde_json::json!({
"content": content
})),
Err(_) => HttpResponse::BadRequest().json(serde_json::json!({
"error": "File is not valid UTF-8 text"
})),
}
}
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
"error": e.to_string()
})),
},
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
"error": e.to_string()
})),
}
} else {
HttpResponse::ServiceUnavailable().json(serde_json::json!({
"error": "Drive not connected"
}))
}
}
async fn write_file(
req: web::Json<WriteRequest>,
app_state: web::Data<Arc<AppState>>,
) -> impl Responder {
if let Some(drive) = &app_state.drive {
match drive
.put_object()
.bucket(&req.bucket)
.key(&req.path)
.body(req.content.clone().into_bytes().into())
.send()
.await
{
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"success": true
})),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
"error": e.to_string()
})),
}
} else {
HttpResponse::ServiceUnavailable().json(serde_json::json!({
"error": "Drive not connected"
}))
}
}
async fn delete_file(
req: web::Json<DeleteRequest>,
app_state: web::Data<Arc<AppState>>,
) -> impl Responder {
if let Some(drive) = &app_state.drive {
match drive
.delete_object()
.bucket(&req.bucket)
.key(&req.path)
.send()
.await
{
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"success": true
})),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
"error": e.to_string()
})),
}
} else {
HttpResponse::ServiceUnavailable().json(serde_json::json!({
"error": "Drive not connected"
}))
}
}
async fn create_folder(
req: web::Json<CreateFolderRequest>,
app_state: web::Data<Arc<AppState>>,
) -> impl Responder {
if let Some(drive) = &app_state.drive {
let folder_path = if req.path.is_empty() {
format!("{}/", req.name)
} else {
format!("{}/{}/", req.path, req.name)
};
match drive
.put_object()
.bucket(&req.bucket)
.key(&folder_path)
.body(Vec::new().into())
.send()
.await
{
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"success": true
})),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
"error": e.to_string()
})),
}
} else {
HttpResponse::ServiceUnavailable().json(serde_json::json!({
"error": "Drive not connected"
}))
}
}
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/files")
.route("/list", web::get().to(list_files))
.route("/read", web::post().to(read_file))
.route("/write", web::post().to(write_file))
.route("/delete", web::post().to(delete_file))
.route("/create-folder", web::post().to(create_folder)),
);
}