Zum Hauptinhalt springen

πŸ“ File Upload & Storage Abstractions

Handling user file uploads (profile avatars, document attachments, media assets) requires decoupled storage backends that can switch seamlessly between local filesystem storage in development and S3/MinIO cloud object storage in production.

Ferrox provides ferrox-storage and integrations/ferrox-storage.


1. Storage Abstraction Interface​

use async_trait::async_trait;
use ferrox_errors::AppError;

#[async_trait]
pub trait StorageService: Send + Sync {
/// Uploads a file stream/bytes to storage
async fn upload(&self, path: &str, bytes: &[u8]) -> Result<String, AppError>;

/// Downloads file bytes from storage
async fn download(&self, path: &str) -> Result<Vec<u8>, AppError>;

/// Deletes a file from storage
async fn delete(&self, path: &str) -> Result<(), AppError>;
}

2. Local Disk Storage Implementation​

pub struct LocalStorageService {
base_dir: String,
}

impl LocalStorageService {
pub fn new(base_dir: &str) -> Self {
std::fs::create_dir_all(base_dir).ok();
Self { base_dir: base_dir.to_string() }
}
}

3. Streaming Multipart Upload Handler​

To prevent high RAM consumption during large file uploads, stream bytes directly using Axum's Multipart extractor:

use axum::extract::Multipart;
use ferrox_errors::AppError;

pub async fn upload_avatar(mut multipart: Multipart) -> Result<String, AppError> {
while let Some(field) = multipart.next_field().await.map_err(|e| AppError::ValidationError(e.to_string()))? {
let file_name = field.file_name().unwrap_or("upload.bin").to_string();
let bytes = field.bytes().await.map_err(|e| AppError::InternalServerError(Box::new(e)))?;

println!("Saved {} bytes for file {}", bytes.len(), file_name);
return Ok(format!("/uploads/{}", file_name));
}

Err(AppError::ValidationError("No file uploaded".into()))
}