botserver/src/file/mod.rs

148 lines
5 KiB
Rust
Raw Normal View History

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
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
let bucket_name = match &state.config {
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);
// Perform the upload
let s3_client = get_s3_client(&state).await;
match upload_to_s3(&s3_client, &bucket_name, &s3_key, &temp_file_path).await {
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
async fn get_s3_client(state: &AppState) -> Client {
if let Some(cfg) = &state.config.as_ref().and_then(|c| Some(&c.minio)) {
// 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 pathstyle addressing.
let s3_config = aws_sdk_s3::config::Builder::new()
.region(aws_sdk_s3::config::Region::new("us-east-1"))
.endpoint_url(endpoint)
.credentials_provider(credentials)
.force_path_style(true)
.build();
Client::from_conf(s3_config)
} else {
panic!("MinIO configuration is missing in application state");
}
}
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> {
// Convert the file at `file_path` into a `ByteStream`. Any I/O error is
// turned into a constructionfailure `SdkError` so that the functions
// `Result` type (`Result<(), S3Error>`) stays consistent.
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,
aws_sdk_s3::operation::put_object::PutObjectOutput,
>::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()
.await?;
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
}