1use crate::config::StorageConfig;
4use crate::drivers::{LocalDriver, MemoryDriver};
5#[cfg(feature = "s3")]
6use crate::env_helpers::env_with_fallback;
7use crate::storage::{FileMetadata, PutOptions, StorageDriver};
8use crate::Error;
9use bytes::Bytes;
10use dashmap::DashMap;
11use std::sync::Arc;
12use std::time::Duration;
13
14#[derive(Debug, Clone)]
16pub struct DiskConfig {
17 pub driver: DiskDriver,
19 pub root: Option<String>,
21 pub url: Option<String>,
23 pub cdn_url: Option<String>,
25 #[cfg(feature = "s3")]
27 pub bucket: Option<String>,
28 #[cfg(feature = "s3")]
30 pub region: Option<String>,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum DiskDriver {
36 Local,
38 Memory,
40 #[cfg(feature = "s3")]
42 S3,
43}
44
45impl Default for DiskConfig {
46 fn default() -> Self {
47 Self {
48 driver: DiskDriver::Local,
49 root: Some("storage".to_string()),
50 url: None,
51 cdn_url: None,
52 #[cfg(feature = "s3")]
53 bucket: None,
54 #[cfg(feature = "s3")]
55 region: None,
56 }
57 }
58}
59
60impl DiskConfig {
61 pub fn local(root: impl Into<String>) -> Self {
63 Self {
64 driver: DiskDriver::Local,
65 root: Some(root.into()),
66 url: None,
67 cdn_url: None,
68 #[cfg(feature = "s3")]
69 bucket: None,
70 #[cfg(feature = "s3")]
71 region: None,
72 }
73 }
74
75 pub fn memory() -> Self {
77 Self {
78 driver: DiskDriver::Memory,
79 root: None,
80 url: None,
81 cdn_url: None,
82 #[cfg(feature = "s3")]
83 bucket: None,
84 #[cfg(feature = "s3")]
85 region: None,
86 }
87 }
88
89 pub fn with_url(mut self, url: impl Into<String>) -> Self {
91 self.url = Some(url.into());
92 self
93 }
94
95 pub fn with_cdn_url(mut self, cdn_url: impl Into<String>) -> Self {
97 self.cdn_url = Some(cdn_url.into());
98 self
99 }
100}
101
102#[derive(Clone)]
104pub struct Storage {
105 inner: Arc<StorageInner>,
106}
107
108struct StorageInner {
109 disks: DashMap<String, (Arc<dyn StorageDriver>, Option<String>)>,
110 default_disk: String,
111}
112
113impl Default for Storage {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119impl Storage {
120 pub fn new() -> Self {
122 let inner = StorageInner {
123 disks: DashMap::new(),
124 default_disk: "local".to_string(),
125 };
126
127 let storage = Self {
128 inner: Arc::new(inner),
129 };
130
131 let local = LocalDriver::new("storage");
133 storage
134 .inner
135 .disks
136 .insert("local".to_string(), (Arc::new(local), None));
137
138 storage
139 }
140
141 pub fn with_config(default_disk: &str, configs: Vec<(&str, DiskConfig)>) -> Self {
143 let inner = StorageInner {
144 disks: DashMap::new(),
145 default_disk: default_disk.to_string(),
146 };
147
148 let storage = Self {
149 inner: Arc::new(inner),
150 };
151
152 for (name, config) in configs {
153 let driver = Self::create_driver(&config);
154 storage
155 .inner
156 .disks
157 .insert(name.to_string(), (driver, config.cdn_url.clone()));
158 }
159
160 storage
161 }
162
163 pub fn with_storage_config(config: StorageConfig) -> Self {
181 let inner = StorageInner {
182 disks: DashMap::new(),
183 default_disk: config.default,
184 };
185
186 let storage = Self {
187 inner: Arc::new(inner),
188 };
189
190 for (name, disk_config) in config.disks {
191 let driver = Self::create_driver(&disk_config);
192 storage
193 .inner
194 .disks
195 .insert(name, (driver, disk_config.cdn_url.clone()));
196 }
197
198 storage
199 }
200
201 fn create_driver(config: &DiskConfig) -> Arc<dyn StorageDriver> {
203 match config.driver {
204 DiskDriver::Local => {
205 let root = config.root.clone().unwrap_or_else(|| "storage".to_string());
206 let mut driver = LocalDriver::new(root);
207 if let Some(url) = &config.url {
208 driver = driver.with_url_base(url);
209 }
210 Arc::new(driver)
211 }
212 DiskDriver::Memory => {
213 let mut driver = MemoryDriver::new();
214 if let Some(url) = &config.url {
215 driver = driver.with_url_base(url);
216 }
217 Arc::new(driver)
218 }
219 #[cfg(feature = "s3")]
220 DiskDriver::S3 => {
221 let bucket = config.bucket.clone().unwrap_or_default();
222 let region = config
223 .region
224 .clone()
225 .unwrap_or_else(|| "us-east-1".to_string());
226 let url_base = config.url.clone();
227 let endpoint_url = env_with_fallback("STORAGE_ENDPOINT", &["AWS_URL"]);
228 Arc::new(crate::drivers::S3Driver::new(
229 bucket,
230 region,
231 url_base,
232 endpoint_url,
233 ))
234 }
235 }
236 }
237
238 pub fn disk(&self, name: &str) -> Result<Disk, Error> {
240 let entry = self
241 .inner
242 .disks
243 .get(name)
244 .ok_or_else(|| Error::disk_not_configured(name))?;
245 let (driver, cdn_url) = entry.clone();
246
247 Ok(Disk::new(driver, cdn_url))
248 }
249
250 pub fn default_disk(&self) -> Result<Disk, Error> {
252 self.disk(&self.inner.default_disk)
253 }
254
255 pub fn register_disk(&self, name: impl Into<String>, driver: Arc<dyn StorageDriver>) {
257 self.inner.disks.insert(name.into(), (driver, None));
258 }
259
260 pub fn register_disk_with_cdn(
265 &self,
266 name: impl Into<String>,
267 driver: Arc<dyn StorageDriver>,
268 cdn_url: Option<String>,
269 ) {
270 self.inner.disks.insert(name.into(), (driver, cdn_url));
271 }
272
273 pub async fn exists(&self, path: &str) -> Result<bool, Error> {
277 self.default_disk()?.exists(path).await
278 }
279
280 pub async fn get(&self, path: &str) -> Result<Bytes, Error> {
282 self.default_disk()?.get(path).await
283 }
284
285 pub async fn get_string(&self, path: &str) -> Result<String, Error> {
287 self.default_disk()?.get_string(path).await
288 }
289
290 pub async fn put(&self, path: &str, contents: impl Into<Bytes>) -> Result<(), Error> {
292 self.default_disk()?.put(path, contents).await
293 }
294
295 pub async fn put_with_options(
297 &self,
298 path: &str,
299 contents: impl Into<Bytes>,
300 options: PutOptions,
301 ) -> Result<(), Error> {
302 self.default_disk()?
303 .put_with_options(path, contents, options)
304 .await
305 }
306
307 pub async fn delete(&self, path: &str) -> Result<(), Error> {
309 self.default_disk()?.delete(path).await
310 }
311
312 pub async fn copy(&self, from: &str, to: &str) -> Result<(), Error> {
314 self.default_disk()?.copy(from, to).await
315 }
316
317 pub async fn rename(&self, from: &str, to: &str) -> Result<(), Error> {
319 self.default_disk()?.rename(from, to).await
320 }
321
322 pub async fn url(&self, path: &str) -> Result<String, Error> {
324 self.default_disk()?.url(path).await
325 }
326
327 pub async fn cdn_url(&self, path: &str) -> Result<String, Error> {
329 self.default_disk()?.cdn_url(path).await
330 }
331}
332
333#[derive(Clone)]
335pub struct Disk {
336 driver: Arc<dyn StorageDriver>,
337 cdn_url: Option<String>,
338}
339
340impl Disk {
341 pub fn new(driver: Arc<dyn StorageDriver>, cdn_url: Option<String>) -> Self {
343 Self { driver, cdn_url }
344 }
345
346 pub async fn exists(&self, path: &str) -> Result<bool, Error> {
348 self.driver.exists(path).await
349 }
350
351 pub async fn get(&self, path: &str) -> Result<Bytes, Error> {
353 self.driver.get(path).await
354 }
355
356 pub async fn get_string(&self, path: &str) -> Result<String, Error> {
358 self.driver.get_string(path).await
359 }
360
361 pub async fn put(&self, path: &str, contents: impl Into<Bytes>) -> Result<(), Error> {
363 self.driver
364 .put(path, contents.into(), PutOptions::new())
365 .await
366 }
367
368 pub async fn put_with_options(
370 &self,
371 path: &str,
372 contents: impl Into<Bytes>,
373 options: PutOptions,
374 ) -> Result<(), Error> {
375 self.driver.put(path, contents.into(), options).await
376 }
377
378 pub async fn delete(&self, path: &str) -> Result<(), Error> {
380 self.driver.delete(path).await
381 }
382
383 pub async fn copy(&self, from: &str, to: &str) -> Result<(), Error> {
385 self.driver.copy(from, to).await
386 }
387
388 pub async fn rename(&self, from: &str, to: &str) -> Result<(), Error> {
390 self.driver.rename(from, to).await
391 }
392
393 pub async fn size(&self, path: &str) -> Result<u64, Error> {
395 self.driver.size(path).await
396 }
397
398 pub async fn metadata(&self, path: &str) -> Result<FileMetadata, Error> {
400 self.driver.metadata(path).await
401 }
402
403 pub async fn url(&self, path: &str) -> Result<String, Error> {
405 self.driver.url(path).await
406 }
407
408 pub async fn cdn_url(&self, path: &str) -> Result<String, Error> {
410 match &self.cdn_url {
411 Some(base) => {
412 let path = path.trim_start_matches('/');
413 Ok(format!("{}/{}", base.trim_end_matches('/'), path))
414 }
415 None => self.url(path).await,
416 }
417 }
418
419 pub async fn temporary_url(&self, path: &str, expiration: Duration) -> Result<String, Error> {
421 self.driver.temporary_url(path, expiration).await
422 }
423
424 pub async fn files(&self, directory: &str) -> Result<Vec<String>, Error> {
426 self.driver.files(directory).await
427 }
428
429 pub async fn all_files(&self, directory: &str) -> Result<Vec<String>, Error> {
431 self.driver.all_files(directory).await
432 }
433
434 pub async fn directories(&self, directory: &str) -> Result<Vec<String>, Error> {
436 self.driver.directories(directory).await
437 }
438
439 pub async fn make_directory(&self, path: &str) -> Result<(), Error> {
441 self.driver.make_directory(path).await
442 }
443
444 pub async fn delete_directory(&self, path: &str) -> Result<(), Error> {
446 self.driver.delete_directory(path).await
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[tokio::test]
455 async fn cdn_url_returns_cdn_when_configured() {
456 let storage = Storage::with_config(
457 "memory",
458 vec![(
459 "memory",
460 DiskConfig::memory()
461 .with_url("https://origin.example.com")
462 .with_cdn_url("https://cdn.example.com"),
463 )],
464 );
465 let url = storage
466 .disk("memory")
467 .unwrap()
468 .cdn_url("images/photo.jpg")
469 .await
470 .unwrap();
471 assert_eq!(url, "https://cdn.example.com/images/photo.jpg");
472 }
473
474 #[tokio::test]
475 async fn cdn_url_falls_back_to_origin() {
476 let storage = Storage::with_config(
477 "memory",
478 vec![(
479 "memory",
480 DiskConfig::memory().with_url("https://origin.example.com"),
481 )],
482 );
483 let url = storage
484 .disk("memory")
485 .unwrap()
486 .cdn_url("images/photo.jpg")
487 .await
488 .unwrap();
489 assert_eq!(url, "https://origin.example.com/images/photo.jpg");
490 }
491
492 #[tokio::test]
493 async fn cdn_url_no_double_slash() {
494 let storage = Storage::with_config(
495 "memory",
496 vec![(
497 "memory",
498 DiskConfig::memory().with_cdn_url("https://cdn.example.com/"),
499 )],
500 );
501 let url = storage
502 .disk("memory")
503 .unwrap()
504 .cdn_url("/index.html")
505 .await
506 .unwrap();
507 assert_eq!(url, "https://cdn.example.com/index.html");
508 }
509
510 #[tokio::test]
511 async fn cdn_url_via_storage_facade() {
512 let storage = Storage::with_config(
513 "memory",
514 vec![(
515 "memory",
516 DiskConfig::memory()
517 .with_url("https://origin.example.com")
518 .with_cdn_url("https://cdn.example.com"),
519 )],
520 );
521 let url = storage.cdn_url("images/photo.jpg").await.unwrap();
522 assert_eq!(url, "https://cdn.example.com/images/photo.jpg");
523 }
524
525 #[tokio::test]
526 async fn test_storage_default_disk() {
527 let storage = Storage::with_config("memory", vec![("memory", DiskConfig::memory())]);
528
529 storage.put("test.txt", "hello world").await.unwrap();
530 let contents = storage.get_string("test.txt").await.unwrap();
531 assert_eq!(contents, "hello world");
532 }
533
534 #[tokio::test]
535 async fn test_storage_multiple_disks() {
536 let storage = Storage::with_config(
537 "primary",
538 vec![
539 ("primary", DiskConfig::memory()),
540 ("backup", DiskConfig::memory()),
541 ],
542 );
543
544 storage
546 .disk("primary")
547 .unwrap()
548 .put("data.txt", "primary data")
549 .await
550 .unwrap();
551
552 storage
554 .disk("backup")
555 .unwrap()
556 .put("data.txt", "backup data")
557 .await
558 .unwrap();
559
560 let primary = storage
562 .disk("primary")
563 .unwrap()
564 .get_string("data.txt")
565 .await
566 .unwrap();
567 let backup = storage
568 .disk("backup")
569 .unwrap()
570 .get_string("data.txt")
571 .await
572 .unwrap();
573
574 assert_eq!(primary, "primary data");
575 assert_eq!(backup, "backup data");
576 }
577
578 #[tokio::test]
579 async fn test_disk_not_configured() {
580 let storage = Storage::with_config("memory", vec![("memory", DiskConfig::memory())]);
581 let result = storage.disk("nonexistent");
582 assert!(result.is_err());
583 }
584
585 #[tokio::test]
586 async fn test_register_disk() {
587 let storage = Storage::new();
588 let memory_driver = Arc::new(MemoryDriver::new());
589 storage.register_disk("test", memory_driver);
590
591 storage
592 .disk("test")
593 .unwrap()
594 .put("file.txt", "content")
595 .await
596 .unwrap();
597
598 assert!(storage
599 .disk("test")
600 .unwrap()
601 .exists("file.txt")
602 .await
603 .unwrap());
604 }
605
606 #[tokio::test]
608 async fn test_register_disk_with_cdn_url() {
609 let storage = Storage::new();
610 let driver = Arc::new(MemoryDriver::new());
611 storage.register_disk_with_cdn(
612 "cdn-disk",
613 driver,
614 Some("https://cdn.example.com".to_string()),
615 );
616 let url = storage
617 .disk("cdn-disk")
618 .unwrap()
619 .cdn_url("assets/app.js")
620 .await
621 .unwrap();
622 assert_eq!(url, "https://cdn.example.com/assets/app.js");
623 }
624
625 #[tokio::test]
627 async fn test_register_disk_with_cdn_none_falls_back() {
628 let storage = Storage::new();
629 storage.register_disk_with_cdn("no-cdn", Arc::new(MemoryDriver::new()), None);
630 assert!(storage.disk("no-cdn").is_ok());
632 }
633}