pub mod handler;
pub mod storage;
pub use handler::upload_routes;
pub use storage::{LocalStorage, StorageBackend, UploadStorage};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct UploadConfig {
pub max_file_size: usize,
pub allowed_types: Vec<String>,
pub upload_dir: String,
pub max_files: usize,
}
impl Default for UploadConfig {
fn default() -> Self {
Self {
max_file_size: 10 * 1024 * 1024, allowed_types: Vec::new(),
upload_dir: "./uploads".to_string(),
max_files: 10,
}
}
}
impl UploadConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_size(mut self, bytes: usize) -> Self {
self.max_file_size = bytes;
self
}
pub fn with_allowed_types(mut self, types: Vec<impl Into<String>>) -> Self {
self.allowed_types = types.into_iter().map(|t| t.into()).collect();
self
}
pub fn with_upload_dir(mut self, dir: impl Into<String>) -> Self {
self.upload_dir = dir.into();
self
}
pub fn with_max_files(mut self, max: usize) -> Self {
self.max_files = max;
self
}
pub fn is_allowed(&self, content_type: &str) -> bool {
if self.allowed_types.is_empty() {
return true;
}
self.allowed_types.iter().any(|t| t == content_type || t == "*/*")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UploadedFile {
pub id: Uuid,
pub original_name: String,
pub stored_name: String,
pub content_type: String,
pub size: usize,
pub url: String,
pub uploaded_at: DateTime<Utc>,
}
impl UploadedFile {
pub fn new(
original_name: String,
stored_name: String,
content_type: String,
size: usize,
url: String,
) -> Self {
Self {
id: Uuid::new_v4(),
original_name,
stored_name,
content_type,
size,
url,
uploaded_at: Utc::now(),
}
}
}
pub struct FileUploadService {
pub config: UploadConfig,
pub storage: StorageBackend,
}
impl FileUploadService {
pub fn new(config: UploadConfig) -> Self {
let storage = StorageBackend::Local(LocalStorage::new(&config.upload_dir));
Self { config, storage }
}
pub fn with_storage(config: UploadConfig, storage: StorageBackend) -> Self {
Self { config, storage }
}
pub async fn save(
&self,
filename: &str,
content_type: &str,
data: &[u8],
) -> Result<UploadedFile, crate::error::ApiError> {
if data.len() > self.config.max_file_size {
return Err(crate::error::ApiError::BadRequest(format!(
"File size {} exceeds maximum allowed size {}",
data.len(),
self.config.max_file_size
)));
}
if !self.config.is_allowed(content_type) {
return Err(crate::error::ApiError::BadRequest(format!(
"Content type '{}' is not allowed",
content_type
)));
}
self.storage.save(filename, content_type, data).await
}
pub async fn delete(&self, stored_name: &str) -> Result<(), crate::error::ApiError> {
self.storage.delete(stored_name).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_upload_config_defaults() {
let config = UploadConfig::default();
assert_eq!(config.max_file_size, 10 * 1024 * 1024);
assert!(config.allowed_types.is_empty());
assert_eq!(config.max_files, 10);
}
#[test]
fn test_upload_config_builder() {
let config = UploadConfig::new()
.with_max_size(5 * 1024 * 1024)
.with_allowed_types(vec!["image/jpeg", "image/png"])
.with_upload_dir("/tmp/uploads");
assert_eq!(config.max_file_size, 5 * 1024 * 1024);
assert_eq!(config.allowed_types.len(), 2);
assert_eq!(config.upload_dir, "/tmp/uploads");
}
#[test]
fn test_is_allowed() {
let config = UploadConfig::new()
.with_allowed_types(vec!["image/jpeg", "image/png"]);
assert!(config.is_allowed("image/jpeg"));
assert!(config.is_allowed("image/png"));
assert!(!config.is_allowed("text/plain"));
}
#[test]
fn test_is_allowed_empty_means_all() {
let config = UploadConfig::new();
assert!(config.is_allowed("image/jpeg"));
assert!(config.is_allowed("application/pdf"));
assert!(config.is_allowed("text/plain"));
}
}