Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions sdk/storage/.dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ immutabilitypolicy
incrementalcopy
legalhold
missingcontainer
numofmessages
pagelist
peekonly
policyid
prevsnapshot
RAGRS
Expand All @@ -25,7 +27,6 @@ testblob3
testblob4
testcontainer
uncommittedblobs
urlencoding
westus
yourtagname
numofmessages
peekonly
1 change: 1 addition & 0 deletions sdk/storage/azure_storage_blob/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ serde.workspace = true
serde_json.workspace = true
typespec_client_core = { workspace = true, features = ["derive"] }
url.workspace = true
urlencoding = "2.1.3"
uuid.workspace = true

[lints]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
directory: specification/storage/Microsoft.BlobStorage
commit: 309e0ffb479980eea4b44f294ea4c50549848867
repo: Azure/azure-rest-api-specs
additionalDirectories:
36 changes: 24 additions & 12 deletions sdk/storage/azure_storage_blob/src/clients/append_blob_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
AppendBlobClientCreateOptions, AppendBlobClientCreateResult, AppendBlobClientSealOptions,
AppendBlobClientSealResult,
},
parsers::parse_url_name_components,
pipeline::StorageHeadersPolicy,
AppendBlobClientOptions, BlobClientOptions,
};
Expand All @@ -24,8 +25,10 @@ use std::sync::Arc;

/// A client to interact with a specific Azure storage Append blob, although that blob may not yet exist.
pub struct AppendBlobClient {
pub(crate) endpoint: Url,
pub(crate) client: GeneratedAppendBlobClient,
pub(super) endpoint: Url,
pub(super) client: GeneratedAppendBlobClient,
pub(super) container_name: String,
pub(super) blob_name: String,
}

impl AppendBlobClient {
Expand Down Expand Up @@ -53,16 +56,25 @@ impl AppendBlobClient {
.per_call_policies
.push(storage_headers_policy);

let client = GeneratedAppendBlobClient::new(
endpoint,
credential,
container_name,
blob_name,
Some(options),
)?;
let mut url = Url::parse(endpoint)?;
if !url.scheme().starts_with("http") {
return Err(azure_core::Error::with_message(
azure_core::error::ErrorKind::Other,
format!("{url} must use http(s)"),
));
}

// Build Blob URL, Url crate handles encoding only path params
url.path_segments_mut()
.expect("Cannot be base")
.extend([&container_name, &blob_name]);

let client = GeneratedAppendBlobClient::new(url.as_str(), credential, Some(options))?;
Ok(Self {
endpoint: endpoint.parse()?,
endpoint: client.endpoint().clone(),
client,
container_name,
blob_name,
})
}

Expand All @@ -73,12 +85,12 @@ impl AppendBlobClient {

/// Gets the container name of the Storage account this client is connected to.
pub fn container_name(&self) -> &str {
&self.client.container_name
&self.container_name
}

/// Gets the blob name of the Storage account this client is connected to.
pub fn blob_name(&self) -> &str {
&self.client.blob_name
&self.blob_name
}

/// Creates a new Append blob.
Expand Down
141 changes: 111 additions & 30 deletions sdk/storage/azure_storage_blob/src/clients/blob_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// Licensed under the MIT License.

use crate::{
generated::clients::AppendBlobClient as GeneratedAppendBlobClient,
generated::clients::BlobClient as GeneratedBlobClient,
generated::clients::BlockBlobClient as GeneratedBlockBlobClient,
generated::clients::PageBlobClient as GeneratedPageBlobClient,
generated::models::{
BlobClientAcquireLeaseResult, BlobClientBreakLeaseResult, BlobClientChangeLeaseResult,
BlobClientDownloadResult, BlobClientGetAccountInfoResult, BlobClientGetPropertiesResult,
Expand All @@ -19,6 +22,7 @@ use crate::{
BlobTags, BlockBlobClientCommitBlockListOptions, BlockBlobClientUploadOptions, BlockList,
BlockListType, BlockLookupList, StorageErrorCode,
},
parsers::{build_blob_url, parse_url_name_components},
pipeline::StorageHeadersPolicy,
AppendBlobClient, BlobClientOptions, BlockBlobClient, PageBlobClient,
};
Expand All @@ -27,54 +31,109 @@ use azure_core::{
error::ErrorKind,
http::{
policies::{BearerTokenCredentialPolicy, Policy},
AsyncResponse, JsonFormat, NoFormat, RequestContent, Response, StatusCode, Url, XmlFormat,
AsyncResponse, JsonFormat, NoFormat, Pipeline, RequestContent, Response, StatusCode, Url,
XmlFormat,
},
Bytes, Result,
tracing, Bytes, Result,
};
use std::collections::HashMap;
use std::sync::Arc;

/// A client to interact with a specific Azure storage blob, although that blob may not yet exist.
/// A client to interact with a specific Azure Storage blob, although that blob may not yet exist.
pub struct BlobClient {
pub(super) endpoint: Url,
pub(super) client: GeneratedBlobClient,
pub(super) container_name: String,
pub(super) blob_name: String,
}

impl GeneratedBlobClient {
#[tracing::new("Storage.Blob.Blob")]
pub fn from_url(
blob_url: &str,
credential: Option<Arc<dyn TokenCredential>>,
options: Option<BlobClientOptions>,
) -> Result<Self> {
let mut options = options.unwrap_or_default();

let storage_headers_policy = Arc::new(StorageHeadersPolicy);
options
.client_options
.per_call_policies
.push(storage_headers_policy);

let per_retry_policies = if let Some(token_credential) = credential {
if !blob_url.starts_with("https://") {
return Err(azure_core::Error::with_message(
azure_core::error::ErrorKind::Other,
format!("{blob_url} must use http(s)"),
));
}
let auth_policy: Arc<dyn Policy> = Arc::new(BearerTokenCredentialPolicy::new(
token_credential,
vec!["https://storage.azure.com/.default"],
));
vec![auth_policy]
} else {
Vec::default()
};

let pipeline = Pipeline::new(
option_env!("CARGO_PKG_NAME"),
option_env!("CARGO_PKG_VERSION"),
options.client_options.clone(),
Vec::default(),
per_retry_policies,
None,
);

Ok(Self {
// This is the crux of the issue. We have now resolved the encoding to align with other Storage SDK offerings
// However because the generated code has to build a Request model that expects a Url type, we will always get our '/' encoded as '%2F'
endpoint: Url::parse(blob_url)?,
version: options.version,
pipeline,
})
}
}
impl BlobClient {
/// Creates a new BlobClient, using Entra ID authentication.
/// Creates a new BlobClient.
///
/// # Arguments
///
/// * `endpoint` - The full URL of the Azure storage account, for example `https://myaccount.blob.core.windows.net/`
/// * `container_name` - The name of the container containing this blob.
/// * `blob_name` - The name of the blob to interact with.
/// * `credential` - An implementation of [`TokenCredential`] that can provide an Entra ID token to use when authenticating.
/// * `credential` - An optional implementation of [`TokenCredential`] that can provide an Entra ID token to use when authenticating.
/// * `options` - Optional configuration for the client.
pub fn new(
endpoint: &str,
container_name: String,
blob_name: String,
credential: Arc<dyn TokenCredential>,
credential: Option<Arc<dyn TokenCredential>>,
options: Option<BlobClientOptions>,
) -> Result<Self> {
let mut options = options.unwrap_or_default();
let blob_url = build_blob_url(endpoint, &container_name, &blob_name);

let storage_headers_policy = Arc::new(StorageHeadersPolicy);
options
.client_options
.per_call_policies
.push(storage_headers_policy);

let client = GeneratedBlobClient::new(
endpoint,
credential,
let client = GeneratedBlobClient::from_url(&blob_url, credential, options)?;
Ok(Self {
client,
container_name,
blob_name,
Some(options),
)?;
})
}

pub fn from_blob_url(
blob_url: &str,
credential: Option<Arc<dyn TokenCredential>>,
options: Option<BlobClientOptions>,
) -> Result<Self> {
let (container_name, blob_name) = parse_url_name_components(blob_url)?;
let client = GeneratedBlobClient::from_url(blob_url, credential, options)?;

Ok(Self {
endpoint: endpoint.parse()?,
client,
container_name,
blob_name,
})
}

Expand All @@ -85,7 +144,14 @@ impl BlobClient {
pub fn append_blob_client(&self) -> AppendBlobClient {
AppendBlobClient {
endpoint: self.client.endpoint.clone(),
client: self.client.get_append_blob_client(),
client: GeneratedAppendBlobClient {
endpoint: self.client.endpoint.clone(),
pipeline: self.client.pipeline.clone(),
version: self.client.version.clone(),
tracer: self.client.tracer.clone(),
},
container_name: self.container_name().to_string(),
blob_name: self.blob_name().to_string(),
}
}

Expand All @@ -96,7 +162,14 @@ impl BlobClient {
pub fn block_blob_client(&self) -> BlockBlobClient {
BlockBlobClient {
endpoint: self.client.endpoint.clone(),
client: self.client.get_block_blob_client(),
client: GeneratedBlockBlobClient {
endpoint: self.client.endpoint.clone(),
pipeline: self.client.pipeline.clone(),
version: self.client.version.clone(),
tracer: self.client.tracer.clone(),
},
container_name: self.container_name().to_string(),
blob_name: self.blob_name().to_string(),
}
}

Expand All @@ -107,23 +180,31 @@ impl BlobClient {
pub fn page_blob_client(&self) -> PageBlobClient {
PageBlobClient {
endpoint: self.client.endpoint.clone(),
client: self.client.get_page_blob_client(),
client: GeneratedPageBlobClient {
endpoint: self.client.endpoint.clone(),
pipeline: self.client.pipeline.clone(),
version: self.client.version.clone(),
tracer: self.client.tracer.clone(),
},
container_name: self.container_name().to_string(),
blob_name: self.blob_name().to_string(),
}
}

/// Gets the endpoint of the Storage account this client is connected to.
pub fn endpoint(&self) -> &Url {
&self.endpoint
//TODO: This should be a &str, therefore generated code needs to be changed here to hold a &str on the struct and not a Url type
/// Gets the full URL of the Storage blob this client is connected to.
pub fn blob_url(&self) -> &Url {
&self.client.endpoint
}

/// Gets the container name of the Storage account this client is connected to.
pub fn container_name(&self) -> &str {
&self.client.container_name
&self.container_name
}

/// Gets the blob name of the Storage account this client is connected to.
pub fn blob_name(&self) -> &str {
&self.client.blob_name
&self.blob_name
}

/// Returns all user-defined metadata, standard HTTP properties, and system properties for the blob.
Expand Down Expand Up @@ -183,8 +264,8 @@ impl BlobClient {
options.if_none_match = Some(String::from("*"));
}

self.client
.get_block_blob_client()
self.block_blob_client()
.client
.upload(data, content_length, Some(options))
.await
}
Expand Down
Loading
Loading