2025-10-06 10:30:17 -03:00
|
|
|
|
use actix_multipart::Multipart;
|
2025-10-11 20:02:14 -03:00
|
|
|
|
use actix_web::web;
|
2025-10-06 20:49:38 -03:00
|
|
|
|
use actix_web::{post, HttpResponse};
|
2025-10-11 20:02:14 -03:00
|
|
|
|
use aws_sdk_s3::{Client, Error as S3Error};
|
2025-10-06 10:30:17 -03:00
|
|
|
|
use std::io::Write;
|
2025-10-06 20:49:38 -03:00
|
|
|
|
use tempfile::NamedTempFile;
|
2025-10-11 20:02:14 -03:00
|
|
|
|
use tokio_stream::StreamExt as TokioStreamExt;
|
2025-10-06 20:49:38 -03:00
|
|
|
|
|
2025-10-15 12:45:15 -03:00
|
|
|
|
use crate::config::DriveConfig;
|
2025-10-06 20:49:38 -03:00
|
|
|
|
use crate::shared::state::AppState;
|
|
|
|
|
|
|
2025-10-06 10:30:17 -03:00
|
|
|
|
#[post("/files/upload/{folder_path}")]
|
|
|
|
|
|
pub async fn upload_file(
|
2025-10-06 20:49:38 -03:00
|
|
|
|
folder_path: web::Path<String>,
|
2025-10-06 10:30:17 -03:00
|
|
|
|
mut payload: Multipart,
|
2025-10-06 20:49:38 -03:00
|
|
|
|
state: web::Data<AppState>,
|
|
|
|
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
|
|
|
|
let folder_path = folder_path.into_inner();
|
2025-10-06 10:30:17 -03:00
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Create a temporary file that will hold the uploaded data
|
2025-10-06 20:49:38 -03:00
|
|
|
|
let mut temp_file = NamedTempFile::new().map_err(|e| {
|
|
|
|
|
|
actix_web::error::ErrorInternalServerError(format!("Failed to create temp file: {}", e))
|
|
|
|
|
|
})?;
|
2025-10-06 10:30:17 -03:00
|
|
|
|
|
2025-10-06 20:49:38 -03:00
|
|
|
|
let mut file_name: Option<String> = None;
|
|
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Process multipart form data
|
2025-10-06 20:49:38 -03:00
|
|
|
|
while let Some(mut field) = payload.try_next().await? {
|
|
|
|
|
|
if let Some(disposition) = field.content_disposition() {
|
|
|
|
|
|
if let Some(name) = disposition.get_filename() {
|
|
|
|
|
|
file_name = Some(name.to_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Write each chunk of the field to the temporary file
|
2025-10-06 20:49:38 -03:00
|
|
|
|
while let Some(chunk) = field.try_next().await? {
|
|
|
|
|
|
temp_file.write_all(&chunk).map_err(|e| {
|
|
|
|
|
|
actix_web::error::ErrorInternalServerError(format!(
|
|
|
|
|
|
"Failed to write to temp file: {}",
|
|
|
|
|
|
e
|
|
|
|
|
|
))
|
|
|
|
|
|
})?;
|
2025-10-06 20:06:43 -03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-06 10:30:17 -03:00
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Use a fallback name if the client didn't supply one
|
2025-10-06 20:49:38 -03:00
|
|
|
|
let file_name = file_name.unwrap_or_else(|| "unnamed_file".to_string());
|
2025-10-11 12:29:03 -03:00
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Convert the NamedTempFile into a TempPath so we can get a stable path
|
|
|
|
|
|
let temp_file_path = temp_file.into_temp_path();
|
|
|
|
|
|
|
|
|
|
|
|
// Retrieve the bucket name from configuration, handling the case where it is missing
|
2025-10-15 12:45:15 -03:00
|
|
|
|
let bucket_name = match &state.get_ref().config {
|
2025-10-11 20:02:14 -03:00
|
|
|
|
Some(cfg) => cfg.s3_bucket.clone(),
|
|
|
|
|
|
None => {
|
|
|
|
|
|
// Clean up the temp file before returning the error
|
|
|
|
|
|
let _ = std::fs::remove_file(&temp_file_path);
|
|
|
|
|
|
return Err(actix_web::error::ErrorInternalServerError(
|
|
|
|
|
|
"S3 bucket configuration is missing",
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-10-11 12:29:03 -03:00
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Build the S3 object key (folder + filename)
|
|
|
|
|
|
let s3_key = format!("{}/{}", folder_path, file_name);
|
|
|
|
|
|
|
2025-10-15 12:45:15 -03:00
|
|
|
|
// Retrieve a reference to the S3 client, handling the case where it is missing
|
|
|
|
|
|
let s3_client = state.get_ref().s3_client.as_ref().ok_or_else(|| {
|
|
|
|
|
|
actix_web::error::ErrorInternalServerError("S3 client is not initialized")
|
|
|
|
|
|
})?;
|
|
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Perform the upload
|
2025-10-15 12:45:15 -03:00
|
|
|
|
match upload_to_s3(s3_client, &bucket_name, &s3_key, &temp_file_path).await {
|
2025-10-11 20:02:14 -03:00
|
|
|
|
Ok(_) => {
|
|
|
|
|
|
// Remove the temporary file now that the upload succeeded
|
|
|
|
|
|
let _ = std::fs::remove_file(&temp_file_path);
|
|
|
|
|
|
Ok(HttpResponse::Ok().body(format!(
|
|
|
|
|
|
"Uploaded file '{}' to folder '{}' in S3 bucket '{}'",
|
|
|
|
|
|
file_name, folder_path, bucket_name
|
|
|
|
|
|
)))
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
// Ensure the temporary file is cleaned up even on failure
|
|
|
|
|
|
let _ = std::fs::remove_file(&temp_file_path);
|
|
|
|
|
|
Err(actix_web::error::ErrorInternalServerError(format!(
|
2025-10-11 12:29:03 -03:00
|
|
|
|
"Failed to upload file to S3: {}",
|
2025-10-06 20:49:38 -03:00
|
|
|
|
e
|
2025-10-11 20:02:14 -03:00
|
|
|
|
)))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-06 10:30:17 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Helper function to get S3 client
|
2025-10-15 12:45:15 -03:00
|
|
|
|
pub async fn init_drive(cfg: &DriveConfig) -> Result<Client, Box<dyn std::error::Error>> {
|
|
|
|
|
|
// Build static credentials from the Drive configuration.
|
|
|
|
|
|
let credentials = aws_sdk_s3::config::Credentials::new(
|
|
|
|
|
|
cfg.access_key.clone(),
|
|
|
|
|
|
cfg.secret_key.clone(),
|
|
|
|
|
|
None,
|
|
|
|
|
|
None,
|
|
|
|
|
|
"static",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Construct the endpoint URL, respecting the SSL flag.
|
|
|
|
|
|
let scheme = if cfg.use_ssl { "https" } else { "http" };
|
|
|
|
|
|
let endpoint = format!("{}://{}", scheme, cfg.server);
|
|
|
|
|
|
|
|
|
|
|
|
// MinIO requires path‑style addressing.
|
|
|
|
|
|
let s3_config = aws_sdk_s3::config::Builder::new()
|
|
|
|
|
|
// Set the behavior version to the latest to satisfy the SDK requirement.
|
|
|
|
|
|
.behavior_version(aws_sdk_s3::config::BehaviorVersion::latest())
|
|
|
|
|
|
.region(aws_sdk_s3::config::Region::new("us-east-1"))
|
|
|
|
|
|
.endpoint_url(endpoint)
|
|
|
|
|
|
.credentials_provider(credentials)
|
|
|
|
|
|
.force_path_style(true)
|
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
|
|
Ok(Client::from_conf(s3_config))
|
2025-10-11 20:02:14 -03:00
|
|
|
|
}
|
2025-10-06 20:49:38 -03:00
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Helper function to upload file to S3
|
|
|
|
|
|
async fn upload_to_s3(
|
|
|
|
|
|
client: &Client,
|
|
|
|
|
|
bucket: &str,
|
|
|
|
|
|
key: &str,
|
|
|
|
|
|
file_path: &std::path::Path,
|
|
|
|
|
|
) -> Result<(), S3Error> {
|
2025-10-15 12:45:15 -03:00
|
|
|
|
// Convert the file at `file_path` into a ByteStream, mapping any I/O error
|
|
|
|
|
|
// into the appropriate `SdkError` type expected by the function signature.
|
2025-10-11 20:02:14 -03:00
|
|
|
|
let body = aws_sdk_s3::primitives::ByteStream::from_path(file_path)
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
|
aws_sdk_s3::error::SdkError::<
|
|
|
|
|
|
aws_sdk_s3::operation::put_object::PutObjectError,
|
2025-10-15 12:45:15 -03:00
|
|
|
|
aws_sdk_s3::primitives::ByteStream,
|
2025-10-11 20:02:14 -03:00
|
|
|
|
>::construction_failure(e)
|
|
|
|
|
|
})?;
|
2025-10-11 12:29:03 -03:00
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
// Perform the actual upload to S3.
|
|
|
|
|
|
client
|
|
|
|
|
|
.put_object()
|
|
|
|
|
|
.bucket(bucket)
|
|
|
|
|
|
.key(key)
|
|
|
|
|
|
.body(body)
|
|
|
|
|
|
.send()
|
2025-10-15 12:45:15 -03:00
|
|
|
|
.await
|
|
|
|
|
|
.map(|_| ())?; // Convert the successful output to `()`.
|
2025-10-06 10:30:17 -03:00
|
|
|
|
|
2025-10-11 20:02:14 -03:00
|
|
|
|
Ok(())
|
2025-10-06 10:30:17 -03:00
|
|
|
|
}
|