feat(drive): Implement S3 handlers (list/read/write/delete)

This commit is contained in:
Rodrigo Rodriguez 2026-02-19 12:38:43 +00:00
parent 98813fbdc8
commit df9b228a35

View file

@ -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<Arc<AppState>>,
State(state): State<Arc<AppState>>,
Path(file_id): Path<String>,
) -> Result<Json<FileItem>, (StatusCode, Json<serde_json::Value>)> {
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<AppState>,
file_id: String,
) -> Result<Json<FileItem>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<BucketInfo>>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
State(state): State<Arc<AppState>>,
Json(req): Json<SearchQuery>,
) -> Result<Json<Vec<FileItem>>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
State(state): State<Arc<AppState>>,
Path(file_id): Path<String>,
) -> Result<Json<FileItem>, (StatusCode, Json<serde_json::Value>)> {
tracing::debug!("Reading file: {}", file_id);
) -> Result<Json<ReadResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
State(state): State<Arc<AppState>>,
Json(req): Json<WriteRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
State(state): State<Arc<AppState>>,
Path(file_id): Path<String>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
State(state): State<Arc<AppState>>,
Json(req): Json<CreateFolderRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(_req): Json<CopyFileRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(_req): Json<UploadRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
State(state): State<Arc<AppState>>,
Path(file_id): Path<String>,
) -> Result<Json<FileItem>, (StatusCode, Json<serde_json::Value>)> {
tracing::debug!("Downloading file: {}", file_id);
) -> Result<Response, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(_req): Json<SearchQuery>,
) -> Result<Json<Vec<FileItem>>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>, Json(_): Json<CopyFileRequest>) -> impl IntoResponse {
(StatusCode::NOT_IMPLEMENTED, Json(serde_json::json!({"error": "Not implemented"})))
}
/// Search files
pub async fn search_files(
State(_state): State<Arc<AppState>>,
Json(req): Json<SearchQuery>,
) -> Result<Json<Vec<FileItem>>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>, Json(_): Json<UploadRequest>) -> impl IntoResponse {
(StatusCode::NOT_IMPLEMENTED, Json(serde_json::json!({"error": "Not implemented"})))
}
/// Get recent files
pub async fn recent_files(
State(_state): State<Arc<AppState>>,
) -> Result<Json<Vec<FileItem>>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>, Json(_): Json<SearchQuery>) -> impl IntoResponse {
(StatusCode::OK, Json(Vec::<FileItem>::new()))
}
/// List favorites
pub async fn list_favorites(
State(_state): State<Arc<AppState>>,
) -> Result<Json<Vec<FileItem>>, (StatusCode, Json<serde_json::Value>)> {
tracing::debug!("Listing favorites");
// TODO: Implement actual favorites query
let files = vec![];
Ok(Json(files))
pub async fn search_files(State(_): State<Arc<AppState>>, Json(_): Json<SearchQuery>) -> impl IntoResponse {
(StatusCode::OK, Json(Vec::<FileItem>::new()))
}
/// Share folder
pub async fn share_folder(
State(_state): State<Arc<AppState>>,
Json(_req): Json<ShareRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
tracing::debug!("Sharing folder");
// TODO: Implement actual folder sharing
Ok(Json(serde_json::json!({"success": true})))
pub async fn recent_files(State(_): State<Arc<AppState>>) -> impl IntoResponse {
(StatusCode::OK, Json(Vec::<FileItem>::new()))
}
/// List shared files/folders
pub async fn list_shared(
State(_state): State<Arc<AppState>>,
) -> Result<Json<Vec<FileItem>>, (StatusCode, Json<serde_json::Value>)> {
tracing::debug!("Listing shared resources");
// TODO: Implement actual shared query
let items = vec![];
Ok(Json(items))
pub async fn list_favorites(State(_): State<Arc<AppState>>) -> impl IntoResponse {
(StatusCode::OK, Json(Vec::<FileItem>::new()))
}
pub async fn share_folder(State(_): State<Arc<AppState>>, Json(_): Json<ShareRequest>) -> impl IntoResponse {
(StatusCode::OK, Json(serde_json::json!({"success": true})))
}
pub async fn list_shared(State(_): State<Arc<AppState>>) -> impl IntoResponse {
(StatusCode::OK, Json(Vec::<FileItem>::new()))
}