1use std::time::Duration;
37use std::path::PathBuf;
38use std::collections::HashMap;
39use bytes::Bytes;
40use serde::{Serialize, Deserialize};
41use async_trait::async_trait;
42use thiserror::Error;
43use chrono::{DateTime, Utc};
44use uuid::Uuid;
45
46pub mod backends;
47pub mod config;
48pub mod upload;
49pub mod validation;
50pub mod permissions;
51pub mod cleanup;
52
53#[cfg(feature = "image-processing")]
54pub mod image_processing;
55
56pub use backends::*;
57pub use config::*;
58pub use upload::*;
59pub use validation::*;
60pub use permissions::*;
61pub use cleanup::*;
62
63#[cfg(feature = "image-processing")]
64pub use image_processing::*;
65
66#[derive(Error, Debug)]
68pub enum StorageError {
69 #[error("IO error: {0}")]
70 Io(#[from] std::io::Error),
71
72 #[error("Backend error: {0}")]
73 Backend(String),
74
75 #[error("File not found: {0}")]
76 FileNotFound(String),
77
78 #[error("Permission denied: {0}")]
79 PermissionDenied(String),
80
81 #[error("Validation error: {0}")]
82 Validation(String),
83
84 #[error("Configuration error: {0}")]
85 Configuration(String),
86
87 #[error("Network error: {0}")]
88 Network(String),
89
90 #[error("Timeout error")]
91 Timeout,
92
93 #[error("File too large: {0} bytes, max allowed: {1} bytes")]
94 FileTooLarge(u64, u64),
95
96 #[error("Unsupported file type: {0}")]
97 UnsupportedFileType(String),
98
99 #[error("Image processing error: {0}")]
100 ImageProcessing(String),
101
102 #[error("CDN error: {0}")]
103 Cdn(String),
104}
105
106pub type StorageResult<T> = Result<T, StorageError>;
108
109pub type FilePath = String;
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct FileMetadata {
115 pub path: FilePath,
117
118 pub size: u64,
120
121 pub content_type: String,
123
124 pub created_at: DateTime<Utc>,
126
127 pub modified_at: DateTime<Utc>,
129
130 pub etag: Option<String>,
132
133 pub metadata: HashMap<String, String>,
135
136 #[cfg(feature = "access-control")]
138 pub permissions: Option<FilePermissions>,
139}
140
141impl FileMetadata {
142 pub fn new(path: FilePath, size: u64, content_type: String) -> Self {
144 let now = Utc::now();
145 Self {
146 path,
147 size,
148 content_type,
149 created_at: now,
150 modified_at: now,
151 etag: None,
152 metadata: HashMap::new(),
153 #[cfg(feature = "access-control")]
154 permissions: None,
155 }
156 }
157
158 pub fn with_metadata(mut self, key: String, value: String) -> Self {
160 self.metadata.insert(key, value);
161 self
162 }
163
164 pub fn with_etag(mut self, etag: String) -> Self {
166 self.etag = Some(etag);
167 self
168 }
169
170 #[cfg(feature = "access-control")]
172 pub fn with_permissions(mut self, permissions: FilePermissions) -> Self {
173 self.permissions = Some(permissions);
174 self
175 }
176}
177
178#[derive(Debug, Clone)]
180pub struct UploadOptions {
181 pub content_type: Option<String>,
183
184 pub metadata: HashMap<String, String>,
186
187 #[cfg(feature = "access-control")]
189 pub permissions: Option<FilePermissions>,
190
191 pub cache_control: Option<String>,
193
194 pub content_disposition: Option<String>,
196
197 pub overwrite: bool,
199}
200
201impl Default for UploadOptions {
202 fn default() -> Self {
203 Self {
204 content_type: None,
205 metadata: HashMap::new(),
206 #[cfg(feature = "access-control")]
207 permissions: None,
208 cache_control: None,
209 content_disposition: None,
210 overwrite: false,
211 }
212 }
213}
214
215impl UploadOptions {
216 pub fn new() -> Self {
218 Self::default()
219 }
220
221 pub fn content_type(mut self, content_type: String) -> Self {
223 self.content_type = Some(content_type);
224 self
225 }
226
227 pub fn metadata(mut self, key: String, value: String) -> Self {
229 self.metadata.insert(key, value);
230 self
231 }
232
233 #[cfg(feature = "access-control")]
235 pub fn permissions(mut self, permissions: FilePermissions) -> Self {
236 self.permissions = Some(permissions);
237 self
238 }
239
240 pub fn cache_control(mut self, cache_control: String) -> Self {
242 self.cache_control = Some(cache_control);
243 self
244 }
245
246 pub fn content_disposition(mut self, content_disposition: String) -> Self {
248 self.content_disposition = Some(content_disposition);
249 self
250 }
251
252 pub fn overwrite(mut self) -> Self {
254 self.overwrite = true;
255 self
256 }
257}
258
259#[derive(Debug, Clone, Default)]
261pub struct StorageStats {
262 pub total_files: u64,
263 pub total_size: u64,
264 pub available_space: Option<u64>,
265 pub used_space: Option<u64>,
266}
267
268#[async_trait]
270pub trait StorageBackend: Send + Sync {
271 async fn put(&self, path: &str, data: &[u8], options: Option<UploadOptions>) -> StorageResult<FileMetadata>;
273
274 async fn put_stream<S>(&self, path: &str, stream: S, options: Option<UploadOptions>) -> StorageResult<FileMetadata>
276 where
277 S: futures::Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin;
278
279 async fn get(&self, path: &str) -> StorageResult<Option<Bytes>>;
281
282 async fn get_stream(&self, path: &str) -> StorageResult<Option<Box<dyn futures::Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin>>>;
284
285 async fn exists(&self, path: &str) -> StorageResult<bool>;
287
288 async fn metadata(&self, path: &str) -> StorageResult<Option<FileMetadata>>;
290
291 async fn delete(&self, path: &str) -> StorageResult<bool>;
293
294 async fn list(&self, prefix: Option<&str>, limit: Option<u32>) -> StorageResult<Vec<FileMetadata>>;
296
297 async fn copy(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata>;
299
300 async fn move_file(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata>;
302
303 async fn signed_url(&self, path: &str, expires_in: Duration) -> StorageResult<String>;
305
306 async fn public_url(&self, path: &str) -> StorageResult<String>;
308
309 async fn stats(&self) -> StorageResult<StorageStats> {
311 Ok(StorageStats::default())
312 }
313
314 async fn delete_many(&self, paths: &[&str]) -> StorageResult<Vec<String>> {
316 let mut deleted = Vec::new();
317 for path in paths {
318 if self.delete(path).await? {
319 deleted.push(path.to_string());
320 }
321 }
322 Ok(deleted)
323 }
324}
325
326pub struct Storage<B: StorageBackend> {
328 backend: B,
329 config: StorageConfig,
330}
331
332impl<B: StorageBackend> Storage<B> {
333 pub fn new(backend: B) -> Self {
335 Self {
336 backend,
337 config: StorageConfig::default(),
338 }
339 }
340
341 pub fn with_config(backend: B, config: StorageConfig) -> Self {
343 Self { backend, config }
344 }
345
346 pub async fn put(&self, path: &str, data: &[u8], options: Option<UploadOptions>) -> StorageResult<FileMetadata> {
348 if let Err(e) = validate_file_size(data.len() as u64, self.config.max_file_size) {
350 return Err(e);
351 }
352
353 let content_type = options.as_ref()
354 .and_then(|o| o.content_type.clone())
355 .unwrap_or_else(|| detect_content_type(path, data));
356
357 if let Err(e) = validate_file_type(&content_type, &self.config.allowed_types) {
358 return Err(e);
359 }
360
361 self.backend.put(path, data, options).await
363 }
364
365 pub async fn put_stream<S>(&self, path: &str, stream: S, options: Option<UploadOptions>) -> StorageResult<FileMetadata>
367 where
368 S: futures::Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin,
369 {
370 self.backend.put_stream(path, stream, options).await
371 }
372
373 pub async fn get(&self, path: &str) -> StorageResult<Option<Bytes>> {
375 #[cfg(feature = "access-control")]
376 if let Some(permissions) = self.get_file_permissions(path).await? {
377 }
380
381 self.backend.get(path).await
382 }
383
384 pub async fn get_stream(&self, path: &str) -> StorageResult<Option<Box<dyn futures::Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin>>> {
386 self.backend.get_stream(path).await
387 }
388
389 pub async fn exists(&self, path: &str) -> StorageResult<bool> {
391 self.backend.exists(path).await
392 }
393
394 pub async fn metadata(&self, path: &str) -> StorageResult<Option<FileMetadata>> {
396 self.backend.metadata(path).await
397 }
398
399 pub async fn delete(&self, path: &str) -> StorageResult<bool> {
401 self.backend.delete(path).await
402 }
403
404 pub async fn list(&self, prefix: Option<&str>, limit: Option<u32>) -> StorageResult<Vec<FileMetadata>> {
406 self.backend.list(prefix, limit).await
407 }
408
409 pub async fn copy(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata> {
411 self.backend.copy(from, to, options).await
412 }
413
414 pub async fn move_file(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata> {
416 self.backend.move_file(from, to, options).await
417 }
418
419 pub async fn signed_url(&self, path: &str, expires_in: Duration) -> StorageResult<String> {
421 self.backend.signed_url(path, expires_in).await
422 }
423
424 pub async fn public_url(&self, path: &str) -> StorageResult<String> {
426 self.backend.public_url(path).await
427 }
428
429 pub async fn stats(&self) -> StorageResult<StorageStats> {
431 self.backend.stats().await
432 }
433
434 pub async fn delete_many(&self, paths: &[&str]) -> StorageResult<Vec<String>> {
436 self.backend.delete_many(paths).await
437 }
438
439 #[cfg(feature = "access-control")]
440 async fn get_file_permissions(&self, _path: &str) -> StorageResult<Option<FilePermissions>> {
441 Ok(None)
443 }
444}
445
446fn detect_content_type(path: &str, data: &[u8]) -> String {
448 if let Some(ext) = std::path::Path::new(path).extension().and_then(|e| e.to_str()) {
450 match ext.to_lowercase().as_str() {
451 "jpg" | "jpeg" => return "image/jpeg".to_string(),
452 "png" => return "image/png".to_string(),
453 "gif" => return "image/gif".to_string(),
454 "webp" => return "image/webp".to_string(),
455 "pdf" => return "application/pdf".to_string(),
456 "txt" => return "text/plain".to_string(),
457 "json" => return "application/json".to_string(),
458 "xml" => return "application/xml".to_string(),
459 "html" => return "text/html".to_string(),
460 "css" => return "text/css".to_string(),
461 "js" => return "application/javascript".to_string(),
462 _ => {}
463 }
464 }
465
466 if data.len() >= 4 {
468 match &data[..4] {
469 [0xFF, 0xD8, 0xFF, _] => return "image/jpeg".to_string(),
470 [0x89, 0x50, 0x4E, 0x47] => return "image/png".to_string(),
471 [0x47, 0x49, 0x46, 0x38] => return "image/gif".to_string(),
472 [0x52, 0x49, 0x46, 0x46] if data.len() >= 12 && &data[8..12] == b"WEBP" => return "image/webp".to_string(),
473 [0x25, 0x50, 0x44, 0x46] => return "application/pdf".to_string(),
474 _ => {}
475 }
476 }
477
478 "application/octet-stream".to_string()
479}
480
481fn validate_file_size(size: u64, max_size: Option<u64>) -> StorageResult<()> {
483 if let Some(max) = max_size {
484 if size > max {
485 return Err(StorageError::FileTooLarge(size, max));
486 }
487 }
488 Ok(())
489}
490
491fn validate_file_type(content_type: &str, allowed_types: &Option<Vec<String>>) -> StorageResult<()> {
493 if let Some(allowed) = allowed_types {
494 if !allowed.iter().any(|t| content_type.starts_with(t)) {
495 return Err(StorageError::UnsupportedFileType(content_type.to_string()));
496 }
497 }
498 Ok(())
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn test_detect_content_type() {
507 assert_eq!(detect_content_type("test.jpg", &[]), "image/jpeg");
509 assert_eq!(detect_content_type("test.png", &[]), "image/png");
510 assert_eq!(detect_content_type("test.pdf", &[]), "application/pdf");
511
512 let jpeg_data = [0xFF, 0xD8, 0xFF, 0xE0];
514 assert_eq!(detect_content_type("unknown", &jpeg_data), "image/jpeg");
515
516 let png_data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
517 assert_eq!(detect_content_type("unknown", &png_data), "image/png");
518
519 assert_eq!(detect_content_type("unknown", &[0x00, 0x01, 0x02, 0x03]), "application/octet-stream");
521 }
522
523 #[test]
524 fn test_validate_file_size() {
525 assert!(validate_file_size(1000, Some(2000)).is_ok());
526 assert!(validate_file_size(2000, Some(2000)).is_ok());
527 assert!(validate_file_size(3000, Some(2000)).is_err());
528 assert!(validate_file_size(1000, None).is_ok());
529 }
530
531 #[test]
532 fn test_validate_file_type() {
533 let allowed = Some(vec!["image/".to_string(), "text/plain".to_string()]);
534
535 assert!(validate_file_type("image/jpeg", &allowed).is_ok());
536 assert!(validate_file_type("image/png", &allowed).is_ok());
537 assert!(validate_file_type("text/plain", &allowed).is_ok());
538 assert!(validate_file_type("application/pdf", &allowed).is_err());
539 assert!(validate_file_type("application/pdf", &None).is_ok());
540 }
541}