1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::error::{Error, Result};
5
6use super::config::BucketConfig;
7use super::facade::Storage;
8
9pub struct Buckets {
13 inner: Arc<HashMap<String, Storage>>,
14}
15
16impl Clone for Buckets {
17 fn clone(&self) -> Self {
18 Self {
19 inner: Arc::clone(&self.inner),
20 }
21 }
22}
23
24impl Buckets {
25 pub fn new(configs: &[BucketConfig]) -> Result<Self> {
34 let mut map = HashMap::with_capacity(configs.len());
35 for config in configs {
36 if config.name.is_empty() {
37 return Err(Error::internal(
38 "bucket config must have a name when used with Buckets",
39 ));
40 }
41 if map.contains_key(&config.name) {
42 return Err(Error::internal(format!(
43 "duplicate bucket name '{}'",
44 config.name
45 )));
46 }
47 let storage = Storage::new(config)?;
48 map.insert(config.name.clone(), storage);
49 }
50 Ok(Self {
51 inner: Arc::new(map),
52 })
53 }
54
55 pub fn get(&self, name: &str) -> Result<Storage> {
61 self.inner
62 .get(name)
63 .cloned()
64 .ok_or_else(|| Error::internal(format!("bucket '{name}' not configured")))
65 }
66
67 #[cfg(any(test, feature = "test-helpers"))]
69 pub fn memory(names: &[&str]) -> Self {
70 let mut map = HashMap::with_capacity(names.len());
71 for name in names {
72 map.insert((*name).to_string(), Storage::memory());
73 }
74 Self {
75 inner: Arc::new(map),
76 }
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::super::facade::PutInput;
83 use super::*;
84
85 fn test_input() -> PutInput {
86 PutInput {
87 data: bytes::Bytes::from_static(b"hello"),
88 prefix: "test/".into(),
89 filename: Some("test.txt".into()),
90 content_type: "text/plain".into(),
91 }
92 }
93
94 #[tokio::test]
95 async fn memory_buckets_get_existing() {
96 let buckets = Buckets::memory(&["avatars", "docs"]);
97 let store = buckets.get("avatars").unwrap();
98 let key = store.put(&test_input()).await.unwrap();
99 assert!(store.exists(&key).await.unwrap());
100 }
101
102 #[test]
103 fn get_unknown_name_returns_error() {
104 let buckets = Buckets::memory(&["avatars"]);
105 assert!(buckets.get("nonexistent").is_err());
106 }
107
108 #[tokio::test]
109 async fn buckets_are_isolated() {
110 let buckets = Buckets::memory(&["a", "b"]);
111 let store_a = buckets.get("a").unwrap();
112 let store_b = buckets.get("b").unwrap();
113
114 let key = store_a.put(&test_input()).await.unwrap();
115
116 assert!(store_a.exists(&key).await.unwrap());
117 assert!(!store_b.exists(&key).await.unwrap());
118 }
119
120 #[test]
121 fn empty_names_vec_is_valid() {
122 let buckets = Buckets::memory(&[]);
123 assert!(buckets.get("anything").is_err());
124 }
125
126 #[test]
127 fn clone_is_cheap() {
128 let buckets = Buckets::memory(&["a"]);
129 let cloned = buckets.clone();
130 assert!(cloned.get("a").is_ok());
131 }
132
133 #[test]
134 fn new_rejects_empty_name() {
135 let configs = vec![BucketConfig {
136 bucket: "b1".into(),
137 endpoint: "https://s3.example.com".into(),
138 ..Default::default()
139 }];
140 assert!(Buckets::new(&configs).is_err());
141 }
142}