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