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",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug GB API Server",
|
"name": "Debug GB Server",
|
||||||
"cargo": {
|
"cargo": {
|
||||||
"args": [
|
"args": [
|
||||||
"build",
|
"build",
|
||||||
"--bin=gb-server"
|
"--bin=gbserver"
|
||||||
],
|
],
|
||||||
"filter": {
|
"filter": {
|
||||||
"name": "gb-server",
|
"name": "gbserver",
|
||||||
"kind": "bin"
|
"kind": "bin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}",
|
"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",
|
"type": "lldb",
|
||||||
|
@ -55,7 +27,7 @@
|
||||||
"test",
|
"test",
|
||||||
"--no-run",
|
"--no-run",
|
||||||
"--lib",
|
"--lib",
|
||||||
"--package=gb-server"
|
"--package=gbserver"
|
||||||
],
|
],
|
||||||
"filter": {
|
"filter": {
|
||||||
"name": "integration",
|
"name": "integration",
|
||||||
|
@ -63,17 +35,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}", "env": {
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
"RUST_LOG": "info"
|
"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 actix_web::{middleware, web, App, HttpServer};
|
||||||
use gbserver::{init_minio, upload_file, AppConfig, AppState};
|
|
||||||
use tracing_subscriber::fmt::format::FmtSpan;
|
use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
use dotenv::dotenv;
|
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]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
|
||||||
// Initialize tracing
|
// 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