Skip to main content

fraiseql_server/files/
handler.rs

1//! File upload handler
2
3use std::{collections::HashMap, sync::Arc, time::Duration};
4
5use bytes::Bytes;
6use serde::{Deserialize, Serialize};
7
8use crate::files::{
9    config::FileConfig,
10    error::{FileError, ProcessingError, ScanError, StorageError},
11    processing::ImageProcessorImpl,
12    traits::{FileValidator, ImageProcessor, MalwareScanner, StorageBackend},
13    validation::DefaultFileValidator,
14};
15
16#[derive(Debug, Serialize, Deserialize)]
17pub struct FileResponse {
18    pub id:                String,
19    pub name:              String,
20    pub filename:          String,
21    pub original_filename: Option<String>,
22    pub content_type:      String,
23    pub size:              u64,
24    pub url:               String,
25    pub variants:          Option<HashMap<String, String>>,
26    pub metadata:          Option<serde_json::Value>,
27    pub created_at:        chrono::DateTime<chrono::Utc>,
28}
29
30#[derive(Debug, Serialize)]
31pub struct SignedUrlResponse {
32    pub url:        String,
33    pub expires_at: chrono::DateTime<chrono::Utc>,
34}
35
36#[derive(Debug)]
37pub enum HandlerError {
38    File(FileError),
39    Storage(StorageError),
40    Processing(ProcessingError),
41    Scan(ScanError),
42}
43
44impl From<FileError> for HandlerError {
45    fn from(e: FileError) -> Self {
46        Self::File(e)
47    }
48}
49
50impl From<StorageError> for HandlerError {
51    fn from(e: StorageError) -> Self {
52        Self::Storage(e)
53    }
54}
55
56impl From<ProcessingError> for HandlerError {
57    fn from(e: ProcessingError) -> Self {
58        Self::Processing(e)
59    }
60}
61
62impl From<ScanError> for HandlerError {
63    fn from(e: ScanError) -> Self {
64        Self::Scan(e)
65    }
66}
67
68pub struct FileHandler {
69    upload_type: String,
70    config:      FileConfig,
71    storage:     Arc<dyn StorageBackend>,
72    validator:   Arc<dyn FileValidator>,
73    processor:   Option<Arc<dyn ImageProcessor>>,
74    scanner:     Option<Arc<dyn MalwareScanner>>,
75}
76
77impl FileHandler {
78    pub fn new(upload_type: &str, config: FileConfig, storage: Arc<dyn StorageBackend>) -> Self {
79        let validator = Arc::new(DefaultFileValidator) as Arc<dyn FileValidator>;
80        let processor = config
81            .processing
82            .as_ref()
83            .map(|p| Arc::new(ImageProcessorImpl::new(p.clone())) as Arc<dyn ImageProcessor>);
84
85        Self {
86            upload_type: upload_type.to_string(),
87            config,
88            storage,
89            validator,
90            processor,
91            scanner: None,
92        }
93    }
94
95    pub fn with_validator(mut self, validator: Arc<dyn FileValidator>) -> Self {
96        self.validator = validator;
97        self
98    }
99
100    pub fn with_scanner(mut self, scanner: Arc<dyn MalwareScanner>) -> Self {
101        self.scanner = Some(scanner);
102        self
103    }
104
105    pub async fn upload(
106        &self,
107        original_filename: &str,
108        content_type: &str,
109        data: Bytes,
110        metadata: Option<serde_json::Value>,
111    ) -> Result<FileResponse, HandlerError> {
112        // Validate file
113        let validated =
114            self.validator.validate(&data, content_type, original_filename, &self.config)?;
115
116        // Scan for malware if configured
117        if self.config.scan_malware {
118            if let Some(scanner) = &self.scanner {
119                let scan_result = scanner.scan(&data).await?;
120                if !scan_result.clean {
121                    return Err(FileError::MalwareDetected {
122                        threat_name: scan_result
123                            .threat_name
124                            .unwrap_or_else(|| "Unknown".to_string()),
125                    }
126                    .into());
127                }
128            }
129        }
130
131        // Generate unique filename
132        let file_id = uuid::Uuid::new_v4();
133        let extension = validated.sanitized_filename.rsplit('.').next().unwrap_or("bin");
134        let filename = format!("{}.{}", file_id, extension);
135
136        // Storage key
137        let storage_key = format!("{}/{}", self.upload_type, filename);
138
139        // Process image if applicable
140        let (variants, processed_data) = if self.is_image(content_type) {
141            if let Some(processor) = &self.processor {
142                let processing_config = self.config.processing.as_ref().unwrap();
143                let processed = processor.process(&data, processing_config).await?;
144
145                let mut variant_urls = HashMap::new();
146
147                // Upload variants
148                for (variant_name, variant_data) in &processed.variants {
149                    if variant_name == "original" {
150                        continue; // Skip original, we'll upload it separately
151                    }
152
153                    let variant_key = format!(
154                        "{}/{}_{}.{}",
155                        self.upload_type,
156                        file_id,
157                        variant_name,
158                        self.get_output_extension()
159                    );
160
161                    let result = self
162                        .storage
163                        .upload(
164                            &variant_key,
165                            variant_data.clone(),
166                            &self.get_output_content_type(),
167                            None,
168                        )
169                        .await?;
170
171                    variant_urls.insert(variant_name.clone(), result.url);
172                }
173
174                (
175                    Some(variant_urls),
176                    processed.variants.get("original").cloned().unwrap_or(data.clone()),
177                )
178            } else {
179                (None, data.clone())
180            }
181        } else {
182            (None, data.clone())
183        };
184
185        // Upload main file
186        let upload_result = self
187            .storage
188            .upload(&storage_key, processed_data.clone(), content_type, None)
189            .await?;
190
191        // Create file record
192        let file_record = FileResponse {
193            id: file_id.to_string(),
194            name: self.upload_type.clone(),
195            filename,
196            original_filename: Some(validated.sanitized_filename),
197            content_type: content_type.to_string(),
198            size: processed_data.len() as u64,
199            url: upload_result.url,
200            variants,
201            metadata,
202            created_at: chrono::Utc::now(),
203        };
204
205        Ok(file_record)
206    }
207
208    pub async fn signed_url(
209        &self,
210        storage_key: &str,
211        expiry: Duration,
212    ) -> Result<SignedUrlResponse, HandlerError> {
213        let url = self.storage.signed_url(storage_key, expiry).await?;
214
215        Ok(SignedUrlResponse {
216            url,
217            expires_at: chrono::Utc::now()
218                + chrono::Duration::from_std(expiry).map_err(|e| StorageError::Provider {
219                    message: e.to_string(),
220                })?,
221        })
222    }
223
224    pub async fn delete(&self, storage_key: &str) -> Result<(), HandlerError> {
225        self.storage.delete(storage_key).await?;
226        Ok(())
227    }
228
229    pub async fn exists(&self, storage_key: &str) -> Result<bool, HandlerError> {
230        let exists = self.storage.exists(storage_key).await?;
231        Ok(exists)
232    }
233
234    fn is_image(&self, content_type: &str) -> bool {
235        content_type.starts_with("image/")
236    }
237
238    fn get_output_content_type(&self) -> String {
239        match self.config.processing.as_ref().and_then(|p| p.output_format.as_deref()) {
240            Some("webp") => "image/webp".to_string(),
241            Some("png") => "image/png".to_string(),
242            _ => "image/jpeg".to_string(),
243        }
244    }
245
246    fn get_output_extension(&self) -> &str {
247        match self.config.processing.as_ref().and_then(|p| p.output_format.as_deref()) {
248            Some("webp") => "webp",
249            Some("png") => "png",
250            _ => "jpg",
251        }
252    }
253}