Files
Oxicloud/src/interfaces/api/handlers/batch_handler.rs
T

622 lines
18 KiB
Rust
Raw Normal View History

2025-03-19 00:44:27 +01:00
use axum::{
2026-02-14 01:29:34 +01:00
extract::{Json, State},
2025-03-19 00:44:27 +01:00
http::StatusCode,
response::{IntoResponse, Response},
2025-03-19 00:44:27 +01:00
};
use serde::{Deserialize, Serialize};
2026-02-14 01:29:34 +01:00
use std::sync::Arc;
2025-03-19 00:44:27 +01:00
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
2026-02-14 01:29:34 +01:00
use crate::application::services::batch_operations::{
BatchOperationService, BatchResult, BatchStats,
};
2025-03-19 00:44:27 +01:00
use crate::interfaces::api::handlers::ApiResult;
use crate::interfaces::middleware::auth::AuthUser;
2025-03-19 00:44:27 +01:00
/// Shared state for the batch handler
2025-03-19 00:44:27 +01:00
#[derive(Clone)]
pub struct BatchHandlerState {
pub batch_service: Arc<BatchOperationService>,
}
/// DTO for batch file operation requests
2025-03-19 00:44:27 +01:00
#[derive(Debug, Deserialize)]
pub struct BatchFileOperationRequest {
/// IDs of the files to process
2025-03-19 00:44:27 +01:00
pub file_ids: Vec<String>,
/// Target folder ID (optional)
2025-03-19 00:44:27 +01:00
#[serde(skip_serializing_if = "Option::is_none")]
pub target_folder_id: Option<String>,
}
/// DTO for batch folder operation requests
2025-03-19 00:44:27 +01:00
#[derive(Debug, Deserialize)]
pub struct BatchFolderOperationRequest {
/// IDs of the folders to process
2025-03-19 00:44:27 +01:00
pub folder_ids: Vec<String>,
/// Whether the operation should be recursive
2025-03-19 00:44:27 +01:00
#[serde(default)]
pub recursive: bool,
/// Target folder ID (optional)
2025-03-19 00:44:27 +01:00
#[serde(skip_serializing_if = "Option::is_none")]
pub target_folder_id: Option<String>,
}
/// DTO for batch folder creation requests
2025-03-19 00:44:27 +01:00
#[derive(Debug, Deserialize)]
pub struct BatchCreateFoldersRequest {
/// Details of the folders to create
2025-03-19 00:44:27 +01:00
pub folders: Vec<CreateFolderDetail>,
}
/// Detail for folder creation
2025-03-19 00:44:27 +01:00
#[derive(Debug, Deserialize)]
pub struct CreateFolderDetail {
/// Folder name
2025-03-19 00:44:27 +01:00
pub name: String,
/// Parent folder ID (optional)
2025-03-19 00:44:27 +01:00
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
}
/// DTO for batch operation results
2025-03-19 00:44:27 +01:00
#[derive(Debug, Serialize)]
pub struct BatchOperationResponse<T> {
/// Successfully processed entities
2025-03-19 00:44:27 +01:00
pub successful: Vec<T>,
/// Failed operations with their error messages
2025-03-19 00:44:27 +01:00
pub failed: Vec<FailedOperation>,
/// Operation statistics
2025-03-19 00:44:27 +01:00
pub stats: BatchOperationStats,
}
/// Failed operation in a batch
2025-03-19 00:44:27 +01:00
#[derive(Debug, Serialize)]
pub struct FailedOperation {
/// Identifier of the entity that failed
2025-03-19 00:44:27 +01:00
pub id: String,
/// Error message
2025-03-19 00:44:27 +01:00
pub error: String,
}
/// Statistics for a batch operation
2025-03-19 00:44:27 +01:00
#[derive(Debug, Serialize)]
pub struct BatchOperationStats {
/// Total number of operations
2025-03-19 00:44:27 +01:00
pub total: usize,
/// Number of successful operations
2025-03-19 00:44:27 +01:00
pub successful: usize,
/// Number of failed operations
2025-03-19 00:44:27 +01:00
pub failed: usize,
/// Total execution time in milliseconds
2025-03-19 00:44:27 +01:00
pub execution_time_ms: u128,
}
/// Converts domain BatchStats to DTO
2025-03-19 00:44:27 +01:00
impl From<BatchStats> for BatchOperationStats {
fn from(stats: BatchStats) -> Self {
Self {
total: stats.total,
successful: stats.successful,
failed: stats.failed,
execution_time_ms: stats.execution_time_ms,
}
}
}
/// Converts domain BatchResult<T> to DTO
2025-03-19 00:44:27 +01:00
impl<T, U> From<BatchResult<T>> for BatchOperationResponse<U>
where
U: From<T>,
{
fn from(result: BatchResult<T>) -> Self {
let successful = result.successful.into_iter().map(U::from).collect();
2026-02-14 01:29:34 +01:00
let failed = result
.failed
.into_iter()
2025-03-19 00:44:27 +01:00
.map(|(id, error)| FailedOperation { id, error })
.collect();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Self {
successful,
failed,
stats: result.stats.into(),
}
}
}
/// Handler for moving multiple files in batch
2025-03-19 00:44:27 +01:00
pub async fn move_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verify there are files to process
2025-03-19 00:44:27 +01:00
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No file IDs provided"
2026-02-14 01:29:34 +01:00
})),
)
.into_response());
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
// Execute batch operation
2026-02-14 01:29:34 +01:00
let result = state
.batch_service
2025-03-19 00:44:27 +01:00
.move_files(request.file_ids, request.target_folder_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
2026-02-14 01:29:34 +01:00
// Convert result to DTO
2025-03-19 00:44:27 +01:00
let response: BatchOperationResponse<FileDto> = result.into();
2026-02-14 01:29:34 +01:00
// Determine status code based on results
2025-03-19 00:44:27 +01:00
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
2025-03-19 00:44:27 +01:00
} else {
StatusCode::BAD_REQUEST // All failed
2025-03-19 00:44:27 +01:00
}
} else {
StatusCode::OK // All successful
2025-03-19 00:44:27 +01:00
};
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok((status_code, Json(response)).into_response())
}
/// Handler for copying multiple files in batch
2025-03-19 00:44:27 +01:00
pub async fn copy_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verify there are files to process
2025-03-19 00:44:27 +01:00
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No file IDs provided"
2026-02-14 01:29:34 +01:00
})),
)
.into_response());
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
// Execute batch operation
2026-02-14 01:29:34 +01:00
let result = state
.batch_service
2025-03-19 00:44:27 +01:00
.copy_files(request.file_ids, request.target_folder_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
2026-02-14 01:29:34 +01:00
// Convert result to DTO
2025-03-19 00:44:27 +01:00
let response: BatchOperationResponse<FileDto> = result.into();
2026-02-14 01:29:34 +01:00
// Determine status code based on results
2025-03-19 00:44:27 +01:00
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
2025-03-19 00:44:27 +01:00
} else {
StatusCode::BAD_REQUEST // All failed
2025-03-19 00:44:27 +01:00
}
} else {
StatusCode::OK // All successful
2025-03-19 00:44:27 +01:00
};
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok((status_code, Json(response)).into_response())
}
/// Handler for deleting multiple files in batch
2025-03-19 00:44:27 +01:00
pub async fn delete_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verify there are files to process
2025-03-19 00:44:27 +01:00
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No file IDs provided"
2026-02-14 01:29:34 +01:00
})),
)
.into_response());
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
// Execute batch operation
2026-02-14 01:29:34 +01:00
let result = state
.batch_service
2025-03-19 00:44:27 +01:00
.delete_files(request.file_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
2026-02-14 01:29:34 +01:00
// Create custom response for string IDs
2025-03-19 00:44:27 +01:00
let response = BatchOperationResponse {
successful: result.successful,
2026-02-14 01:29:34 +01:00
failed: result
.failed
.into_iter()
2025-03-19 00:44:27 +01:00
.map(|(id, error)| FailedOperation { id, error })
.collect(),
stats: result.stats.into(),
};
2026-02-14 01:29:34 +01:00
// Determine status code based on results
2025-03-19 00:44:27 +01:00
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
2025-03-19 00:44:27 +01:00
} else {
StatusCode::BAD_REQUEST // All failed
2025-03-19 00:44:27 +01:00
}
} else {
StatusCode::OK // All successful
2025-03-19 00:44:27 +01:00
};
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok((status_code, Json(response)).into_response())
}
/// Handler for deleting multiple folders in batch
2025-03-19 00:44:27 +01:00
pub async fn delete_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verify there are folders to process
2025-03-19 00:44:27 +01:00
if request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No folder IDs provided"
2026-02-14 01:29:34 +01:00
})),
)
.into_response());
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
// Execute batch operation
2026-02-14 01:29:34 +01:00
let result = state
.batch_service
2025-03-19 00:44:27 +01:00
.delete_folders(request.folder_ids, request.recursive)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
2026-02-14 01:29:34 +01:00
// Create custom response for string IDs
2025-03-19 00:44:27 +01:00
let response = BatchOperationResponse {
successful: result.successful,
2026-02-14 01:29:34 +01:00
failed: result
.failed
.into_iter()
2025-03-19 00:44:27 +01:00
.map(|(id, error)| FailedOperation { id, error })
.collect(),
stats: result.stats.into(),
};
2026-02-14 01:29:34 +01:00
// Determine status code based on results
2025-03-19 00:44:27 +01:00
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
2025-03-19 00:44:27 +01:00
} else {
StatusCode::BAD_REQUEST // All failed
2025-03-19 00:44:27 +01:00
}
} else {
StatusCode::OK // All successful
2025-03-19 00:44:27 +01:00
};
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok((status_code, Json(response)).into_response())
}
/// Handler for creating multiple folders in batch
2025-03-19 00:44:27 +01:00
pub async fn create_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchCreateFoldersRequest>,
) -> ApiResult<impl IntoResponse> {
// Verify there are folders to process
2025-03-19 00:44:27 +01:00
if request.folders.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No folders provided"
2026-02-14 01:29:34 +01:00
})),
)
.into_response());
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
// Transform the format for the service
2026-02-14 01:29:34 +01:00
let folders = request
.folders
2025-03-19 00:44:27 +01:00
.into_iter()
.map(|detail| (detail.name, detail.parent_id))
.collect();
2026-02-14 01:29:34 +01:00
// Execute batch operation
2026-02-14 01:29:34 +01:00
let result = state
.batch_service
2025-03-19 00:44:27 +01:00
.create_folders(folders)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
2026-02-14 01:29:34 +01:00
// Convert result to DTO
2025-03-19 00:44:27 +01:00
let response: BatchOperationResponse<FolderDto> = result.into();
2026-02-14 01:29:34 +01:00
// Determine status code based on results
2025-03-19 00:44:27 +01:00
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
2025-03-19 00:44:27 +01:00
} else {
StatusCode::BAD_REQUEST // All failed
2025-03-19 00:44:27 +01:00
}
} else {
StatusCode::CREATED // All successful
2025-03-19 00:44:27 +01:00
};
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok((status_code, Json(response)).into_response())
}
/// Handler for getting multiple files in batch
2025-03-19 00:44:27 +01:00
pub async fn get_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verify there are files to process
2025-03-19 00:44:27 +01:00
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No file IDs provided"
2026-02-14 01:29:34 +01:00
})),
)
.into_response());
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
// Execute batch operation
2026-02-14 01:29:34 +01:00
let result = state
.batch_service
2025-03-19 00:44:27 +01:00
.get_multiple_files(request.file_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
2026-02-14 01:29:34 +01:00
// Convert result to DTO
2025-03-19 00:44:27 +01:00
let response: BatchOperationResponse<FileDto> = result.into();
2026-02-14 01:29:34 +01:00
// Determine status code based on results
2025-03-19 00:44:27 +01:00
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
2025-03-19 00:44:27 +01:00
} else {
StatusCode::BAD_REQUEST // All failed
2025-03-19 00:44:27 +01:00
}
} else {
StatusCode::OK // All successful
2025-03-19 00:44:27 +01:00
};
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok((status_code, Json(response)).into_response())
}
/// Handler for getting multiple folders in batch
2025-03-19 00:44:27 +01:00
pub async fn get_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verify there are folders to process
2025-03-19 00:44:27 +01:00
if request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No folder IDs provided"
2026-02-14 01:29:34 +01:00
})),
)
.into_response());
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
// Execute batch operation
2026-02-14 01:29:34 +01:00
let result = state
.batch_service
2025-03-19 00:44:27 +01:00
.get_multiple_folders(request.folder_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
2026-02-14 01:29:34 +01:00
// Convert result to DTO
2025-03-19 00:44:27 +01:00
let response: BatchOperationResponse<FolderDto> = result.into();
2026-02-14 01:29:34 +01:00
// Determine status code based on results
2025-03-19 00:44:27 +01:00
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
2025-03-19 00:44:27 +01:00
} else {
StatusCode::BAD_REQUEST // All failed
2025-03-19 00:44:27 +01:00
}
} else {
StatusCode::OK // All successful
2025-03-19 00:44:27 +01:00
};
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok((status_code, Json(response)).into_response())
2026-02-14 01:29:34 +01:00
}
/// DTO for batch trash operation requests
#[derive(Debug, Deserialize)]
pub struct BatchTrashRequest {
/// IDs of the files to move to trash
#[serde(default)]
pub file_ids: Vec<String>,
/// IDs of the folders to move to trash
#[serde(default)]
pub folder_ids: Vec<String>,
}
/// DTO for batch download requests
#[derive(Debug, Deserialize)]
pub struct BatchDownloadRequest {
/// IDs of the files to include in the ZIP
#[serde(default)]
pub file_ids: Vec<String>,
/// IDs of the folders to include in the ZIP
#[serde(default)]
pub folder_ids: Vec<String>,
}
/// Handler for moving multiple files and folders to trash in batch
pub async fn trash_batch(
State(state): State<BatchHandlerState>,
auth_user: AuthUser,
Json(request): Json<BatchTrashRequest>,
) -> ApiResult<impl IntoResponse> {
if request.file_ids.is_empty() && request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No file or folder IDs provided"
})),
)
.into_response());
}
let mut all_successful: Vec<String> = Vec::new();
let mut all_failed: Vec<FailedOperation> = Vec::new();
let total = request.file_ids.len() + request.folder_ids.len();
let start_time = std::time::Instant::now();
// Trash files
if !request.file_ids.is_empty() {
match state
.batch_service
.trash_files(request.file_ids, &auth_user.id)
.await
{
Ok(result) => {
all_successful.extend(result.successful);
all_failed.extend(
result
.failed
.into_iter()
.map(|(id, error)| FailedOperation { id, error }),
);
}
Err(e) => {
return Ok((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response());
}
}
}
// Trash folders
if !request.folder_ids.is_empty() {
match state
.batch_service
.trash_folders(request.folder_ids, &auth_user.id)
.await
{
Ok(result) => {
all_successful.extend(result.successful);
all_failed.extend(
result
.failed
.into_iter()
.map(|(id, error)| FailedOperation { id, error }),
);
}
Err(e) => {
return Ok((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response());
}
}
}
let successful_count = all_successful.len();
let failed_count = all_failed.len();
let response = BatchOperationResponse {
successful: all_successful,
failed: all_failed,
stats: BatchOperationStats {
total,
successful: successful_count,
failed: failed_count,
execution_time_ms: start_time.elapsed().as_millis(),
},
};
let status_code = if failed_count > 0 {
if successful_count > 0 {
StatusCode::PARTIAL_CONTENT
} else {
StatusCode::BAD_REQUEST
}
} else {
StatusCode::OK
};
Ok((status_code, Json(response)).into_response())
}
/// Handler for moving multiple folders in batch
pub async fn move_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> {
if request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "No folder IDs provided"
})),
)
.into_response());
}
let result = state
.batch_service
.move_folders(request.folder_ids, request.target_folder_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let response: BatchOperationResponse<FolderDto> = result.into();
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT
} else {
StatusCode::BAD_REQUEST
}
} else {
StatusCode::OK
};
Ok((status_code, Json(response)).into_response())
}
/// Handler for downloading multiple files and folders as a single ZIP
pub async fn download_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchDownloadRequest>,
) -> Result<Response, (StatusCode, String)> {
if request.file_ids.is_empty() && request.folder_ids.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
"No file or folder IDs provided".to_string(),
));
}
let zip_bytes = state
.batch_service
.download_zip(request.file_ids, request.folder_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let filename = format!("oxicloud-download-{}.zip", chrono::Utc::now().timestamp());
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/zip")
.header(
"Content-Disposition",
format!("attachment; filename=\"{}\"", filename),
)
.header("Content-Length", zip_bytes.len().to_string())
.body(axum::body::Body::from(zip_bytes))
.unwrap())
}