1use 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 let validated =
114 self.validator.validate(&data, content_type, original_filename, &self.config)?;
115
116 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 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 let storage_key = format!("{}/{}", self.upload_type, filename);
138
139 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 for (variant_name, variant_data) in &processed.variants {
149 if variant_name == "original" {
150 continue; }
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 let upload_result = self
187 .storage
188 .upload(&storage_key, processed_data.clone(), content_type, None)
189 .await?;
190
191 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}