2025-03-17 21:28:08 +01:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
use axum::{
|
2025-03-19 00:44:27 +01:00
|
|
|
extract::{Path, State, Multipart, Query},
|
|
|
|
|
http::{StatusCode, header, HeaderName, HeaderValue, Response},
|
2025-03-17 21:28:08 +01:00
|
|
|
response::IntoResponse,
|
|
|
|
|
Json,
|
|
|
|
|
};
|
|
|
|
|
use serde::Deserialize;
|
2025-03-19 00:44:27 +01:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use futures::Stream;
|
2025-03-23 22:44:18 +01:00
|
|
|
use futures::StreamExt;
|
2025-03-19 00:44:27 +01:00
|
|
|
use std::task::{Context, Poll};
|
|
|
|
|
use std::pin::Pin;
|
2025-03-17 21:28:08 +01:00
|
|
|
|
2025-03-19 00:44:27 +01:00
|
|
|
use crate::application::services::file_service::{FileService, FileServiceError};
|
|
|
|
|
use crate::infrastructure::services::compression_service::{
|
|
|
|
|
CompressionService, GzipCompressionService, CompressionLevel
|
|
|
|
|
};
|
2025-03-26 18:33:22 +01:00
|
|
|
use crate::common::di::AppState;
|
2025-03-17 21:28:08 +01:00
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
type FileServiceState = Arc<FileService>;
|
|
|
|
|
type GlobalState = AppState;
|
2025-03-17 21:28:08 +01:00
|
|
|
|
|
|
|
|
/// Handler for file-related API endpoints
|
|
|
|
|
pub struct FileHandler;
|
|
|
|
|
|
2025-03-19 00:44:27 +01:00
|
|
|
// Simpler approach to make streams Unpin - use Pin<Box<dyn Stream>> directly
|
|
|
|
|
struct BoxedStream<T> {
|
|
|
|
|
inner: Pin<Box<dyn Stream<Item = T> + Send + 'static>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> Stream for BoxedStream<T> {
|
|
|
|
|
type Item = T;
|
|
|
|
|
|
|
|
|
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
|
|
|
// Accessing the field directly is safe because BoxedStream is not a structural pinning type
|
|
|
|
|
unsafe { self.get_unchecked_mut().inner.as_mut().poll_next(cx) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This is safe because BoxedStream's inner field is already Pin<Box<dyn Stream>>
|
|
|
|
|
impl<T> Unpin for BoxedStream<T> {}
|
|
|
|
|
|
|
|
|
|
impl<T> BoxedStream<T> {
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
fn new<S>(stream: S) -> Self
|
|
|
|
|
where
|
|
|
|
|
S: Stream<Item = T> + Send + 'static,
|
|
|
|
|
{
|
|
|
|
|
BoxedStream {
|
|
|
|
|
inner: Box::pin(stream),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-17 21:28:08 +01:00
|
|
|
impl FileHandler {
|
|
|
|
|
/// Uploads a file
|
|
|
|
|
pub async fn upload_file(
|
2025-03-26 18:33:22 +01:00
|
|
|
State(service): State<FileServiceState>,
|
2025-03-17 21:28:08 +01:00
|
|
|
mut multipart: Multipart,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
// Extract file from multipart request
|
|
|
|
|
let mut file_part = None;
|
|
|
|
|
let mut folder_id = None;
|
|
|
|
|
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::info!("Processing file upload request");
|
|
|
|
|
|
2025-03-17 21:28:08 +01:00
|
|
|
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
|
|
|
|
let name = field.name().unwrap_or("").to_string();
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::info!("Multipart field received: {}", name);
|
2025-03-17 21:28:08 +01:00
|
|
|
|
|
|
|
|
if name == "file" {
|
2025-03-23 22:44:18 +01:00
|
|
|
let filename = field.file_name().unwrap_or("unnamed").to_string();
|
|
|
|
|
let content_type = field.content_type().unwrap_or("application/octet-stream").to_string();
|
|
|
|
|
tracing::info!("File received: {} ({})", filename, content_type);
|
|
|
|
|
|
|
|
|
|
let bytes = field.bytes().await.unwrap_or_default();
|
|
|
|
|
tracing::info!("File size: {} bytes", bytes.len());
|
|
|
|
|
|
|
|
|
|
file_part = Some((filename, content_type, bytes));
|
2025-03-17 21:28:08 +01:00
|
|
|
} else if name == "folder_id" {
|
|
|
|
|
let folder_id_value = field.text().await.unwrap_or_default();
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::info!("folder_id received: {}", folder_id_value);
|
|
|
|
|
|
2025-03-17 21:28:08 +01:00
|
|
|
if !folder_id_value.is_empty() {
|
|
|
|
|
folder_id = Some(folder_id_value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if file was provided
|
|
|
|
|
if let Some((filename, content_type, data)) = file_part {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::info!("Uploading file '{}' to folder_id: {:?}", filename, folder_id);
|
|
|
|
|
|
|
|
|
|
// Use the proper file service to handle the upload
|
|
|
|
|
match service.upload_file_from_bytes(filename.clone(), folder_id.clone(), content_type.clone(), data.to_vec()).await {
|
|
|
|
|
Ok(file) => {
|
|
|
|
|
tracing::info!("File uploaded successfully: {} (ID: {})", filename, file.id);
|
|
|
|
|
|
|
|
|
|
// Log additional debugging information
|
|
|
|
|
tracing::info!("Created file details: folder_id={:?}, size={}, path={}",
|
|
|
|
|
file.folder_id, file.size, file.path);
|
|
|
|
|
|
|
|
|
|
// Return success response with file information
|
|
|
|
|
(StatusCode::CREATED, Json(file)).into_response()
|
|
|
|
|
},
|
2025-03-17 21:28:08 +01:00
|
|
|
Err(err) => {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::error!("Error uploading file '{}' through service: {}", filename, err);
|
|
|
|
|
|
|
|
|
|
// Return error response
|
2025-03-17 21:28:08 +01:00
|
|
|
let status = match &err {
|
2025-03-19 00:44:27 +01:00
|
|
|
FileServiceError::NotFound(_) => StatusCode::NOT_FOUND,
|
2025-03-23 22:44:18 +01:00
|
|
|
FileServiceError::AccessError(_) => StatusCode::SERVICE_UNAVAILABLE,
|
2025-03-17 21:28:08 +01:00
|
|
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(status, Json(serde_json::json!({
|
2025-03-23 22:44:18 +01:00
|
|
|
"error": format!("Error uploading file: {}", err)
|
2025-03-17 21:28:08 +01:00
|
|
|
}))).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::error!("Error: No file provided in request");
|
|
|
|
|
|
2025-03-17 21:28:08 +01:00
|
|
|
(StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
|
|
|
|
"error": "No file provided"
|
|
|
|
|
}))).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 00:44:27 +01:00
|
|
|
/// Downloads a file with optional compression
|
2025-03-17 21:28:08 +01:00
|
|
|
pub async fn download_file(
|
2025-03-26 18:33:22 +01:00
|
|
|
State(service): State<FileServiceState>,
|
2025-03-17 21:28:08 +01:00
|
|
|
Path(id): Path<String>,
|
2025-03-19 00:44:27 +01:00
|
|
|
Query(params): Query<HashMap<String, String>>,
|
2025-03-17 21:28:08 +01:00
|
|
|
) -> impl IntoResponse {
|
2025-03-19 00:44:27 +01:00
|
|
|
// Initialize compression service
|
|
|
|
|
let compression_service = GzipCompressionService::new();
|
|
|
|
|
|
|
|
|
|
// Check if compression is explicitly requested or rejected
|
|
|
|
|
let compression_param = params.get("compress").map(|v| v.as_str());
|
|
|
|
|
let force_compress = compression_param == Some("true") || compression_param == Some("1");
|
|
|
|
|
let force_no_compress = compression_param == Some("false") || compression_param == Some("0");
|
2025-03-17 21:28:08 +01:00
|
|
|
|
2025-03-19 00:44:27 +01:00
|
|
|
// Determine compression level from query params
|
|
|
|
|
let compression_level = match params.get("compression_level").map(|v| v.as_str()) {
|
|
|
|
|
Some("none") => CompressionLevel::None,
|
|
|
|
|
Some("fast") => CompressionLevel::Fast,
|
|
|
|
|
Some("best") => CompressionLevel::Best,
|
|
|
|
|
_ => CompressionLevel::Default, // Default or unrecognized
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get file info first to check it exists and get metadata
|
|
|
|
|
match service.get_file(&id).await {
|
|
|
|
|
Ok(file) => {
|
|
|
|
|
// Determine if we should compress based on file type and size
|
|
|
|
|
let should_compress = if force_no_compress {
|
|
|
|
|
false
|
|
|
|
|
} else if force_compress {
|
|
|
|
|
true
|
|
|
|
|
} else {
|
|
|
|
|
compression_service.should_compress(&file.mime_type, file.size)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Log compression decision for debugging
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
"Download file: name={}, size={}KB, mime={}, compress={}",
|
|
|
|
|
file.name, file.size / 1024, file.mime_type, should_compress
|
|
|
|
|
);
|
2025-03-17 21:28:08 +01:00
|
|
|
|
2025-03-19 00:44:27 +01:00
|
|
|
// For large files, use streaming response with potential compression
|
|
|
|
|
if file.size > 10 * 1024 * 1024 { // 10MB threshold for streaming
|
|
|
|
|
match service.get_file_content(&id).await {
|
|
|
|
|
Ok(content) => {
|
|
|
|
|
// Create base headers
|
|
|
|
|
let mut headers = HashMap::new();
|
|
|
|
|
headers.insert(
|
|
|
|
|
header::CONTENT_DISPOSITION.to_string(),
|
|
|
|
|
format!("attachment; filename=\"{}\"", file.name)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if should_compress {
|
|
|
|
|
// Add content-encoding header for compressed response
|
|
|
|
|
headers.insert(header::CONTENT_ENCODING.to_string(), "gzip".to_string());
|
|
|
|
|
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
|
|
|
|
|
headers.insert(header::VARY.to_string(), "Accept-Encoding".to_string());
|
|
|
|
|
|
|
|
|
|
// Compress the content
|
|
|
|
|
match compression_service.compress_data(&content, compression_level).await {
|
|
|
|
|
Ok(compressed_content) => {
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
"Compressed file: {} from {}KB to {}KB (ratio: {:.2})",
|
|
|
|
|
file.name,
|
|
|
|
|
content.len() / 1024,
|
|
|
|
|
compressed_content.len() / 1024,
|
|
|
|
|
content.len() as f64 / compressed_content.len() as f64
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Build a custom response with headers and body
|
|
|
|
|
let mut response = Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.body(axum::body::Body::from(compressed_content))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Add headers to response
|
|
|
|
|
for (name, value) in headers {
|
|
|
|
|
response.headers_mut().insert(
|
|
|
|
|
HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
|
|
|
|
HeaderValue::from_str(&value).unwrap()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!("Compression failed, sending uncompressed: {}", e);
|
|
|
|
|
// Fall back to uncompressed
|
|
|
|
|
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
|
|
|
|
|
|
|
|
|
|
// Build a custom response with headers and body
|
|
|
|
|
let mut response = Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.body(axum::body::Body::from(content))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Add headers to response
|
|
|
|
|
for (name, value) in headers {
|
|
|
|
|
response.headers_mut().insert(
|
|
|
|
|
HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
|
|
|
|
HeaderValue::from_str(&value).unwrap()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No compression, return as-is
|
|
|
|
|
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
|
|
|
|
|
|
|
|
|
|
// Build a custom response with headers and body
|
|
|
|
|
let mut response = Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.body(axum::body::Body::from(content))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Add headers to response
|
|
|
|
|
for (name, value) in headers {
|
|
|
|
|
response.headers_mut().insert(
|
|
|
|
|
HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
|
|
|
|
HeaderValue::from_str(&value).unwrap()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(err) => {
|
|
|
|
|
tracing::error!("Error getting file content: {}", err);
|
|
|
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
|
|
|
"error": format!("Error reading file: {}", err)
|
|
|
|
|
}))).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// For smaller files, load entirely but still potentially compress
|
|
|
|
|
match service.get_file_content(&id).await {
|
|
|
|
|
Ok(content) => {
|
|
|
|
|
// Create base headers
|
|
|
|
|
let mut headers = HashMap::new();
|
|
|
|
|
headers.insert(
|
|
|
|
|
header::CONTENT_DISPOSITION.to_string(),
|
|
|
|
|
format!("attachment; filename=\"{}\"", file.name)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if should_compress {
|
|
|
|
|
// Add content-encoding header for compressed response
|
|
|
|
|
headers.insert(header::CONTENT_ENCODING.to_string(), "gzip".to_string());
|
|
|
|
|
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
|
|
|
|
|
headers.insert(header::VARY.to_string(), "Accept-Encoding".to_string());
|
|
|
|
|
|
|
|
|
|
// Compress the content
|
|
|
|
|
match compression_service.compress_data(&content, compression_level).await {
|
|
|
|
|
Ok(compressed_content) => {
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
"Compressed file: {} from {}KB to {}KB (ratio: {:.2})",
|
|
|
|
|
file.name,
|
|
|
|
|
content.len() / 1024,
|
|
|
|
|
compressed_content.len() / 1024,
|
|
|
|
|
content.len() as f64 / compressed_content.len() as f64
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Build a custom response with headers and body
|
|
|
|
|
let mut response = Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.body(axum::body::Body::from(compressed_content))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Add headers to response
|
|
|
|
|
for (name, value) in headers {
|
|
|
|
|
response.headers_mut().insert(
|
|
|
|
|
HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
|
|
|
|
HeaderValue::from_str(&value).unwrap()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!("Compression failed, sending uncompressed: {}", e);
|
|
|
|
|
// Fall back to uncompressed
|
|
|
|
|
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
|
|
|
|
|
|
|
|
|
|
// Build a custom response with headers and body
|
|
|
|
|
let mut response = Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.body(axum::body::Body::from(content))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Add headers to response
|
|
|
|
|
for (name, value) in headers {
|
|
|
|
|
response.headers_mut().insert(
|
|
|
|
|
HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
|
|
|
|
HeaderValue::from_str(&value).unwrap()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No compression, return as-is
|
|
|
|
|
headers.insert(header::CONTENT_TYPE.to_string(), file.mime_type.clone());
|
|
|
|
|
|
|
|
|
|
// Build a custom response with headers and body
|
|
|
|
|
let mut response = Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.body(axum::body::Body::from(content))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Add headers to response
|
|
|
|
|
for (name, value) in headers {
|
|
|
|
|
response.headers_mut().insert(
|
|
|
|
|
HeaderName::from_bytes(name.as_bytes()).unwrap(),
|
|
|
|
|
HeaderValue::from_str(&value).unwrap()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(err) => {
|
|
|
|
|
tracing::error!("Error getting file content: {}", err);
|
|
|
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
|
|
|
"error": format!("Error reading file: {}", err)
|
|
|
|
|
}))).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-17 21:28:08 +01:00
|
|
|
},
|
2025-03-19 00:44:27 +01:00
|
|
|
Err(err) => {
|
2025-03-17 21:28:08 +01:00
|
|
|
let status = match &err {
|
2025-03-19 00:44:27 +01:00
|
|
|
FileServiceError::NotFound(_) => StatusCode::NOT_FOUND,
|
|
|
|
|
FileServiceError::AccessError(_) => StatusCode::SERVICE_UNAVAILABLE,
|
2025-03-17 21:28:08 +01:00
|
|
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(status, Json(serde_json::json!({
|
|
|
|
|
"error": err.to_string()
|
|
|
|
|
}))).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lists files, optionally filtered by folder ID
|
|
|
|
|
pub async fn list_files(
|
2025-03-26 18:33:22 +01:00
|
|
|
State(service): State<FileServiceState>,
|
2025-03-17 21:28:08 +01:00
|
|
|
folder_id: Option<&str>,
|
|
|
|
|
) -> impl IntoResponse {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::info!("Listing files with folder_id: {:?}", folder_id);
|
|
|
|
|
|
|
|
|
|
// Simply use the file service to list files
|
2025-03-17 21:28:08 +01:00
|
|
|
match service.list_files(folder_id).await {
|
|
|
|
|
Ok(files) => {
|
2025-03-23 22:44:18 +01:00
|
|
|
// Log success for debugging purposes
|
|
|
|
|
tracing::info!("Found {} files through the service", files.len());
|
|
|
|
|
|
|
|
|
|
if !files.is_empty() {
|
|
|
|
|
tracing::info!("First file in service list: {} (ID: {})",
|
|
|
|
|
files[0].name, files[0].id);
|
|
|
|
|
} else {
|
|
|
|
|
tracing::info!("No files found in folder through service");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return the files as JSON response
|
2025-03-17 21:28:08 +01:00
|
|
|
(StatusCode::OK, Json(files)).into_response()
|
|
|
|
|
},
|
|
|
|
|
Err(err) => {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::error!("Error listing files through service: {}", err);
|
|
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
let status = StatusCode::INTERNAL_SERVER_ERROR;
|
2025-03-17 21:28:08 +01:00
|
|
|
|
|
|
|
|
// Return a JSON error response
|
|
|
|
|
(status, Json(serde_json::json!({
|
|
|
|
|
"error": err.to_string()
|
|
|
|
|
}))).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
/// Deletes a file (with trash support)
|
2025-03-17 21:28:08 +01:00
|
|
|
pub async fn delete_file(
|
2025-03-26 18:33:22 +01:00
|
|
|
State(state): State<GlobalState>,
|
2025-03-17 21:28:08 +01:00
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> impl IntoResponse {
|
2025-03-26 18:33:22 +01:00
|
|
|
// Check if trash service is available
|
|
|
|
|
if let Some(trash_service) = &state.trash_service {
|
|
|
|
|
tracing::info!("Moving file to trash: {}", id);
|
|
|
|
|
|
|
|
|
|
// Debug logs to track trash components
|
|
|
|
|
tracing::debug!("Trash service type: {}", std::any::type_name_of_val(&*trash_service));
|
|
|
|
|
let default_user_id = "00000000-0000-0000-0000-000000000000".to_string();
|
|
|
|
|
tracing::info!("Using default user ID: {}", default_user_id);
|
|
|
|
|
|
|
|
|
|
// Try to move to trash first - add more detailed logging
|
|
|
|
|
tracing::info!("About to call trash_service.move_to_trash with id={}, type=file", id);
|
|
|
|
|
match trash_service.move_to_trash(&id, "file", &default_user_id).await {
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
tracing::info!("File successfully moved to trash: {}", id);
|
|
|
|
|
// Note: Use 204 No Content for consistency with DELETE operations
|
|
|
|
|
return StatusCode::NO_CONTENT.into_response();
|
|
|
|
|
},
|
|
|
|
|
Err(err) => {
|
|
|
|
|
tracing::error!("Could not move file to trash: {:?}", err);
|
|
|
|
|
tracing::error!("Error kind: {:?}, Error details: {}", err.kind, err);
|
|
|
|
|
tracing::warn!("Could not move file to trash, falling back to permanent delete: {}", err);
|
|
|
|
|
// Fall through to regular delete if trash fails
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
tracing::warn!("Trash service not available, using permanent delete");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback to permanent delete if trash is unavailable or failed
|
|
|
|
|
tracing::warn!("Falling back to permanent delete for file: {}", id);
|
|
|
|
|
let file_service = &state.applications.file_service;
|
|
|
|
|
match file_service.delete_file(&id).await {
|
2025-03-23 22:44:18 +01:00
|
|
|
Ok(_) => {
|
2025-03-26 18:33:22 +01:00
|
|
|
tracing::info!("File permanently deleted: {}", id);
|
|
|
|
|
// CRITICAL FIX: Return status code that matches the API expectations (204 No Content)
|
|
|
|
|
// This ensures the client knows the operation was successful
|
2025-03-23 22:44:18 +01:00
|
|
|
StatusCode::NO_CONTENT.into_response()
|
|
|
|
|
},
|
2025-03-17 21:28:08 +01:00
|
|
|
Err(err) => {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::error!("Error deleting file: {}", err);
|
|
|
|
|
|
2025-03-26 18:33:22 +01:00
|
|
|
let status = match err.kind {
|
|
|
|
|
crate::common::errors::ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
2025-03-17 21:28:08 +01:00
|
|
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(status, Json(serde_json::json!({
|
2025-03-23 22:44:18 +01:00
|
|
|
"error": format!("Error deleting file: {}", err)
|
2025-03-17 21:28:08 +01:00
|
|
|
}))).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Moves a file to a different folder
|
|
|
|
|
pub async fn move_file(
|
2025-03-26 18:33:22 +01:00
|
|
|
State(service): State<FileServiceState>,
|
2025-03-17 21:28:08 +01:00
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(payload): Json<MoveFilePayload>,
|
|
|
|
|
) -> impl IntoResponse {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::info!("API request: Moving file with ID: {} to folder: {:?}", id, payload.folder_id);
|
2025-03-17 21:28:08 +01:00
|
|
|
|
2025-03-23 22:44:18 +01:00
|
|
|
// First verify if the file exists
|
2025-03-17 21:28:08 +01:00
|
|
|
match service.get_file(&id).await {
|
|
|
|
|
Ok(file) => {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::info!("File found: {} (ID: {}), proceeding with move operation", file.name, id);
|
2025-03-17 21:28:08 +01:00
|
|
|
|
2025-03-23 22:44:18 +01:00
|
|
|
// For target folders, we trust that the move operation will verify their existence
|
2025-03-17 21:28:08 +01:00
|
|
|
if let Some(folder_id) = &payload.folder_id {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::info!("Will attempt to move to folder: {}", folder_id);
|
2025-03-17 21:28:08 +01:00
|
|
|
}
|
|
|
|
|
|
2025-03-23 22:44:18 +01:00
|
|
|
// Proceed with the move operation
|
2025-03-17 21:28:08 +01:00
|
|
|
match service.move_file(&id, payload.folder_id).await {
|
|
|
|
|
Ok(file) => {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::info!("File moved successfully: {} (ID: {})", file.name, file.id);
|
2025-03-17 21:28:08 +01:00
|
|
|
(StatusCode::OK, Json(file)).into_response()
|
|
|
|
|
},
|
|
|
|
|
Err(err) => {
|
2025-03-26 18:33:22 +01:00
|
|
|
// Simplify error handling
|
|
|
|
|
let status = StatusCode::INTERNAL_SERVER_ERROR;
|
|
|
|
|
tracing::error!("Error moving file: {}", err);
|
2025-03-17 21:28:08 +01:00
|
|
|
|
|
|
|
|
(status, Json(serde_json::json!({
|
2025-03-26 18:33:22 +01:00
|
|
|
"error": format!("Error moving file: {}", err)
|
2025-03-17 21:28:08 +01:00
|
|
|
}))).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(err) => {
|
2025-03-23 22:44:18 +01:00
|
|
|
tracing::error!("Error finding file to move - does not exist: {} (ID: {})", err, id);
|
2025-03-17 21:28:08 +01:00
|
|
|
(StatusCode::NOT_FOUND, Json(serde_json::json!({
|
2025-03-23 22:44:18 +01:00
|
|
|
"error": format!("The file with ID: {} does not exist", id),
|
2025-03-17 21:28:08 +01:00
|
|
|
"code": StatusCode::NOT_FOUND.as_u16()
|
|
|
|
|
}))).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Payload for moving a file
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct MoveFilePayload {
|
|
|
|
|
/// Target folder ID (None means root)
|
|
|
|
|
pub folder_id: Option<String>,
|
|
|
|
|
}
|