use actix_web::{web, HttpResponse};
use uuid::Uuid;
use crate::error::AppError;
#[derive(Debug, Clone)]
pub struct UploadConfig {
pub max_size: usize, pub allowed_extensions: Vec<String>,
pub storage_path: String,
}
impl Default for UploadConfig {
fn default() -> Self {
Self {
max_size: 10 * 1024 * 1024, allowed_extensions: vec![
"jpg".to_string(),
"jpeg".to_string(),
"png".to_string(),
"gif".to_string(),
"pdf".to_string(),
"txt".to_string(),
"doc".to_string(),
"docx".to_string(),
],
storage_path: "./uploads".to_string(),
}
}
}
impl UploadConfig {
pub fn max_size(mut self, size: usize) -> Self {
self.max_size = size;
self
}
pub fn allowed_extensions(mut self, extensions: Vec<&str>) -> Self {
self.allowed_extensions = extensions.into_iter().map(String::from).collect();
self
}
pub fn storage_path(mut self, path: &str) -> Self {
self.storage_path = path.to_string();
self
}
}
#[derive(Debug)]
pub struct UploadedFile {
pub id: String,
pub original_name: String,
pub extension: String,
pub size: usize,
pub mime_type: String,
pub path: String,
}
pub struct FileUploader {
config: UploadConfig,
}
impl FileUploader {
pub fn new(config: UploadConfig) -> Self {
Self { config }
}
pub fn default() -> Self {
Self::new(UploadConfig::default())
}
pub fn is_allowed_extension(&self, filename: &str) -> bool {
if let Some(ext) = filename.rsplit('.').next() {
self.config.allowed_extensions
.iter()
.any(|e| e.eq_ignore_ascii_case(ext))
} else {
false
}
}
pub fn generate_filename(&self, original: &str) -> String {
let ext = original.rsplit('.').next().unwrap_or("");
let id = Uuid::new_v4().to_string().replace("-", "");
if ext.is_empty() {
id
} else {
format!("{}.{}", id, ext.to_lowercase())
}
}
}
pub fn guess_mime_type(filename: &str) -> String {
let ext = filename.rsplit('.').next().unwrap_or("").to_lowercase();
match ext.as_str() {
"jpg" | "jpeg" => "image/jpeg",
"png" => "image/png",
"gif" => "image/gif",
"webp" => "image/webp",
"svg" => "image/svg+xml",
"pdf" => "application/pdf",
"txt" => "text/plain",
"html" | "htm" => "text/html",
"css" => "text/css",
"js" => "application/javascript",
"json" => "application/json",
"xml" => "application/xml",
"zip" => "application/zip",
"doc" => "application/msword",
"docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xls" => "application/vnd.ms-excel",
"xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"ppt" => "application/vnd.ms-powerpoint",
_ => "application/octet-stream",
}.to_string()
}
pub async fn download_file(path: web::Path<String>) -> Result<HttpResponse, AppError> {
let filename = path.into_inner();
let file_path = std::path::Path::new(&filename);
if !file_path.exists() {
return Err(AppError::NotFound(format!("File not found: {}", filename).into()));
}
let data = std::fs::read(file_path)?;
let mime = guess_mime_type(&filename);
Ok(HttpResponse::Ok()
.content_type(mime.as_str())
.body(data))
}
pub async fn serve_static(path: web::Path<String>) -> Result<HttpResponse, AppError> {
let file_path = std::path::Path::new("./static").join(path.into_inner());
if !file_path.exists() {
return Err(AppError::NotFound("File not found".into()));
}
let data = std::fs::read(&file_path)?;
let mime = guess_mime_type(&file_path.to_string_lossy());
Ok(HttpResponse::Ok()
.content_type(mime.as_str())
.body(data))
}