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,
|
2026-02-15 23:45:11 +01:00
|
|
|
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;
|
2026-02-15 23:45:11 +01:00
|
|
|
use crate::interfaces::middleware::auth::AuthUser;
|
2025-03-19 00:44:27 +01:00
|
|
|
|
2026-03-05 16:57:44 +01:00
|
|
|
/// Maximum number of items allowed in a single batch request.
|
|
|
|
|
/// Prevents fan-out amplification attacks and database connection exhaustion.
|
|
|
|
|
const MAX_BATCH_SIZE: usize = 1_000;
|
|
|
|
|
|
2026-02-12 09:41:25 +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>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// DTO for batch file operation requests
|
2025-03-19 00:44:27 +01:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct BatchFileOperationRequest {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// IDs of the files to process
|
2025-03-19 00:44:27 +01:00
|
|
|
pub file_ids: Vec<String>,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// 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>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// DTO for batch folder operation requests
|
2025-03-19 00:44:27 +01:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct BatchFolderOperationRequest {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// IDs of the folders to process
|
2025-03-19 00:44:27 +01:00
|
|
|
pub folder_ids: Vec<String>,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Whether the operation should be recursive
|
2025-03-19 00:44:27 +01:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub recursive: bool,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// 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>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// DTO for batch folder creation requests
|
2025-03-19 00:44:27 +01:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct BatchCreateFoldersRequest {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Details of the folders to create
|
2025-03-19 00:44:27 +01:00
|
|
|
pub folders: Vec<CreateFolderDetail>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Detail for folder creation
|
2025-03-19 00:44:27 +01:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct CreateFolderDetail {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Folder name
|
2025-03-19 00:44:27 +01:00
|
|
|
pub name: String,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Parent folder ID (optional)
|
2025-03-19 00:44:27 +01:00
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub parent_id: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// DTO for batch operation results
|
2025-03-19 00:44:27 +01:00
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
pub struct BatchOperationResponse<T> {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Successfully processed entities
|
2025-03-19 00:44:27 +01:00
|
|
|
pub successful: Vec<T>,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Failed operations with their error messages
|
2025-03-19 00:44:27 +01:00
|
|
|
pub failed: Vec<FailedOperation>,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Operation statistics
|
2025-03-19 00:44:27 +01:00
|
|
|
pub stats: BatchOperationStats,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Failed operation in a batch
|
2025-03-19 00:44:27 +01:00
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
pub struct FailedOperation {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Identifier of the entity that failed
|
2025-03-19 00:44:27 +01:00
|
|
|
pub id: String,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Error message
|
2025-03-19 00:44:27 +01:00
|
|
|
pub error: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Statistics for a batch operation
|
2025-03-19 00:44:27 +01:00
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
pub struct BatchOperationStats {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Total number of operations
|
2025-03-19 00:44:27 +01:00
|
|
|
pub total: usize,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Number of successful operations
|
2025-03-19 00:44:27 +01:00
|
|
|
pub successful: usize,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Number of failed operations
|
2025-03-19 00:44:27 +01:00
|
|
|
pub failed: usize,
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Total execution time in milliseconds
|
2025-03-19 00:44:27 +01:00
|
|
|
pub execution_time_ms: u128,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// 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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// 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(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// 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>,
|
2026-03-05 10:30:39 +01:00
|
|
|
auth_user: AuthUser,
|
2025-03-19 00:44:27 +01:00
|
|
|
Json(request): Json<BatchFileOperationRequest>,
|
|
|
|
|
) -> ApiResult<impl IntoResponse> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// 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-03-05 16:57:44 +01:00
|
|
|
if request.file_ids.len() > MAX_BATCH_SIZE {
|
|
|
|
|
return Ok((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"error": format!("Batch size {} exceeds maximum of {}", request.file_ids.len(), MAX_BATCH_SIZE)
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Execute batch operation
|
2026-02-14 01:29:34 +01:00
|
|
|
let result = state
|
|
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.move_files(request.file_ids, request.target_folder_id, auth_user.id)
|
2025-03-19 00:44:27 +01:00
|
|
|
.await
|
2026-03-05 13:15:34 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Batch move_files failed: {}", e);
|
2026-03-05 21:28:51 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Batch operation failed".to_string(),
|
|
|
|
|
)
|
2026-03-05 13:15:34 +01:00
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
|
2026-02-12 09:41:25 +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 {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
2025-03-19 00:44:27 +01:00
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::BAD_REQUEST // All failed
|
2025-03-19 00:44:27 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
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-12 09:41:25 +01:00
|
|
|
/// 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>,
|
2026-03-05 10:30:39 +01:00
|
|
|
auth_user: AuthUser,
|
2025-03-19 00:44:27 +01:00
|
|
|
Json(request): Json<BatchFileOperationRequest>,
|
|
|
|
|
) -> ApiResult<impl IntoResponse> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// 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-03-05 16:57:44 +01:00
|
|
|
if request.file_ids.len() > MAX_BATCH_SIZE {
|
|
|
|
|
return Ok((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"error": format!("Batch size {} exceeds maximum of {}", request.file_ids.len(), MAX_BATCH_SIZE)
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Execute batch operation
|
2026-02-14 01:29:34 +01:00
|
|
|
let result = state
|
|
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.copy_files(request.file_ids, request.target_folder_id, auth_user.id)
|
2025-03-19 00:44:27 +01:00
|
|
|
.await
|
2026-03-05 13:15:34 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Batch copy_files failed: {}", e);
|
2026-03-05 21:28:51 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Batch operation failed".to_string(),
|
|
|
|
|
)
|
2026-03-05 13:15:34 +01:00
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
|
2026-02-12 09:41:25 +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 {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
2025-03-19 00:44:27 +01:00
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::BAD_REQUEST // All failed
|
2025-03-19 00:44:27 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
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-12 09:41:25 +01:00
|
|
|
/// 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>,
|
2026-03-05 10:30:39 +01:00
|
|
|
auth_user: AuthUser,
|
2025-03-19 00:44:27 +01:00
|
|
|
Json(request): Json<BatchFileOperationRequest>,
|
|
|
|
|
) -> ApiResult<impl IntoResponse> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// 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-03-05 16:57:44 +01:00
|
|
|
if request.file_ids.len() > MAX_BATCH_SIZE {
|
|
|
|
|
return Ok((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"error": format!("Batch size {} exceeds maximum of {}", request.file_ids.len(), MAX_BATCH_SIZE)
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Execute batch operation
|
2026-02-14 01:29:34 +01:00
|
|
|
let result = state
|
|
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.delete_files(request.file_ids, auth_user.id)
|
2025-03-19 00:44:27 +01:00
|
|
|
.await
|
2026-03-05 13:15:34 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Batch delete_files failed: {}", e);
|
2026-03-05 21:28:51 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Batch operation failed".to_string(),
|
|
|
|
|
)
|
2026-03-05 13:15:34 +01:00
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
|
2026-02-12 09:41:25 +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 {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
2025-03-19 00:44:27 +01:00
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::BAD_REQUEST // All failed
|
2025-03-19 00:44:27 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
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-12 09:41:25 +01:00
|
|
|
/// 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>,
|
2026-02-16 00:22:42 +01:00
|
|
|
auth_user: AuthUser,
|
2025-03-19 00:44:27 +01:00
|
|
|
Json(request): Json<BatchFolderOperationRequest>,
|
|
|
|
|
) -> ApiResult<impl IntoResponse> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// 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-03-05 16:57:44 +01:00
|
|
|
if request.folder_ids.len() > MAX_BATCH_SIZE {
|
|
|
|
|
return Ok((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"error": format!("Batch size {} exceeds maximum of {}", request.folder_ids.len(), MAX_BATCH_SIZE)
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Execute batch operation
|
2026-02-14 01:29:34 +01:00
|
|
|
let result = state
|
|
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.delete_folders(request.folder_ids, request.recursive, auth_user.id)
|
2025-03-19 00:44:27 +01:00
|
|
|
.await
|
2026-03-05 13:15:34 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Batch delete_folders failed: {}", e);
|
2026-03-05 21:28:51 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Batch operation failed".to_string(),
|
|
|
|
|
)
|
2026-03-05 13:15:34 +01:00
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
|
2026-02-12 09:41:25 +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 {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
2025-03-19 00:44:27 +01:00
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::BAD_REQUEST // All failed
|
2025-03-19 00:44:27 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
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-12 09:41:25 +01:00
|
|
|
/// 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>,
|
2026-03-05 10:30:39 +01:00
|
|
|
auth_user: AuthUser,
|
2025-03-19 00:44:27 +01:00
|
|
|
Json(request): Json<BatchCreateFoldersRequest>,
|
|
|
|
|
) -> ApiResult<impl IntoResponse> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// 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-03-05 16:57:44 +01:00
|
|
|
if request.folders.len() > MAX_BATCH_SIZE {
|
|
|
|
|
return Ok((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"error": format!("Batch size {} exceeds maximum of {}", request.folders.len(), MAX_BATCH_SIZE)
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Execute batch operation
|
2026-02-14 01:29:34 +01:00
|
|
|
let result = state
|
|
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.create_folders(folders, auth_user.id)
|
2025-03-19 00:44:27 +01:00
|
|
|
.await
|
2026-03-05 13:15:34 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Batch create_folders failed: {}", e);
|
2026-03-05 21:28:51 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Batch operation failed".to_string(),
|
|
|
|
|
)
|
2026-03-05 13:15:34 +01:00
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
|
2026-02-12 09:41:25 +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 {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
2025-03-19 00:44:27 +01:00
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::BAD_REQUEST // All failed
|
2025-03-19 00:44:27 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// 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>,
|
2026-03-05 10:30:39 +01:00
|
|
|
auth_user: AuthUser,
|
2025-03-19 00:44:27 +01:00
|
|
|
Json(request): Json<BatchFileOperationRequest>,
|
|
|
|
|
) -> ApiResult<impl IntoResponse> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// 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-03-05 16:57:44 +01:00
|
|
|
if request.file_ids.len() > MAX_BATCH_SIZE {
|
|
|
|
|
return Ok((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"error": format!("Batch size {} exceeds maximum of {}", request.file_ids.len(), MAX_BATCH_SIZE)
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Execute batch operation
|
2026-02-14 01:29:34 +01:00
|
|
|
let result = state
|
|
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.get_multiple_files(request.file_ids, auth_user.id)
|
2025-03-19 00:44:27 +01:00
|
|
|
.await
|
2026-03-05 13:15:34 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Batch get_files failed: {}", e);
|
2026-03-05 21:28:51 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Batch operation failed".to_string(),
|
|
|
|
|
)
|
2026-03-05 13:15:34 +01:00
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
|
2026-02-12 09:41:25 +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 {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
2025-03-19 00:44:27 +01:00
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::BAD_REQUEST // All failed
|
2025-03-19 00:44:27 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
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-12 09:41:25 +01:00
|
|
|
/// 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>,
|
2026-03-05 10:30:39 +01:00
|
|
|
auth_user: AuthUser,
|
2025-03-19 00:44:27 +01:00
|
|
|
Json(request): Json<BatchFolderOperationRequest>,
|
|
|
|
|
) -> ApiResult<impl IntoResponse> {
|
2026-02-12 09:41:25 +01:00
|
|
|
// 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-03-05 16:57:44 +01:00
|
|
|
if request.folder_ids.len() > MAX_BATCH_SIZE {
|
|
|
|
|
return Ok((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"error": format!("Batch size {} exceeds maximum of {}", request.folder_ids.len(), MAX_BATCH_SIZE)
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Execute batch operation
|
2026-02-14 01:29:34 +01:00
|
|
|
let result = state
|
|
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.get_multiple_folders(request.folder_ids, auth_user.id)
|
2025-03-19 00:44:27 +01:00
|
|
|
.await
|
2026-03-05 13:15:34 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Batch get_folders failed: {}", e);
|
2026-03-05 21:28:51 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Batch operation failed".to_string(),
|
|
|
|
|
)
|
2026-03-05 13:15:34 +01:00
|
|
|
})?;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +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
|
|
|
|
2026-02-12 09:41:25 +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 {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
|
2025-03-19 00:44:27 +01:00
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
StatusCode::BAD_REQUEST // All failed
|
2025-03-19 00:44:27 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
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
|
|
|
}
|
2026-02-15 23:45:11 +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());
|
|
|
|
|
}
|
2026-03-05 16:57:44 +01:00
|
|
|
let combined_size = request.file_ids.len() + request.folder_ids.len();
|
|
|
|
|
if combined_size > MAX_BATCH_SIZE {
|
|
|
|
|
return Ok((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"error": format!("Batch size {} exceeds maximum of {}", combined_size, MAX_BATCH_SIZE)
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
2026-02-15 23:45:11 +01:00
|
|
|
|
|
|
|
|
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
|
2026-03-07 14:59:32 +01:00
|
|
|
.trash_files(request.file_ids, auth_user.id)
|
2026-02-15 23:45:11 +01:00
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(result) => {
|
|
|
|
|
all_successful.extend(result.successful);
|
|
|
|
|
all_failed.extend(
|
|
|
|
|
result
|
|
|
|
|
.failed
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(id, error)| FailedOperation { id, error }),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2026-03-05 13:15:34 +01:00
|
|
|
tracing::error!("Batch trash_files failed: {}", e);
|
2026-02-15 23:45:11 +01:00
|
|
|
return Ok((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
2026-03-05 13:15:34 +01:00
|
|
|
Json(serde_json::json!({ "error": "Batch trash operation failed" })),
|
2026-02-15 23:45:11 +01:00
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trash folders
|
|
|
|
|
if !request.folder_ids.is_empty() {
|
|
|
|
|
match state
|
|
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.trash_folders(request.folder_ids, auth_user.id)
|
2026-02-15 23:45:11 +01:00
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(result) => {
|
|
|
|
|
all_successful.extend(result.successful);
|
|
|
|
|
all_failed.extend(
|
|
|
|
|
result
|
|
|
|
|
.failed
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(id, error)| FailedOperation { id, error }),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2026-03-05 13:15:34 +01:00
|
|
|
tracing::error!("Batch trash_folders failed: {}", e);
|
2026-02-15 23:45:11 +01:00
|
|
|
return Ok((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
2026-03-05 13:15:34 +01:00
|
|
|
Json(serde_json::json!({ "error": "Batch trash operation failed" })),
|
2026-02-15 23:45:11 +01:00
|
|
|
)
|
|
|
|
|
.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>,
|
2026-02-16 00:22:42 +01:00
|
|
|
auth_user: AuthUser,
|
2026-02-15 23:45:11 +01:00
|
|
|
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());
|
|
|
|
|
}
|
2026-03-05 16:57:44 +01:00
|
|
|
if request.folder_ids.len() > MAX_BATCH_SIZE {
|
|
|
|
|
return Ok((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"error": format!("Batch size {} exceeds maximum of {}", request.folder_ids.len(), MAX_BATCH_SIZE)
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response());
|
|
|
|
|
}
|
2026-02-15 23:45:11 +01:00
|
|
|
|
|
|
|
|
let result = state
|
|
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.move_folders(request.folder_ids, request.target_folder_id, auth_user.id)
|
2026-02-15 23:45:11 +01:00
|
|
|
.await
|
2026-03-05 13:15:34 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Batch move_folders failed: {}", e);
|
2026-03-05 21:28:51 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Batch operation failed".to_string(),
|
|
|
|
|
)
|
2026-03-05 13:15:34 +01:00
|
|
|
})?;
|
2026-02-15 23:45:11 +01:00
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 23:12:19 +01:00
|
|
|
/// Handler for downloading multiple files and folders as a single ZIP.
|
|
|
|
|
///
|
|
|
|
|
/// The ZIP is written to a temporary file and streamed to the client,
|
|
|
|
|
/// so RAM usage is O(buffer_size) regardless of archive size.
|
2026-02-15 23:45:11 +01:00
|
|
|
pub async fn download_batch(
|
|
|
|
|
State(state): State<BatchHandlerState>,
|
2026-03-05 10:30:39 +01:00
|
|
|
auth_user: AuthUser,
|
2026-02-15 23:45:11 +01:00
|
|
|
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(),
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-03-05 16:57:44 +01:00
|
|
|
let combined_size = request.file_ids.len() + request.folder_ids.len();
|
|
|
|
|
if combined_size > MAX_BATCH_SIZE {
|
|
|
|
|
return Err((
|
|
|
|
|
StatusCode::BAD_REQUEST,
|
2026-03-05 21:28:51 +01:00
|
|
|
format!(
|
|
|
|
|
"Batch size {} exceeds maximum of {}",
|
|
|
|
|
combined_size, MAX_BATCH_SIZE
|
|
|
|
|
),
|
2026-03-05 16:57:44 +01:00
|
|
|
));
|
|
|
|
|
}
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2026-02-25 23:12:19 +01:00
|
|
|
let temp_file = state
|
2026-02-15 23:45:11 +01:00
|
|
|
.batch_service
|
2026-03-07 14:59:32 +01:00
|
|
|
.download_zip(request.file_ids, request.folder_ids, auth_user.id)
|
2026-02-15 23:45:11 +01:00
|
|
|
.await
|
2026-03-05 13:15:34 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Batch download ZIP failed: {}", e);
|
2026-03-05 21:28:51 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
"Batch download failed".to_string(),
|
|
|
|
|
)
|
2026-03-05 13:15:34 +01:00
|
|
|
})?;
|
2026-02-15 23:45:11 +01:00
|
|
|
|
2026-02-25 23:12:19 +01:00
|
|
|
// Read file size for Content-Length before splitting ownership
|
|
|
|
|
let file_size = temp_file
|
|
|
|
|
.as_file()
|
|
|
|
|
.metadata()
|
|
|
|
|
.map(|m| m.len())
|
2026-02-26 00:47:32 +01:00
|
|
|
.map_err(|e| {
|
2026-03-05 13:15:34 +01:00
|
|
|
tracing::error!("Failed to read temp file metadata: {}", e);
|
2026-02-26 00:47:32 +01:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
2026-03-05 13:15:34 +01:00
|
|
|
"Failed to prepare download".to_string(),
|
2026-02-26 00:47:32 +01:00
|
|
|
)
|
|
|
|
|
})?;
|
2026-02-25 23:12:19 +01:00
|
|
|
|
|
|
|
|
// Split into the already-open fd + auto-delete path
|
|
|
|
|
let (std_file, temp_path) = temp_file.into_parts();
|
|
|
|
|
let tokio_file = tokio::fs::File::from_std(std_file);
|
|
|
|
|
|
|
|
|
|
// Stream to client — O(64 KB) RAM regardless of ZIP size
|
|
|
|
|
let stream = tokio_util::io::ReaderStream::new(tokio_file);
|
|
|
|
|
let body = axum::body::Body::from_stream(stream);
|
|
|
|
|
|
2026-02-15 23:45:11 +01:00
|
|
|
let filename = format!("oxicloud-download-{}.zip", chrono::Utc::now().timestamp());
|
|
|
|
|
|
2026-02-25 23:12:19 +01:00
|
|
|
let mut response = Response::builder()
|
2026-02-15 23:45:11 +01:00
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header("Content-Type", "application/zip")
|
|
|
|
|
.header(
|
|
|
|
|
"Content-Disposition",
|
|
|
|
|
format!("attachment; filename=\"{}\"", filename),
|
|
|
|
|
)
|
2026-02-25 23:12:19 +01:00
|
|
|
.header("Content-Length", file_size)
|
|
|
|
|
.body(body)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Keep TempPath alive in response extensions so the file is only
|
|
|
|
|
// deleted AFTER the body stream finishes sending.
|
2026-02-26 00:47:32 +01:00
|
|
|
response
|
|
|
|
|
.extensions_mut()
|
|
|
|
|
.insert(std::sync::Arc::new(temp_path));
|
2026-02-25 23:12:19 +01:00
|
|
|
|
|
|
|
|
Ok(response)
|
2026-02-15 23:45:11 +01:00
|
|
|
}
|