use bytes::Bytes;
use std::time::Duration;
use crate::auth::Credentials;
use crate::error::{AzureError, CloudError, Result};
use crate::retry::{RetryConfig, RetryExecutor};
use super::CloudStorageBackend;
#[derive(Debug, Clone, Copy)]
pub enum AccessTier {
Hot,
Cool,
Archive,
}
#[derive(Debug, Clone)]
pub struct AzureBlobBackend {
pub account_name: String,
pub container: String,
pub prefix: String,
pub sas_token: Option<String>,
pub account_key: Option<String>,
pub access_tier: AccessTier,
pub timeout: Duration,
pub retry_config: RetryConfig,
pub credentials: Option<Credentials>,
pub hierarchical_namespace: bool,
}
impl AzureBlobBackend {
#[must_use]
pub fn new(account_name: impl Into<String>, container: impl Into<String>) -> Self {
Self {
account_name: account_name.into(),
container: container.into(),
prefix: String::new(),
sas_token: None,
account_key: None,
access_tier: AccessTier::Hot,
timeout: Duration::from_secs(300),
retry_config: RetryConfig::default(),
credentials: None,
hierarchical_namespace: false,
}
}
#[must_use]
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
#[must_use]
pub fn with_sas_token(mut self, token: impl Into<String>) -> Self {
self.sas_token = Some(token.into());
self
}
#[must_use]
pub fn with_account_key(mut self, key: impl Into<String>) -> Self {
self.account_key = Some(key.into());
self
}
#[must_use]
pub fn with_access_tier(mut self, tier: AccessTier) -> Self {
self.access_tier = tier;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
self.retry_config = config;
self
}
#[must_use]
pub fn with_hierarchical_namespace(mut self, enabled: bool) -> Self {
self.hierarchical_namespace = enabled;
self
}
fn full_blob_name(&self, key: &str) -> String {
if self.prefix.is_empty() {
key.to_string()
} else {
format!("{}/{}", self.prefix, key)
}
}
fn get_blob_endpoint(&self) -> String {
if self.hierarchical_namespace {
format!("https://{}.dfs.core.windows.net", self.account_name)
} else {
format!("https://{}.blob.core.windows.net", self.account_name)
}
}
}
#[cfg(all(feature = "azure-blob", feature = "async"))]
#[async_trait::async_trait]
impl CloudStorageBackend for AzureBlobBackend {
async fn get(&self, key: &str) -> Result<Bytes> {
let mut executor = RetryExecutor::new(self.retry_config.clone());
executor
.execute(|| async {
let blob_name = self.full_blob_name(key);
tracing::debug!(
"Getting blob: {}/{}/{}",
self.account_name,
self.container,
blob_name
);
Err(CloudError::Azure(AzureError::Sdk {
message: "Azure SDK integration pending - requires azure_storage_blobs setup"
.to_string(),
}))
})
.await
}
async fn put(&self, key: &str, data: &[u8]) -> Result<()> {
let mut executor = RetryExecutor::new(self.retry_config.clone());
executor
.execute(|| async {
let blob_name = self.full_blob_name(key);
tracing::debug!(
"Putting blob: {}/{}/{} ({} bytes)",
self.account_name,
self.container,
blob_name,
data.len()
);
Err(CloudError::Azure(AzureError::Sdk {
message: "Azure SDK integration pending - requires azure_storage_blobs setup"
.to_string(),
}))
})
.await
}
async fn delete(&self, key: &str) -> Result<()> {
let mut executor = RetryExecutor::new(self.retry_config.clone());
executor
.execute(|| async {
let blob_name = self.full_blob_name(key);
tracing::debug!(
"Deleting blob: {}/{}/{}",
self.account_name,
self.container,
blob_name
);
Err(CloudError::Azure(AzureError::Sdk {
message: "Azure SDK integration pending - requires azure_storage_blobs setup"
.to_string(),
}))
})
.await
}
async fn exists(&self, key: &str) -> Result<bool> {
let blob_name = self.full_blob_name(key);
tracing::debug!(
"Checking blob exists: {}/{}/{}",
self.account_name,
self.container,
blob_name
);
Err(CloudError::Azure(AzureError::Sdk {
message: "Azure SDK integration pending - requires azure_storage_blobs setup"
.to_string(),
}))
}
async fn list_prefix(&self, prefix: &str) -> Result<Vec<String>> {
let full_prefix = self.full_blob_name(prefix);
tracing::debug!(
"Listing blobs: {}/{} with prefix {}",
self.account_name,
self.container,
full_prefix
);
Err(CloudError::Azure(AzureError::Sdk {
message: "Azure SDK integration pending - requires azure_storage_blobs setup"
.to_string(),
}))
}
fn is_readonly(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_azure_backend_new() {
let backend = AzureBlobBackend::new("myaccount", "mycontainer");
assert_eq!(backend.account_name, "myaccount");
assert_eq!(backend.container, "mycontainer");
assert_eq!(backend.prefix, "");
}
#[test]
fn test_azure_backend_builder() {
let backend = AzureBlobBackend::new("myaccount", "mycontainer")
.with_prefix("data/blobs")
.with_sas_token("?sv=2020-08-04&ss=bfqt")
.with_access_tier(AccessTier::Cool)
.with_hierarchical_namespace(true)
.with_timeout(Duration::from_secs(600));
assert_eq!(backend.prefix, "data/blobs");
assert!(backend.sas_token.is_some());
assert!(matches!(backend.access_tier, AccessTier::Cool));
assert!(backend.hierarchical_namespace);
assert_eq!(backend.timeout, Duration::from_secs(600));
}
#[test]
fn test_azure_backend_full_blob_name() {
let backend = AzureBlobBackend::new("account", "container").with_prefix("prefix");
assert_eq!(backend.full_blob_name("file.txt"), "prefix/file.txt");
let backend_no_prefix = AzureBlobBackend::new("account", "container");
assert_eq!(backend_no_prefix.full_blob_name("file.txt"), "file.txt");
}
#[test]
fn test_azure_backend_blob_endpoint() {
let backend = AzureBlobBackend::new("myaccount", "container");
assert_eq!(
backend.get_blob_endpoint(),
"https://myaccount.blob.core.windows.net"
);
let backend_dfs = backend.with_hierarchical_namespace(true);
assert_eq!(
backend_dfs.get_blob_endpoint(),
"https://myaccount.dfs.core.windows.net"
);
}
}