use multistore::api::list_rewrite::ListRewrite;
use multistore::registry::{BucketRegistry, ResolvedBucket};
#[derive(Debug, Clone)]
pub struct PathMapping {
pub bucket_segments: usize,
pub bucket_separator: String,
pub display_bucket_segments: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MappedPath {
pub bucket: String,
pub key: Option<String>,
pub display_bucket: String,
pub key_prefix: String,
pub segments: Vec<String>,
}
impl PathMapping {
pub fn parse(&self, path: &str) -> Option<MappedPath> {
let trimmed = path.strip_prefix('/').unwrap_or(path);
if trimmed.is_empty() {
return None;
}
let parts: Vec<&str> = trimmed.splitn(self.bucket_segments + 1, '/').collect();
if parts.len() < self.bucket_segments {
return None;
}
for part in &parts[..self.bucket_segments] {
if part.is_empty() {
return None;
}
}
let segments: Vec<String> = parts[..self.bucket_segments]
.iter()
.map(|s| s.to_string())
.collect();
let bucket = segments.join(&self.bucket_separator);
let key = if parts.len() > self.bucket_segments {
let k = parts[self.bucket_segments];
if k.is_empty() {
None
} else {
Some(k.to_string())
}
} else {
None
};
let display_bucket = segments[..self.display_bucket_segments].join("/");
let key_prefix = if self.display_bucket_segments < self.bucket_segments {
let prefix_parts = &segments[self.display_bucket_segments..self.bucket_segments];
format!("{}/", prefix_parts.join("/"))
} else {
String::new()
};
Some(MappedPath {
bucket,
key,
display_bucket,
key_prefix,
segments,
})
}
pub fn parse_bucket_name(&self, bucket_name: &str) -> Option<MappedPath> {
let segments: Vec<String> = bucket_name
.split(&self.bucket_separator)
.map(|s| s.to_string())
.collect();
if segments.len() != self.bucket_segments {
return None;
}
for seg in &segments {
if seg.is_empty() {
return None;
}
}
let display_bucket = segments[..self.display_bucket_segments].join("/");
let key_prefix = if self.display_bucket_segments < self.bucket_segments {
let prefix_parts = &segments[self.display_bucket_segments..self.bucket_segments];
format!("{}/", prefix_parts.join("/"))
} else {
String::new()
};
Some(MappedPath {
bucket: bucket_name.to_string(),
key: None,
display_bucket,
key_prefix,
segments,
})
}
}
#[derive(Debug, Clone)]
pub struct MappedRegistry<R> {
inner: R,
mapping: PathMapping,
}
impl<R> MappedRegistry<R> {
pub fn new(inner: R, mapping: PathMapping) -> Self {
Self { inner, mapping }
}
}
impl<R: BucketRegistry> BucketRegistry for MappedRegistry<R> {
async fn get_bucket(
&self,
name: &str,
identity: &multistore::types::ResolvedIdentity,
operation: &multistore::types::S3Operation,
) -> Result<ResolvedBucket, multistore::error::ProxyError> {
let mapped = self.mapping.parse_bucket_name(name);
let mut resolved = self.inner.get_bucket(name, identity, operation).await?;
if let Some(mapped) = mapped {
tracing::debug!(
bucket = %name,
display_name = %mapped.display_bucket,
key_prefix = %mapped.key_prefix,
"Applying path mapping to resolved bucket"
);
resolved.display_name = Some(mapped.display_bucket);
if !mapped.key_prefix.is_empty() {
resolved.list_rewrite = Some(ListRewrite {
strip_prefix: String::new(),
add_prefix: mapped.key_prefix,
});
}
}
Ok(resolved)
}
async fn list_buckets(
&self,
identity: &multistore::types::ResolvedIdentity,
) -> Result<Vec<multistore::api::response::BucketEntry>, multistore::error::ProxyError> {
self.inner.list_buckets(identity).await
}
fn bucket_owner(&self) -> multistore::types::BucketOwner {
self.inner.bucket_owner()
}
}