feat: Refactor and implement MinIO file upload and listing services
This commit is contained in:
parent
184a322c94
commit
59072d0079
6 changed files with 203 additions and 236 deletions
49
.vscode/launch.json
vendored
49
.vscode/launch.json
vendored
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
193
src/lib.rs
193
src/lib.rs
|
@ -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))
|
||||
}
|
150
src/main.rs
150
src/main.rs
|
@ -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
|
||||
|
|
1
src/services.rs
Normal file
1
src/services.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod config;
|
46
src/services/config.rs
Normal file
46
src/services/config.rs
Normal 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue