feat: Refactor and implement MinIO file upload and listing services

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-06-20 21:34:48 -03:00
parent 184a322c94
commit 59072d0079
6 changed files with 203 additions and 236 deletions

49
.vscode/launch.json vendored
View file

@ -4,47 +4,19 @@
{
"type": "lldb",
"request": "launch",
"name": "Debug GB API Server",
"name": "Debug GB Server",
"cargo": {
"args": [
"build",
"--bin=gb-server"
"--bin=gbserver"
],
"filter": {
"name": "gb-server",
"name": "gbserver",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}",
"env": {
"RUST_LOG": "info",
"DATABASE_URL": "postgres://gbuser:gbpassword@localhost:5432/generalbots",
"REDIS_URL": "redis://localhost:6379"
}
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'gb-server'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=gb-server"
],
"filter": {
"name": "gb-server",
"kind": "bin"
}
},
"args": [
"--test-threads=1"
],
"cwd": "${workspaceFolder}", "env": {
"RUST_LOG": "info"
}
},
{
"type": "lldb",
@ -55,7 +27,7 @@
"test",
"--no-run",
"--lib",
"--package=gb-server"
"--package=gbserver"
],
"filter": {
"name": "integration",
@ -63,17 +35,10 @@
}
},
"args": [],
"cwd": "${workspaceFolder}", "env": {
"cwd": "${workspaceFolder}",
"env": {
"RUST_LOG": "info"
}
}
},
],
"compounds": [
{
"name": "API Server + Debug",
"configurations": [
"Debug GB API Server"
]
}
]
}

View file

@ -1,193 +0,0 @@
use actix_multipart::Multipart;
use actix_web::{post, web, HttpResponse};
use minio::s3::builders::ObjectContent;
use minio::s3::Client;
use std::io::Write;
use tempfile::NamedTempFile;
use minio::s3::types::ToStream;
use tokio_stream::StreamExt;
use minio::s3::client::{Client as MinioClient, ClientBuilder as MinioClientBuilder};
use minio::s3::creds::StaticProvider;
use minio::s3::http::BaseUrl;
use std::str::FromStr;
use std::env;
/// Define AppConfig and its nested MinioConfig struct if not already defined elsewhere.
#[derive(Clone)]
pub struct AppConfig {
pub minio: MinioConfig,
pub server: ServerConfig,
}
/// Define ServerConfig as a tuple struct for (host, port)
#[derive(Clone)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
}
impl AppConfig {
pub fn from_env() -> Self {
let minio = MinioConfig {
endpoint: env::var("MINIO_ENDPOINT").expect("MINIO_ENDPOINT not set"),
access_key: env::var("MINIO_ACCESS_KEY").expect("MINIO_ACCESS_KEY not set"),
secret_key: env::var("MINIO_SECRET_KEY").expect("MINIO_SECRET_KEY not set"),
use_ssl: env::var("MINIO_USE_SSL")
.unwrap_or_else(|_| "false".to_string())
.parse()
.unwrap_or(false),
bucket: env::var("MINIO_BUCKET").expect("MINIO_BUCKET not set"),
};
AppConfig {
minio,
server: ServerConfig {
host: env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
port: env::var("SERVER_PORT").ok()
.and_then(|p| p.parse().ok())
.unwrap_or(8080),
},
}
}
}
#[derive(Clone)]
pub struct MinioConfig {
pub endpoint: String,
pub access_key: String,
pub secret_key: String,
pub use_ssl: bool,
pub bucket: String,
}
// App state shared across all handlers
pub struct AppState {
pub minio_client: Option<MinioClient>,
pub config: Option<AppConfig>,
}
pub async fn init_minio(config: &AppConfig) -> Result<MinioClient, minio::s3::error::Error> {
let scheme = if config.minio.use_ssl { "https" } else { "http" };
let base_url = format!("{}://{}", scheme, config.minio.endpoint);
let base_url = BaseUrl::from_str(&base_url)?;
let credentials = StaticProvider::new(
&config.minio.access_key,
&config.minio.secret_key,
None,
);
let minio_client = MinioClientBuilder::new(base_url)
.provider(Some(credentials))
.build()?;
Ok(minio_client)
}
#[post("/files/upload/{folder_path}")]
pub async fn upload_file(
folder_path: web::Path<String>,
mut payload: Multipart,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
let folder_path = folder_path.into_inner();
// Create a temporary file to store the uploaded file.
let mut temp_file = NamedTempFile::new().map_err(|e| {
actix_web::error::ErrorInternalServerError(format!("Failed to create temp file: {}", e))
})?;
let mut file_name = None;
// Iterate over the multipart stream.
while let Some(mut field) = payload.try_next().await? {
let content_disposition = field.content_disposition();
file_name = content_disposition
.get_filename()
.map(|name| name.to_string());
// Write the file content to the temporary file.
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
))
})?;
}
}
// Get the file name or use a default name
let file_name = file_name.unwrap_or_else(|| "unnamed_file".to_string());
// Construct the object name using the folder path and file name
let object_name = format!("{}/{}", folder_path, file_name);
// Upload the file to the MinIO bucket
let client: Client = state.minio_client.clone().unwrap();
let bucket_name = "file-upload-rust-bucket";
let content = ObjectContent::from(temp_file.path());
client
.put_object_content(bucket_name, &object_name, content)
.send()
.await
.map_err(|e| {
actix_web::error::ErrorInternalServerError(format!(
"Failed to upload file to MinIO: {}",
e
))
})?;
// Clean up the temporary file
temp_file.close().map_err(|e| {
actix_web::error::ErrorInternalServerError(format!("Failed to close temp file: {}", e))
})?;
Ok(HttpResponse::Ok().body(format!(
"Uploaded file '{}' to folder '{}'",
file_name, folder_path
)))
}
#[post("/files/list/{folder_path}")]
pub async fn list_file(
folder_path: web::Path<String>,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
let folder_path = folder_path.into_inner();
let client: Client = state.minio_client.clone().unwrap();
let bucket_name = "file-upload-rust-bucket";
// Create the stream using the to_stream() method
let mut objects_stream = client
.list_objects(bucket_name)
.prefix(Some(folder_path))
.to_stream()
.await;
let mut file_list = Vec::new();
// Use StreamExt::next() to iterate through the stream
while let Some(items) = objects_stream.next().await {
match items {
Ok(result) => {
for item in result.contents {
file_list.push(item.name);
}
},
Err(e) => {
return Err(actix_web::error::ErrorInternalServerError(
format!("Failed to list files in MinIO: {}", e)
));
}
}
}
Ok(HttpResponse::Ok().json(file_list))
}

View file

@ -1,12 +1,160 @@
use actix_web::{middleware, web, App, HttpServer};
use gbserver::{init_minio, upload_file, AppConfig, AppState};
use tracing_subscriber::fmt::format::FmtSpan;
use dotenv::dotenv;
use actix_multipart::Multipart;
use actix_web::{post, HttpResponse};
use minio::s3::builders::ObjectContent;
use minio::s3::Client;
use std::io::Write;
use tempfile::NamedTempFile;
use minio::s3::types::ToStream;
use tokio_stream::StreamExt;
use minio::s3::client::{Client as MinioClient, ClientBuilder as MinioClientBuilder};
use minio::s3::creds::StaticProvider;
use minio::s3::http::BaseUrl;
use std::str::FromStr;
use services::config::*;
mod services;
// App state shared across all handlers
pub struct AppState {
pub minio_client: Option<MinioClient>,
pub config: Option<AppConfig>,
}
pub async fn init_minio(config: &AppConfig) -> Result<MinioClient, minio::s3::error::Error> {
let scheme = if config.minio.use_ssl { "https" } else { "http" };
let base_url = format!("{}://{}", scheme, config.minio.endpoint);
let base_url = BaseUrl::from_str(&base_url)?;
let credentials = StaticProvider::new(
&config.minio.access_key,
&config.minio.secret_key,
None,
);
let minio_client = MinioClientBuilder::new(base_url)
.provider(Some(credentials))
.build()?;
Ok(minio_client)
}
#[post("/files/upload/{folder_path}")]
pub async fn upload_file(
folder_path: web::Path<String>,
mut payload: Multipart,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
let folder_path = folder_path.into_inner();
// Create a temporary file to store the uploaded file.
let mut temp_file = NamedTempFile::new().map_err(|e| {
actix_web::error::ErrorInternalServerError(format!("Failed to create temp file: {}", e))
})?;
let mut file_name = None;
// Iterate over the multipart stream.
while let Some(mut field) = payload.try_next().await? {
let content_disposition = field.content_disposition();
file_name = content_disposition
.get_filename()
.map(|name| name.to_string());
// Write the file content to the temporary file.
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
))
})?;
}
}
// Get the file name or use a default name
let file_name = file_name.unwrap_or_else(|| "unnamed_file".to_string());
// Construct the object name using the folder path and file name
let object_name = format!("{}/{}", folder_path, file_name);
// Upload the file to the MinIO bucket
let client: Client = state.minio_client.clone().unwrap();
let bucket_name = "file-upload-rust-bucket";
let content = ObjectContent::from(temp_file.path());
client
.put_object_content(bucket_name, &object_name, content)
.send()
.await
.map_err(|e| {
actix_web::error::ErrorInternalServerError(format!(
"Failed to upload file to MinIO: {}",
e
))
})?;
// Clean up the temporary file
temp_file.close().map_err(|e| {
actix_web::error::ErrorInternalServerError(format!("Failed to close temp file: {}", e))
})?;
Ok(HttpResponse::Ok().body(format!(
"Uploaded file '{}' to folder '{}'",
file_name, folder_path
)))
}
#[post("/files/list/{folder_path}")]
pub async fn list_file(
folder_path: web::Path<String>,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
let folder_path = folder_path.into_inner();
let client: Client = state.minio_client.clone().unwrap();
let bucket_name = "file-upload-rust-bucket";
// Create the stream using the to_stream() method
let mut objects_stream = client
.list_objects(bucket_name)
.prefix(Some(folder_path))
.to_stream()
.await;
let mut file_list = Vec::new();
// Use StreamExt::next() to iterate through the stream
while let Some(items) = objects_stream.next().await {
match items {
Ok(result) => {
for item in result.contents {
file_list.push(item.name);
}
},
Err(e) => {
return Err(actix_web::error::ErrorInternalServerError(
format!("Failed to list files in MinIO: {}", e)
));
}
}
}
Ok(HttpResponse::Ok().json(file_list))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
// Initialize tracing

View file

1
src/services.rs Normal file
View file

@ -0,0 +1 @@
pub mod config;

46
src/services/config.rs Normal file
View file

@ -0,0 +1,46 @@
use std::env;
#[derive(Clone)]
pub struct AppConfig {
pub minio: MinioConfig,
pub server: ServerConfig,
}
#[derive(Clone)]
pub struct MinioConfig {
pub endpoint: String,
pub access_key: String,
pub secret_key: String,
pub use_ssl: bool,
pub bucket: String,
}
#[derive(Clone)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
}
impl AppConfig {
pub fn from_env() -> Self {
let minio = MinioConfig {
endpoint: env::var("MINIO_ENDPOINT").expect("MINIO_ENDPOINT not set"),
access_key: env::var("MINIO_ACCESS_KEY").expect("MINIO_ACCESS_KEY not set"),
secret_key: env::var("MINIO_SECRET_KEY").expect("MINIO_SECRET_KEY not set"),
use_ssl: env::var("MINIO_USE_SSL")
.unwrap_or_else(|_| "false".to_string())
.parse()
.unwrap_or(false),
bucket: env::var("MINIO_BUCKET").expect("MINIO_BUCKET not set"),
};
AppConfig {
minio,
server: ServerConfig {
host: env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
port: env::var("SERVER_PORT").ok()
.and_then(|p| p.parse().ok())
.unwrap_or(8080),
},
}
}
}