multistore_path_mapping/
lib.rs1use multistore::api::list_rewrite::ListRewrite;
10use multistore::registry::{BucketRegistry, ResolvedBucket};
11
12#[derive(Debug, Clone)]
14pub struct PathMapping {
15 pub bucket_segments: usize,
18
19 pub bucket_separator: String,
22
23 pub display_bucket_segments: usize,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct MappedPath {
31 pub bucket: String,
33 pub key: Option<String>,
35 pub display_bucket: String,
37 pub key_prefix: String,
39 pub segments: Vec<String>,
41}
42
43impl PathMapping {
44 pub fn parse(&self, path: &str) -> Option<MappedPath> {
52 let trimmed = path.strip_prefix('/').unwrap_or(path);
53 if trimmed.is_empty() {
54 return None;
55 }
56
57 let parts: Vec<&str> = trimmed.splitn(self.bucket_segments + 1, '/').collect();
60
61 if parts.len() < self.bucket_segments {
62 return None;
63 }
64
65 for part in &parts[..self.bucket_segments] {
67 if part.is_empty() {
68 return None;
69 }
70 }
71
72 let segments: Vec<String> = parts[..self.bucket_segments]
73 .iter()
74 .map(|s| s.to_string())
75 .collect();
76
77 let bucket = segments.join(&self.bucket_separator);
78
79 let key = if parts.len() > self.bucket_segments {
80 let k = parts[self.bucket_segments];
81 if k.is_empty() {
82 None
83 } else {
84 Some(k.to_string())
85 }
86 } else {
87 None
88 };
89
90 let display_bucket = segments[..self.display_bucket_segments].join("/");
91
92 let key_prefix = if self.display_bucket_segments < self.bucket_segments {
93 let prefix_parts = &segments[self.display_bucket_segments..self.bucket_segments];
94 format!("{}/", prefix_parts.join("/"))
95 } else {
96 String::new()
97 };
98
99 Some(MappedPath {
100 bucket,
101 key,
102 display_bucket,
103 key_prefix,
104 segments,
105 })
106 }
107
108 pub fn parse_bucket_name(&self, bucket_name: &str) -> Option<MappedPath> {
113 let segments: Vec<String> = bucket_name
114 .split(&self.bucket_separator)
115 .map(|s| s.to_string())
116 .collect();
117
118 if segments.len() != self.bucket_segments {
119 return None;
120 }
121
122 for seg in &segments {
124 if seg.is_empty() {
125 return None;
126 }
127 }
128
129 let display_bucket = segments[..self.display_bucket_segments].join("/");
130
131 let key_prefix = if self.display_bucket_segments < self.bucket_segments {
132 let prefix_parts = &segments[self.display_bucket_segments..self.bucket_segments];
133 format!("{}/", prefix_parts.join("/"))
134 } else {
135 String::new()
136 };
137
138 Some(MappedPath {
139 bucket: bucket_name.to_string(),
140 key: None,
141 display_bucket,
142 key_prefix,
143 segments,
144 })
145 }
146}
147
148#[derive(Debug, Clone)]
154pub struct MappedRegistry<R> {
155 inner: R,
156 mapping: PathMapping,
157}
158
159impl<R> MappedRegistry<R> {
160 pub fn new(inner: R, mapping: PathMapping) -> Self {
162 Self { inner, mapping }
163 }
164}
165
166impl<R: BucketRegistry> BucketRegistry for MappedRegistry<R> {
167 async fn get_bucket(
168 &self,
169 name: &str,
170 identity: &multistore::types::ResolvedIdentity,
171 operation: &multistore::types::S3Operation,
172 ) -> Result<ResolvedBucket, multistore::error::ProxyError> {
173 let mapped = self.mapping.parse_bucket_name(name);
174
175 let mut resolved = self.inner.get_bucket(name, identity, operation).await?;
176
177 if let Some(mapped) = mapped {
178 tracing::debug!(
179 bucket = %name,
180 display_name = %mapped.display_bucket,
181 key_prefix = %mapped.key_prefix,
182 "Applying path mapping to resolved bucket"
183 );
184
185 resolved.display_name = Some(mapped.display_bucket);
186
187 if !mapped.key_prefix.is_empty() {
188 resolved.list_rewrite = Some(ListRewrite {
189 strip_prefix: String::new(),
190 add_prefix: mapped.key_prefix,
191 });
192 }
193 }
194
195 Ok(resolved)
196 }
197
198 async fn list_buckets(
199 &self,
200 identity: &multistore::types::ResolvedIdentity,
201 ) -> Result<Vec<multistore::api::response::BucketEntry>, multistore::error::ProxyError> {
202 self.inner.list_buckets(identity).await
203 }
204
205 fn bucket_owner(&self) -> multistore::types::BucketOwner {
206 self.inner.bucket_owner()
207 }
208}