diff --git a/src/drive/drive_handlers.rs b/src/drive/drive_handlers.rs index 46ef20336..836c0817e 100644 --- a/src/drive/drive_handlers.rs +++ b/src/drive/drive_handlers.rs @@ -1,256 +1,296 @@ -// Drive HTTP handlers extracted from drive/mod.rs +// Drive HTTP handlers implementation using S3 +// Extracted from drive/mod.rs and using aws-sdk-s3 + use crate::core::shared::state::AppState; use crate::drive::drive_types::*; use axum::{ extract::{Path, State}, - http::StatusCode, - Json, + http::{header, StatusCode}, + response::{IntoResponse, Json, Response}, + body::Body, }; +use aws_sdk_s3::primitives::ByteStream; use chrono::Utc; use std::collections::HashMap; use std::sync::Arc; -use uuid::Uuid; -/// Open a file for editing + +// Import ReadResponse from parent mod if not in drive_types +use super::ReadResponse; + +/// Open a file (get metadata) pub async fn open_file( - State(_state): State>, + State(state): State>, Path(file_id): Path, ) -> Result, (StatusCode, Json)> { - tracing::debug!("Opening file: {}", file_id); + read_metadata(state, file_id).await +} - // TODO: Implement actual file reading - let file_item = FileItem { +/// Helper to get file metadata +async fn read_metadata( + state: Arc, + file_id: String, +) -> Result, (StatusCode, Json)> { + let client = state.drive.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"error": "Drive not configured"})), + ))?; + let bucket = &state.bucket_name; + + let resp = client.head_object() + .bucket(bucket) + .key(&file_id) + .send() + .await + .map_err(|e| (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": e.to_string()}))))?; + + let item = FileItem { id: file_id.clone(), - name: "Untitled".to_string(), - file_type: "document".to_string(), - size: 0, - mime_type: "text/plain".to_string(), - created_at: Utc::now(), - modified_at: Utc::now(), + name: file_id.split('/').last().unwrap_or(&file_id).to_string(), + file_type: if file_id.ends_with('/') { "folder".to_string() } else { "file".to_string() }, + size: resp.content_length.unwrap_or(0), + mime_type: resp.content_type.unwrap_or_else(|| "application/octet-stream".to_string()), + created_at: Utc::now(), // S3 doesn't track creation time easily + modified_at: Utc::now(), // Simplified parent_id: None, url: None, thumbnail_url: None, - is_favorite: false, + is_favorite: false, // Not implemented in S3 tags: vec![], metadata: HashMap::new(), }; - - Ok(Json(file_item)) + Ok(Json(item)) } -/// List all buckets +/// List all buckets (or configured one) pub async fn list_buckets( - State(_state): State>, + State(state): State>, ) -> Result>, (StatusCode, Json)> { - tracing::debug!("Listing buckets"); + let client = state.drive.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"error": "Drive not configured"})), + ))?; - // TODO: Query database for buckets - let buckets = vec![]; - - Ok(Json(buckets)) + match client.list_buckets().send().await { + Ok(resp) => { + let buckets = resp.buckets.unwrap_or_default().iter().map(|b| { + BucketInfo { + id: b.name.clone().unwrap_or_default(), + name: b.name.clone().unwrap_or_default(), + created_at: Utc::now(), + file_count: 0, + total_size: 0, + } + }).collect(); + Ok(Json(buckets)) + }, + Err(_) => { + // Fallback + Ok(Json(vec![BucketInfo { + id: state.bucket_name.clone(), + name: state.bucket_name.clone(), + created_at: Utc::now(), + file_count: 0, + total_size: 0, + }])) + } + } } /// List files in a bucket pub async fn list_files( - State(_state): State>, + State(state): State>, Json(req): Json, ) -> Result>, (StatusCode, Json)> { - let query = req.query.clone().unwrap_or_default(); - let parent_path = req.parent_path.clone(); + let client = state.drive.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"error": "Drive not configured"})), + ))?; + let bucket = req.bucket.clone().unwrap_or_else(|| state.bucket_name.clone()); + let prefix = req.parent_path.clone().unwrap_or_default(); - tracing::debug!("Searching files: query={}, parent={:?}", query, parent_path); + let resp = client.list_objects_v2() + .bucket(&bucket) + .prefix(&prefix) + .send() + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))))?; - // TODO: Implement actual file search - let files = vec![]; + let files = resp.contents.unwrap_or_default().iter().map(|obj| { + let key = obj.key().unwrap_or_default(); + let name = key.split('/').last().unwrap_or(key).to_string(); + FileItem { + id: key.to_string(), + name, + file_type: if key.ends_with('/') { "folder".to_string() } else { "file".to_string() }, + size: obj.size.unwrap_or(0), + mime_type: "application/octet-stream".to_string(), + created_at: Utc::now(), + modified_at: Utc::now(), + parent_id: Some(prefix.clone()), + url: None, + thumbnail_url: None, + is_favorite: false, + tags: vec![], + metadata: HashMap::new(), + } + }).collect(); Ok(Json(files)) } -/// Read file content +/// Read file content (as text) pub async fn read_file( - State(_state): State>, + State(state): State>, Path(file_id): Path, -) -> Result, (StatusCode, Json)> { - tracing::debug!("Reading file: {}", file_id); +) -> Result, (StatusCode, Json)> { + let client = state.drive.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"error": "Drive not configured"})), + ))?; + let bucket = &state.bucket_name; - // TODO: Implement actual file reading - let file_item = FileItem { - id: file_id.clone(), - name: "Untitled".to_string(), - file_type: "document".to_string(), - size: 0, - mime_type: "text/plain".to_string(), - created_at: Utc::now(), - modified_at: Utc::now(), - parent_id: None, - url: None, - thumbnail_url: None, - is_favorite: false, - tags: vec![], - metadata: HashMap::new(), - }; + let resp = client.get_object() + .bucket(bucket) + .key(&file_id) + .send() + .await + .map_err(|e| (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": e.to_string()}))))?; - Ok(Json(file_item)) + let data = resp.body.collect().await.map_err(|e| + (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))) + )?.into_bytes(); + + let content = String::from_utf8(data.to_vec()).unwrap_or_else(|_| "[Binary Content]".to_string()); + + Ok(Json(ReadResponse { content })) } /// Write file content pub async fn write_file( - State(_state): State>, + State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { - let file_id = req.file_id.unwrap_or_else(|| Uuid::new_v4().to_string()); + let client = state.drive.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"error": "Drive not configured"})), + ))?; + let bucket = &state.bucket_name; + let key = req.file_id.ok_or((StatusCode::BAD_REQUEST, Json(serde_json::json!({"error": "Missing file_id"}))))?; - tracing::debug!("Writing file: {}", file_id); + client.put_object() + .bucket(bucket) + .key(&key) + .body(ByteStream::from(req.content.into_bytes())) + .send() + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))))?; - // TODO: Implement actual file writing Ok(Json(serde_json::json!({"success": true}))) } /// Delete a file pub async fn delete_file( - State(_state): State>, + State(state): State>, Path(file_id): Path, ) -> Result, (StatusCode, Json)> { - tracing::debug!("Deleting file: {}", file_id); + let client = state.drive.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"error": "Drive not configured"})), + ))?; + let bucket = &state.bucket_name; + + client.delete_object() + .bucket(bucket) + .key(&file_id) + .send() + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))))?; - // TODO: Implement actual file deletion Ok(Json(serde_json::json!({"success": true}))) } /// Create a folder pub async fn create_folder( - State(_state): State>, + State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { - let _parent_id = req.parent_id.clone().unwrap_or_default(); + let client = state.drive.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"error": "Drive not configured"})), + ))?; + let bucket = &state.bucket_name; + + // Construct folder path/key + let mut key = req.parent_id.unwrap_or_default(); + if !key.ends_with('/') && !key.is_empty() { + key.push('/'); + } + key.push_str(&req.name); + if !key.ends_with('/') { + key.push('/'); + } - tracing::debug!("Creating folder: {:?}", req.name); + client.put_object() + .bucket(bucket) + .key(&key) + .body(ByteStream::from_static(&[])) + .send() + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))))?; - // TODO: Implement actual folder creation Ok(Json(serde_json::json!({"success": true}))) } -/// Copy a file -pub async fn copy_file( - State(_state): State>, - Json(_req): Json, -) -> Result, (StatusCode, Json)> { - tracing::debug!("Copying file"); - - // TODO: Implement actual file copying - Ok(Json(serde_json::json!({"success": true}))) -} - -/// Upload file to drive -pub async fn upload_file_to_drive( - State(_state): State>, - Json(_req): Json, -) -> Result, (StatusCode, Json)> { - tracing::debug!("Uploading to drive"); - - // TODO: Implement actual file upload - Ok(Json(serde_json::json!({"success": true}))) -} - -/// Download file +/// Download file (stream) pub async fn download_file( - State(_state): State>, + State(state): State>, Path(file_id): Path, -) -> Result, (StatusCode, Json)> { - tracing::debug!("Downloading file: {}", file_id); +) -> Result)> { + let client = state.drive.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"error": "Drive not configured"})), + ))?; + let bucket = &state.bucket_name; - // TODO: Implement actual file download - let file_item = FileItem { - id: file_id.clone(), - name: "Download".to_string(), - file_type: "file".to_string(), - size: 0, - mime_type: "application/octet-stream".to_string(), - created_at: Utc::now(), - modified_at: Utc::now(), - parent_id: None, - url: None, - thumbnail_url: None, - is_favorite: false, - tags: vec![], - metadata: HashMap::new(), - }; + let resp = client.get_object() + .bucket(bucket) + .key(&file_id) + .send() + .await + .map_err(|e| (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": e.to_string()}))))?; - Ok(Json(file_item)) + let stream = Body::from_stream(resp.body); + + Ok(Response::builder() + .header(header::CONTENT_TYPE, "application/octet-stream") + .header(header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", file_id.split('/').last().unwrap_or("file"))) + .body(stream) + .unwrap()) } -/// List folder contents -pub async fn list_folder_contents( - State(_state): State>, - Json(_req): Json, -) -> Result>, (StatusCode, Json)> { - tracing::debug!("Listing folder contents"); - - // TODO: Implement actual folder listing - let files = vec![]; - - Ok(Json(files)) +// Stubs for others (list_shared, etc.) +pub async fn copy_file(State(_): State>, Json(_): Json) -> impl IntoResponse { + (StatusCode::NOT_IMPLEMENTED, Json(serde_json::json!({"error": "Not implemented"}))) } - -/// Search files -pub async fn search_files( - State(_state): State>, - Json(req): Json, -) -> Result>, (StatusCode, Json)> { - let query = req.query.clone().unwrap_or_default(); - let parent_path = req.parent_path.clone(); - - tracing::debug!("Searching files: query={:?}, parent_path={:?}", query, parent_path); - - // TODO: Implement actual file search - let files = vec![]; - - Ok(Json(files)) +pub async fn upload_file_to_drive(State(_): State>, Json(_): Json) -> impl IntoResponse { + (StatusCode::NOT_IMPLEMENTED, Json(serde_json::json!({"error": "Not implemented"}))) } - -/// Get recent files -pub async fn recent_files( - State(_state): State>, -) -> Result>, (StatusCode, Json)> { - tracing::debug!("Getting recent files"); - - // TODO: Implement actual recent files query - let files = vec![]; - - Ok(Json(files)) +pub async fn list_folder_contents(State(_): State>, Json(_): Json) -> impl IntoResponse { + (StatusCode::OK, Json(Vec::::new())) } - -/// List favorites -pub async fn list_favorites( - State(_state): State>, -) -> Result>, (StatusCode, Json)> { - tracing::debug!("Listing favorites"); - - // TODO: Implement actual favorites query - let files = vec![]; - - Ok(Json(files)) +pub async fn search_files(State(_): State>, Json(_): Json) -> impl IntoResponse { + (StatusCode::OK, Json(Vec::::new())) } - -/// Share folder -pub async fn share_folder( - State(_state): State>, - Json(_req): Json, -) -> Result, (StatusCode, Json)> { - tracing::debug!("Sharing folder"); - - // TODO: Implement actual folder sharing - Ok(Json(serde_json::json!({"success": true}))) +pub async fn recent_files(State(_): State>) -> impl IntoResponse { + (StatusCode::OK, Json(Vec::::new())) } - -/// List shared files/folders -pub async fn list_shared( - State(_state): State>, -) -> Result>, (StatusCode, Json)> { - tracing::debug!("Listing shared resources"); - - // TODO: Implement actual shared query - let items = vec![]; - - Ok(Json(items)) +pub async fn list_favorites(State(_): State>) -> impl IntoResponse { + (StatusCode::OK, Json(Vec::::new())) +} +pub async fn share_folder(State(_): State>, Json(_): Json) -> impl IntoResponse { + (StatusCode::OK, Json(serde_json::json!({"success": true}))) +} +pub async fn list_shared(State(_): State>) -> impl IntoResponse { + (StatusCode::OK, Json(Vec::::new())) }