use crate::s3::{S3Backend, S3Config};
use crate::StorageBackend;
use async_trait::async_trait;
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub enum Provider {
B2 { region: String },
DigitalOceanSpaces { region: String },
}
impl Provider {
pub fn endpoint(&self) -> String {
match self {
Provider::B2 { region } => {
format!("https://s3.{}.backblazeb2.com", region)
}
Provider::DigitalOceanSpaces { region } => {
format!("https://{}.digitaloceanspaces.com", region)
}
}
}
pub fn name(&self) -> &str {
match self {
Provider::B2 { .. } => "Backblaze B2",
Provider::DigitalOceanSpaces { .. } => "DigitalOcean Spaces",
}
}
pub fn region(&self) -> &str {
match self {
Provider::B2 { region } | Provider::DigitalOceanSpaces { region } => region,
}
}
fn validate(&self) -> anyhow::Result<()> {
let region = match self {
Provider::B2 { region } => {
match region.as_str() {
"us-west-002" | "eu-central-001" | "ap-northeast-001" => region.as_str(),
_ => {
return Err(anyhow::anyhow!(
"Invalid B2 region: {}. Valid regions: us-west-002, eu-central-001, ap-northeast-001",
region
))
}
}
}
Provider::DigitalOceanSpaces { region } => {
match region.as_str() {
"nyc3" | "sfo3" | "ams3" | "sgp1" | "blr1" | "fra1" | "lon1" | "syd1"
| "tor1" | "iad1" => region.as_str(),
_ => {
return Err(anyhow::anyhow!(
"Invalid DigitalOcean region: {}. Valid regions: nyc3, sfo3, ams3, sgp1, blr1, fra1, lon1, syd1, tor1, iad1",
region
))
}
}
}
};
if region.is_empty() {
return Err(anyhow::anyhow!("region cannot be empty"));
}
Ok(())
}
}
#[derive(Clone)]
pub struct B2SpacesBackend {
inner: Arc<S3Backend>,
provider: Provider,
bucket: String,
}
impl B2SpacesBackend {
pub async fn new(
provider: Provider,
bucket: &str,
access_key: &str,
secret_key: &str,
) -> anyhow::Result<Self> {
provider.validate()?;
if bucket.is_empty() {
return Err(anyhow::anyhow!("bucket/space name cannot be empty"));
}
if bucket.len() > 63 {
return Err(anyhow::anyhow!(
"bucket/space name must be 63 characters or less"
));
}
if !bucket
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(anyhow::anyhow!(
"bucket/space name must contain only lowercase letters, numbers, and hyphens"
));
}
if bucket.starts_with('-') || bucket.ends_with('-') {
return Err(anyhow::anyhow!(
"bucket/space name cannot start or end with a hyphen"
));
}
if access_key.is_empty() {
return Err(anyhow::anyhow!("access key cannot be empty"));
}
if secret_key.is_empty() {
return Err(anyhow::anyhow!("secret key cannot be empty"));
}
tracing::info!(
provider = provider.name(),
region = provider.region(),
bucket = bucket,
endpoint = provider.endpoint(),
"Initializing B2/Spaces backend"
);
let s3_config = S3Config {
bucket: bucket.to_string(),
endpoint: Some(provider.endpoint()),
..Default::default()
};
let inner = S3Backend::with_credentials(
s3_config,
access_key,
secret_key,
provider.region(),
)
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to initialize {} backend: {}. Check credentials, bucket name, and network connectivity.",
provider.name(),
e
)
})?;
tracing::info!(
provider = provider.name(),
bucket = bucket,
"Successfully connected to B2/Spaces backend"
);
Ok(B2SpacesBackend {
inner: Arc::new(inner),
provider,
bucket: bucket.to_string(),
})
}
pub async fn from_env() -> anyhow::Result<Self> {
let provider_str = std::env::var("B2_SPACES_PROVIDER")
.map_err(|_| anyhow::anyhow!("B2_SPACES_PROVIDER environment variable not set"))?;
let region = std::env::var("B2_SPACES_REGION")
.map_err(|_| anyhow::anyhow!("B2_SPACES_REGION environment variable not set"))?;
let bucket = std::env::var("B2_SPACES_BUCKET")
.map_err(|_| anyhow::anyhow!("B2_SPACES_BUCKET environment variable not set"))?;
let access_key = std::env::var("B2_SPACES_ACCESS_KEY")
.map_err(|_| anyhow::anyhow!("B2_SPACES_ACCESS_KEY environment variable not set"))?;
let secret_key = std::env::var("B2_SPACES_SECRET_KEY")
.map_err(|_| anyhow::anyhow!("B2_SPACES_SECRET_KEY environment variable not set"))?;
let provider = match provider_str.to_lowercase().as_str() {
"b2" => Provider::B2 { region },
"digitalocean" | "do" => Provider::DigitalOceanSpaces { region },
other => {
return Err(anyhow::anyhow!(
"Invalid provider '{}'. Must be 'b2' or 'digitalocean'",
other
))
}
};
Self::new(provider, &bucket, &access_key, &secret_key).await
}
pub fn provider(&self) -> &Provider {
&self.provider
}
pub fn bucket(&self) -> &str {
&self.bucket
}
pub fn endpoint(&self) -> String {
self.provider.endpoint()
}
}
impl fmt::Debug for B2SpacesBackend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("B2SpacesBackend")
.field("provider", &self.provider.name())
.field("region", &self.provider.region())
.field("bucket", &self.bucket)
.field("endpoint", &self.endpoint())
.finish()
}
}
#[async_trait]
impl StorageBackend for B2SpacesBackend {
async fn get(&self, key: &str) -> anyhow::Result<Vec<u8>> {
tracing::trace!(
provider = self.provider.name(),
bucket = self.bucket,
key = key,
"Getting object from B2/Spaces"
);
self.inner.get(key).await.map_err(|e| {
anyhow::anyhow!("Failed to get object from {}: {}", self.provider.name(), e)
})
}
async fn put(&self, key: &str, data: &[u8]) -> anyhow::Result<()> {
tracing::trace!(
provider = self.provider.name(),
bucket = self.bucket,
key = key,
size = data.len(),
"Putting object to B2/Spaces"
);
self.inner
.put(key, data)
.await
.map_err(|e| anyhow::anyhow!("Failed to put object to {}: {}", self.provider.name(), e))
}
async fn exists(&self, key: &str) -> anyhow::Result<bool> {
tracing::trace!(
provider = self.provider.name(),
bucket = self.bucket,
key = key,
"Checking object existence in B2/Spaces"
);
self.inner.exists(key).await.map_err(|e| {
anyhow::anyhow!(
"Failed to check object existence in {}: {}",
self.provider.name(),
e
)
})
}
async fn delete(&self, key: &str) -> anyhow::Result<()> {
tracing::trace!(
provider = self.provider.name(),
bucket = self.bucket,
key = key,
"Deleting object from B2/Spaces"
);
self.inner.delete(key).await.map_err(|e| {
anyhow::anyhow!(
"Failed to delete object from {}: {}",
self.provider.name(),
e
)
})
}
async fn list_objects(&self, prefix: &str) -> anyhow::Result<Vec<String>> {
tracing::trace!(
provider = self.provider.name(),
bucket = self.bucket,
prefix = prefix,
"Listing objects in B2/Spaces"
);
self.inner.list_objects(prefix).await.map_err(|e| {
anyhow::anyhow!("Failed to list objects in {}: {}", self.provider.name(), e)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provider_b2_endpoint() {
let provider = Provider::B2 {
region: "us-west-002".to_string(),
};
assert_eq!(
provider.endpoint(),
"https://s3.us-west-002.backblazeb2.com"
);
}
#[test]
fn test_provider_digitalocean_endpoint() {
let provider = Provider::DigitalOceanSpaces {
region: "nyc3".to_string(),
};
assert_eq!(provider.endpoint(), "https://nyc3.digitaloceanspaces.com");
}
#[test]
fn test_provider_name() {
let b2_provider = Provider::B2 {
region: "us-west-002".to_string(),
};
assert_eq!(b2_provider.name(), "Backblaze B2");
let do_provider = Provider::DigitalOceanSpaces {
region: "nyc3".to_string(),
};
assert_eq!(do_provider.name(), "DigitalOcean Spaces");
}
#[test]
fn test_provider_region() {
let provider = Provider::B2 {
region: "eu-central-001".to_string(),
};
assert_eq!(provider.region(), "eu-central-001");
}
#[test]
fn test_valid_b2_regions() {
let valid_regions = vec!["us-west-002", "eu-central-001", "ap-northeast-001"];
for region in valid_regions {
let provider = Provider::B2 {
region: region.to_string(),
};
assert!(
provider.validate().is_ok(),
"Region {} should be valid for B2",
region
);
}
}
#[test]
fn test_invalid_b2_region() {
let provider = Provider::B2 {
region: "us-east-1".to_string(), };
assert!(provider.validate().is_err());
}
#[test]
fn test_valid_do_regions() {
let valid_regions = vec![
"nyc3", "sfo3", "ams3", "sgp1", "blr1", "fra1", "lon1", "syd1", "tor1", "iad1",
];
for region in valid_regions {
let provider = Provider::DigitalOceanSpaces {
region: region.to_string(),
};
assert!(
provider.validate().is_ok(),
"Region {} should be valid for DigitalOcean Spaces",
region
);
}
}
#[test]
fn test_invalid_do_region() {
let provider = Provider::DigitalOceanSpaces {
region: "invalid-region".to_string(),
};
assert!(provider.validate().is_err());
}
#[tokio::test]
#[ignore = "requires valid B2 credentials - run with B2_APPLICATION_KEY_ID and B2_APPLICATION_KEY"]
async fn test_new_b2_backend() {
let key_id =
std::env::var("B2_APPLICATION_KEY_ID").expect("B2_APPLICATION_KEY_ID required");
let key = std::env::var("B2_APPLICATION_KEY").expect("B2_APPLICATION_KEY required");
let bucket = std::env::var("B2_TEST_BUCKET").unwrap_or_else(|_| "test-bucket".to_string());
let backend = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
&bucket,
&key_id,
&key,
)
.await;
assert!(
backend.is_ok(),
"Failed to create B2 backend: {:?}",
backend.err()
);
let backend = backend.unwrap();
assert_eq!(backend.bucket(), bucket);
assert_eq!(backend.endpoint(), "https://s3.us-west-002.backblazeb2.com");
}
#[tokio::test]
#[ignore = "requires valid DigitalOcean credentials - run with DO_SPACES_KEY and DO_SPACES_SECRET"]
async fn test_new_digitalocean_backend() {
let key = std::env::var("DO_SPACES_KEY").expect("DO_SPACES_KEY required");
let secret = std::env::var("DO_SPACES_SECRET").expect("DO_SPACES_SECRET required");
let space = std::env::var("DO_TEST_SPACE").unwrap_or_else(|_| "test-space".to_string());
let backend = B2SpacesBackend::new(
Provider::DigitalOceanSpaces {
region: "nyc3".to_string(),
},
&space,
&key,
&secret,
)
.await;
assert!(
backend.is_ok(),
"Failed to create DO Spaces backend: {:?}",
backend.err()
);
let backend = backend.unwrap();
assert_eq!(backend.bucket(), space);
assert_eq!(backend.endpoint(), "https://nyc3.digitaloceanspaces.com");
}
#[tokio::test]
#[ignore = "requires valid B2 credentials for all regions"]
async fn test_new_all_b2_regions() {
let key_id =
std::env::var("B2_APPLICATION_KEY_ID").expect("B2_APPLICATION_KEY_ID required");
let key = std::env::var("B2_APPLICATION_KEY").expect("B2_APPLICATION_KEY required");
let bucket = std::env::var("B2_TEST_BUCKET").unwrap_or_else(|_| "test-bucket".to_string());
let regions = vec!["us-west-002", "eu-central-001", "ap-northeast-001"];
for region in regions {
let backend = B2SpacesBackend::new(
Provider::B2 {
region: region.to_string(),
},
&bucket,
&key_id,
&key,
)
.await;
assert!(
backend.is_ok(),
"Failed to create backend for B2 region {}",
region
);
}
}
#[tokio::test]
#[ignore = "requires valid DO credentials for all regions"]
async fn test_new_all_do_regions() {
let key = std::env::var("DO_SPACES_KEY").expect("DO_SPACES_KEY required");
let secret = std::env::var("DO_SPACES_SECRET").expect("DO_SPACES_SECRET required");
let space = std::env::var("DO_TEST_SPACE").unwrap_or_else(|_| "test-space".to_string());
let regions = vec![
"nyc3", "sfo3", "ams3", "sgp1", "blr1", "fra1", "lon1", "syd1", "tor1", "iad1",
];
for region in regions {
let backend = B2SpacesBackend::new(
Provider::DigitalOceanSpaces {
region: region.to_string(),
},
&space,
&key,
&secret,
)
.await;
assert!(
backend.is_ok(),
"Failed to create backend for DO region {}",
region
);
}
}
#[tokio::test]
async fn test_empty_bucket_name() {
let result = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
"",
"key",
"secret",
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[tokio::test]
async fn test_bucket_name_too_long() {
let long_name = "a".repeat(64);
let result = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
&long_name,
"key",
"secret",
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("63 characters"));
}
#[tokio::test]
async fn test_bucket_name_invalid_characters() {
let result = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
"INVALID_BUCKET",
"key",
"secret",
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("lowercase"));
}
#[tokio::test]
async fn test_bucket_name_starts_with_hyphen() {
let result = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
"-invalid",
"key",
"secret",
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_bucket_name_ends_with_hyphen() {
let result = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
"invalid-",
"key",
"secret",
)
.await;
assert!(result.is_err());
}
#[test]
fn test_valid_bucket_names() {
let valid_names = vec!["my-bucket", "bucket123", "a", "my-bucket-123", "1234567890"];
for name in valid_names {
assert!(
!name.is_empty(),
"Bucket name '{}' should not be empty",
name
);
assert!(
name.len() <= 63,
"Bucket name '{}' should be <= 63 chars",
name
);
assert!(
name.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
"Bucket name '{}' should contain only lowercase letters, numbers, and hyphens",
name
);
assert!(
!name.starts_with('-') && !name.ends_with('-'),
"Bucket name '{}' should not start or end with hyphen",
name
);
}
}
#[tokio::test]
async fn test_empty_access_key() {
let result = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
"bucket",
"",
"secret",
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("access key"));
}
#[tokio::test]
async fn test_empty_secret_key() {
let result = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
"bucket",
"key",
"",
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("secret key"));
}
#[tokio::test]
#[ignore = "requires valid B2 credentials"]
async fn test_debug_impl() {
let key_id =
std::env::var("B2_APPLICATION_KEY_ID").expect("B2_APPLICATION_KEY_ID required");
let key = std::env::var("B2_APPLICATION_KEY").expect("B2_APPLICATION_KEY required");
let bucket = std::env::var("B2_TEST_BUCKET").unwrap_or_else(|_| "test-bucket".to_string());
let backend = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
&bucket,
&key_id,
&key,
)
.await
.unwrap();
let debug_str = format!("{:?}", backend);
println!("Debug output: {}", debug_str);
assert!(debug_str.contains("B2SpacesBackend"));
assert!(debug_str.contains("Backblaze B2"));
assert!(debug_str.contains("us-west-002"));
}
#[tokio::test]
#[ignore = "requires valid B2 credentials"]
async fn test_clone() {
let key_id =
std::env::var("B2_APPLICATION_KEY_ID").expect("B2_APPLICATION_KEY_ID required");
let key = std::env::var("B2_APPLICATION_KEY").expect("B2_APPLICATION_KEY required");
let bucket = std::env::var("B2_TEST_BUCKET").unwrap_or_else(|_| "test-bucket".to_string());
let backend1 = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
&bucket,
&key_id,
&key,
)
.await
.unwrap();
let backend2 = backend1.clone();
assert_eq!(backend2.bucket(), backend1.bucket());
assert_eq!(backend2.endpoint(), backend1.endpoint());
}
#[tokio::test]
#[ignore = "requires valid B2 credentials"]
async fn test_from_env_b2() {
std::env::set_var("B2_SPACES_PROVIDER", "b2");
std::env::set_var("B2_SPACES_REGION", "us-west-002");
std::env::set_var("B2_SPACES_BUCKET", "test-bucket");
std::env::set_var("B2_SPACES_ACCESS_KEY", "testkey");
std::env::set_var("B2_SPACES_SECRET_KEY", "testsecret");
let result = B2SpacesBackend::from_env().await;
assert!(result.is_ok());
let backend = result.unwrap();
assert_eq!(backend.bucket(), "test-bucket");
assert_eq!(backend.endpoint(), "https://s3.us-west-002.backblazeb2.com");
std::env::remove_var("B2_SPACES_PROVIDER");
std::env::remove_var("B2_SPACES_REGION");
std::env::remove_var("B2_SPACES_BUCKET");
std::env::remove_var("B2_SPACES_ACCESS_KEY");
std::env::remove_var("B2_SPACES_SECRET_KEY");
}
#[tokio::test]
#[ignore = "requires valid DigitalOcean credentials"]
async fn test_from_env_digitalocean() {
std::env::set_var("B2_SPACES_PROVIDER", "digitalocean");
std::env::set_var("B2_SPACES_REGION", "nyc3");
std::env::set_var("B2_SPACES_BUCKET", "my-space");
std::env::set_var("B2_SPACES_ACCESS_KEY", "do-key");
std::env::set_var("B2_SPACES_SECRET_KEY", "do-secret");
let result = B2SpacesBackend::from_env().await;
assert!(result.is_ok());
let backend = result.unwrap();
assert_eq!(backend.bucket(), "my-space");
assert_eq!(backend.endpoint(), "https://nyc3.digitaloceanspaces.com");
std::env::remove_var("B2_SPACES_PROVIDER");
std::env::remove_var("B2_SPACES_REGION");
std::env::remove_var("B2_SPACES_BUCKET");
std::env::remove_var("B2_SPACES_ACCESS_KEY");
std::env::remove_var("B2_SPACES_SECRET_KEY");
}
#[tokio::test]
#[ignore = "requires environment setup"]
async fn test_from_env_do_alias() {
std::env::set_var("B2_SPACES_PROVIDER", "do");
std::env::set_var("B2_SPACES_REGION", "sfo3");
std::env::set_var("B2_SPACES_BUCKET", "space");
std::env::set_var("B2_SPACES_ACCESS_KEY", "key");
std::env::set_var("B2_SPACES_SECRET_KEY", "secret");
let result = B2SpacesBackend::from_env().await;
assert!(result.is_ok());
std::env::remove_var("B2_SPACES_PROVIDER");
std::env::remove_var("B2_SPACES_REGION");
std::env::remove_var("B2_SPACES_BUCKET");
std::env::remove_var("B2_SPACES_ACCESS_KEY");
std::env::remove_var("B2_SPACES_SECRET_KEY");
}
#[tokio::test]
async fn test_from_env_missing_variables() {
std::env::remove_var("B2_SPACES_PROVIDER");
std::env::remove_var("B2_SPACES_REGION");
std::env::remove_var("B2_SPACES_BUCKET");
std::env::remove_var("B2_SPACES_ACCESS_KEY");
std::env::remove_var("B2_SPACES_SECRET_KEY");
let result = B2SpacesBackend::from_env().await;
assert!(result.is_err());
}
#[tokio::test]
#[ignore = "requires environment setup"]
async fn test_from_env_invalid_provider() {
std::env::set_var("B2_SPACES_PROVIDER", "invalid");
std::env::set_var("B2_SPACES_REGION", "us-west-002");
std::env::set_var("B2_SPACES_BUCKET", "bucket");
std::env::set_var("B2_SPACES_ACCESS_KEY", "key");
std::env::set_var("B2_SPACES_SECRET_KEY", "secret");
let result = B2SpacesBackend::from_env().await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid provider"));
std::env::remove_var("B2_SPACES_PROVIDER");
std::env::remove_var("B2_SPACES_REGION");
std::env::remove_var("B2_SPACES_BUCKET");
std::env::remove_var("B2_SPACES_ACCESS_KEY");
std::env::remove_var("B2_SPACES_SECRET_KEY");
}
#[tokio::test]
#[ignore = "requires valid B2 credentials"]
async fn test_get_empty_key() {
let key_id =
std::env::var("B2_APPLICATION_KEY_ID").expect("B2_APPLICATION_KEY_ID required");
let key = std::env::var("B2_APPLICATION_KEY").expect("B2_APPLICATION_KEY required");
let bucket = std::env::var("B2_TEST_BUCKET").unwrap_or_else(|_| "test-bucket".to_string());
let backend = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
&bucket,
&key_id,
&key,
)
.await
.unwrap();
let result = backend.get("").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[tokio::test]
#[ignore = "requires valid B2 credentials"]
async fn test_put_empty_key() {
let key_id =
std::env::var("B2_APPLICATION_KEY_ID").expect("B2_APPLICATION_KEY_ID required");
let key = std::env::var("B2_APPLICATION_KEY").expect("B2_APPLICATION_KEY required");
let bucket = std::env::var("B2_TEST_BUCKET").unwrap_or_else(|_| "test-bucket".to_string());
let backend = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
&bucket,
&key_id,
&key,
)
.await
.unwrap();
let result = backend.put("", b"data").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[tokio::test]
#[ignore = "requires valid B2 credentials"]
async fn test_exists_empty_key() {
let key_id =
std::env::var("B2_APPLICATION_KEY_ID").expect("B2_APPLICATION_KEY_ID required");
let key = std::env::var("B2_APPLICATION_KEY").expect("B2_APPLICATION_KEY required");
let bucket = std::env::var("B2_TEST_BUCKET").unwrap_or_else(|_| "test-bucket".to_string());
let backend = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
&bucket,
&key_id,
&key,
)
.await
.unwrap();
let result = backend.exists("").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[tokio::test]
#[ignore = "requires valid B2 credentials"]
async fn test_delete_empty_key() {
let key_id =
std::env::var("B2_APPLICATION_KEY_ID").expect("B2_APPLICATION_KEY_ID required");
let key = std::env::var("B2_APPLICATION_KEY").expect("B2_APPLICATION_KEY required");
let bucket = std::env::var("B2_TEST_BUCKET").unwrap_or_else(|_| "test-bucket".to_string());
let backend = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
&bucket,
&key_id,
&key,
)
.await
.unwrap();
let result = backend.delete("").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[tokio::test]
#[ignore = "requires valid B2 credentials"]
async fn test_crud_operations() {
let key_id =
std::env::var("B2_APPLICATION_KEY_ID").expect("B2_APPLICATION_KEY_ID required");
let key = std::env::var("B2_APPLICATION_KEY").expect("B2_APPLICATION_KEY required");
let bucket =
std::env::var("B2_TEST_BUCKET").expect("B2_TEST_BUCKET required for CRUD tests");
let backend = B2SpacesBackend::new(
Provider::B2 {
region: "us-west-002".to_string(),
},
&bucket,
&key_id,
&key,
)
.await
.expect("Failed to create B2 backend");
let test_key = format!(
"mediagit-test/test-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let test_data = b"Hello, MediaGit B2 Backend!";
backend
.put(&test_key, test_data)
.await
.expect("Failed to put object");
let exists = backend
.exists(&test_key)
.await
.expect("Failed to check existence");
assert!(exists, "Object should exist after put");
let retrieved = backend.get(&test_key).await.expect("Failed to get object");
assert_eq!(retrieved, test_data, "Retrieved data should match original");
let objects = backend
.list_objects("mediagit-test/")
.await
.expect("Failed to list objects");
assert!(
objects.contains(&test_key),
"Listed objects should contain our test key"
);
backend
.delete(&test_key)
.await
.expect("Failed to delete object");
let exists_after = backend
.exists(&test_key)
.await
.expect("Failed to check existence after delete");
assert!(!exists_after, "Object should not exist after delete");
}
#[tokio::test]
#[ignore = "requires valid DigitalOcean credentials"]
async fn test_crud_operations_digitalocean() {
let access_key = std::env::var("DO_SPACES_KEY").expect("DO_SPACES_KEY required");
let secret_key = std::env::var("DO_SPACES_SECRET").expect("DO_SPACES_SECRET required");
let space = std::env::var("DO_TEST_SPACE").expect("DO_TEST_SPACE required for CRUD tests");
let backend = B2SpacesBackend::new(
Provider::DigitalOceanSpaces {
region: "nyc3".to_string(),
},
&space,
&access_key,
&secret_key,
)
.await
.expect("Failed to create DO Spaces backend");
let test_key = format!(
"mediagit-test/test-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let test_data = b"Hello, MediaGit DO Spaces Backend!";
backend
.put(&test_key, test_data)
.await
.expect("Failed to put object");
let exists = backend
.exists(&test_key)
.await
.expect("Failed to check existence");
assert!(exists, "Object should exist after put");
let retrieved = backend.get(&test_key).await.expect("Failed to get object");
assert_eq!(retrieved, test_data, "Retrieved data should match original");
backend
.delete(&test_key)
.await
.expect("Failed to delete object");
}
}