kegani 0.1.1

A developer-friendly, ergonomic, production-ready Rust web framework
Documentation
//! File upload handler for Kegani
//!
//! Provides utilities for handling file uploads.
//!
//! Note: This is a simplified implementation. For full file upload support,
//! enable the `upload` feature in Cargo.toml.

use actix_web::{web, HttpResponse};
use uuid::Uuid;
use crate::error::AppError;

/// File upload configuration
#[derive(Debug, Clone)]
pub struct UploadConfig {
    pub max_size: usize,          // Maximum file size in bytes
    pub allowed_extensions: Vec<String>,
    pub storage_path: String,
}

impl Default for UploadConfig {
    fn default() -> Self {
        Self {
            max_size: 10 * 1024 * 1024, // 10MB default
            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 {
    /// Set maximum file size
    pub fn max_size(mut self, size: usize) -> Self {
        self.max_size = size;
        self
    }

    /// Set allowed extensions
    pub fn allowed_extensions(mut self, extensions: Vec<&str>) -> Self {
        self.allowed_extensions = extensions.into_iter().map(String::from).collect();
        self
    }

    /// Set storage path
    pub fn storage_path(mut self, path: &str) -> Self {
        self.storage_path = path.to_string();
        self
    }
}

/// Uploaded file info
#[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,
}

/// File uploader
pub struct FileUploader {
    config: UploadConfig,
}

impl FileUploader {
    /// Create a new file uploader
    pub fn new(config: UploadConfig) -> Self {
        Self { config }
    }

    /// Default uploader
    pub fn default() -> Self {
        Self::new(UploadConfig::default())
    }

    /// Validate file extension
    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
        }
    }

    /// Generate unique filename
    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())
        }
    }
}

/// Guess MIME type from filename
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()
}

/// File download handler
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))
}

/// Static file server
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))
}