Skip to content

Commit

Permalink
[Storage] get_blob_properties for BlobClient (#2014)
Browse files Browse the repository at this point in the history
  • Loading branch information
vincenttran-msft authored Feb 26, 2025
1 parent 4e0ba1e commit 319a7c5
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
# AzureSDKOwners: @heaths
# ServiceLabel: %Storage
# PRLabel: %Storage
/sdk/storage/ @heaths @RickWinter @JeffreyRichter @LarryOsterman
/sdk/storage/ @heaths @RickWinter @JeffreyRichter @LarryOsterman @vincenttran-msft @jalauzon-msft

# AzureSDKOwners: @heaths
# ServiceOwner: @Pilchie
Expand Down
6 changes: 6 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions sdk/storage/.dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ copyid
deletetype
incrementalcopy
legalhold
missingcontainer
RAGRS
restype
testcontainer
13 changes: 11 additions & 2 deletions sdk/storage/azure_storage_blob/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-trait.workspace = true
azure_storage_common.workspace = true
azure_core = { workspace = true, features = ["xml"] }
serde = { workspace = true }
time = { workspace = true }
azure_identity = { workspace = true }
serde.workspace = true
time.workspace = true
typespec_client_core = { workspace = true, features = ["derive"] }
uuid.workspace = true
url.workspace = true

[lints]
workspace = true

[dev-dependencies]
tokio = { workspace = true, features = ["macros"] }
azure_identity.workspace = true
azure_core_test.path = "../../core/azure_core_test"
67 changes: 67 additions & 0 deletions sdk/storage/azure_storage_blob/src/clients/blob_client.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

use crate::{
models::BlobProperties, pipeline::StorageHeadersPolicy, BlobBlobClientGetPropertiesOptions,
BlobClientOptions, GeneratedBlobClient,
};
use azure_core::{credentials::TokenCredential, BearerTokenCredentialPolicy, Policy, Result, Url};
use std::sync::Arc;

pub struct BlobClient {
endpoint: Url,
container_name: String,
blob_name: String,
client: GeneratedBlobClient,
}

impl BlobClient {
pub fn new(
endpoint: &str,
container_name: String,
blob_name: String,
credential: 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 oauth_token_policy = BearerTokenCredentialPolicy::new(
credential.clone(),
["https://storage.azure.com/.default"],
);
options
.client_options
.per_try_policies
.push(Arc::new(oauth_token_policy) as Arc<dyn Policy>);

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

pub fn endpoint(&self) -> &Url {
&self.endpoint
}

pub async fn get_blob_properties(
&self,
options: Option<BlobBlobClientGetPropertiesOptions<'_>>,
) -> Result<BlobProperties> {
let response = self
.client
.get_blob_blob_client(self.container_name.clone(), self.blob_name.clone())
.get_properties(options)
.await?;

let blob_properties: BlobProperties = response.headers().get()?;
Ok(blob_properties)
}
}
2 changes: 2 additions & 0 deletions sdk/storage/azure_storage_blob/src/clients/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
mod blob_client;
mod blob_container_client;
mod blob_service_client;

pub use blob_client::BlobClient;
16 changes: 10 additions & 6 deletions sdk/storage/azure_storage_blob/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
// Code generated by Microsoft (R) Rust Code Generator. DO NOT EDIT.

mod clients;
mod generated;

pub use crate::generated::clients::*;
pub(crate) mod pipeline;

pub use clients::*;

pub mod models {
pub use crate::generated::enums::*;
pub use crate::generated::models::*;
}
pub mod models;

pub use crate::generated::clients::*;
pub(crate) use blob_client::BlobClient as GeneratedBlobClient;

pub use blob_client::BlobClient;
pub use crate::generated::clients::blob_blob_client::BlobBlobClientGetPropertiesOptions;
pub use crate::generated::clients::blob_client::BlobClientOptions;
104 changes: 104 additions & 0 deletions sdk/storage/azure_storage_blob/src/models/blob_properties.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

use azure_core::{
headers::{
FromHeaders, HeaderName, Headers, BLOB_ACCESS_TIER, BLOB_TYPE, CREATION_TIME, LEASE_STATE,
LEASE_STATUS, SERVER_ENCRYPTED,
},
Error, Etag, LeaseStatus,
};
use typespec_client_core::fmt::SafeDebug;

use crate::models::{AccessTier, BlobType, LeaseState};

pub const CONTENT_LENGTH: HeaderName = HeaderName::from_static("content-length");
pub const CONTENT_MD5: HeaderName = HeaderName::from_static("content-md5");
pub const CONTENT_TYPE: HeaderName = HeaderName::from_static("content-type");
pub const ETAG: HeaderName = HeaderName::from_static("etag");
pub const LAST_MODIFIED: HeaderName = HeaderName::from_static("last-modified");
pub const BLOB_ACCESS_TIER_INFERRED: HeaderName =
HeaderName::from_static("x-ms-access-tier-inferred");

/// Properties of an Azure Storage blob.
///
#[derive(Clone, Default, SafeDebug)]
pub struct BlobProperties {
pub access_tier_inferred: Option<bool>,
pub access_tier: Option<AccessTier>,
pub blob_type: Option<BlobType>,
pub content_length: Option<i64>,
pub content_md5: Option<String>,
pub content_type: Option<String>,
pub creation_time: Option<String>,
pub etag: Option<Etag>,
pub last_modified: Option<String>,
pub lease_state: Option<LeaseState>,
pub lease_status: Option<LeaseStatus>,
pub server_encrypted: Option<bool>,
}

impl FromHeaders for BlobProperties {
type Error = Error;
fn header_names() -> &'static [&'static str] {
&[
"content-length",
"content-md5",
"content-type",
"etag",
"last-modified",
"x-ms-access-tier-inferred",
"x-ms-access-tier",
"x-ms-blob-type",
"x-ms-creation-time",
"x-ms-lease-state",
"x-ms-lease-status",
"x-ms-server-encrypted",
]
}

fn from_headers(headers: &Headers) -> Result<Option<Self>, Error> {
let mut properties = BlobProperties {
..Default::default()
};

let access_tier_inferred: Option<bool> =
headers.get_optional_as(&BLOB_ACCESS_TIER_INFERRED)?;
properties.access_tier_inferred = access_tier_inferred;

let access_tier: Option<AccessTier> = headers.get_optional_as(&BLOB_ACCESS_TIER)?;
properties.access_tier = access_tier;

let blob_type: Option<BlobType> = headers.get_optional_as(&BLOB_TYPE)?;
properties.blob_type = blob_type;

let content_length: Option<i64> = headers.get_optional_as(&CONTENT_LENGTH)?;
properties.content_length = content_length;

let content_md5 = headers.get_optional_str(&CONTENT_MD5);
properties.content_md5 = content_md5.map(|s| s.to_string());

let content_type = headers.get_optional_str(&CONTENT_TYPE);
properties.content_type = content_type.map(|s| s.to_string());

let creation_time = headers.get_optional_str(&CREATION_TIME);
properties.creation_time = creation_time.map(|s| s.to_string());

let etag: Option<Etag> = headers.get_optional_as(&ETAG)?;
properties.etag = etag;

let last_modified = headers.get_optional_str(&LAST_MODIFIED);
properties.last_modified = last_modified.map(|s| s.to_string());

let lease_state: Option<LeaseState> = headers.get_optional_as(&LEASE_STATE)?;
properties.lease_state = lease_state;

let lease_status: Option<LeaseStatus> = headers.get_optional_as(&LEASE_STATUS)?;
properties.lease_status = lease_status;

let server_encrypted: Option<bool> = headers.get_optional_as(&SERVER_ENCRYPTED)?;
properties.server_encrypted = server_encrypted;

Ok(Some(properties))
}
}
9 changes: 9 additions & 0 deletions sdk/storage/azure_storage_blob/src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

mod blob_properties;

pub use crate::generated::enums::*;
pub use crate::generated::models::*;

pub use blob_properties::BlobProperties;
6 changes: 6 additions & 0 deletions sdk/storage/azure_storage_blob/src/pipeline/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

mod storage_headers_policy;

pub use storage_headers_policy::StorageHeadersPolicy;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

use async_trait::async_trait;
use azure_core::{headers::CLIENT_REQUEST_ID, Context, Policy, PolicyResult, Request};
use std::sync::Arc;
use uuid::Uuid;

#[derive(Debug, Clone)]
pub struct StorageHeadersPolicy;

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl Policy for StorageHeadersPolicy {
async fn send(
&self,
ctx: &Context,
request: &mut Request,
next: &[Arc<dyn Policy>],
) -> PolicyResult {
if request
.headers()
.get_optional_string(&CLIENT_REQUEST_ID)
.is_none()
{
let request_id = Uuid::new_v4().to_string();
request.insert_header(CLIENT_REQUEST_ID, &request_id);
}
next[0].send(ctx, request, &next[1..]).await
}
}
60 changes: 60 additions & 0 deletions sdk/storage/azure_storage_blob/tests/blob_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

use azure_core_test::recorded;
use azure_identity::DefaultAzureCredential;
use azure_storage_blob::{BlobBlobClientGetPropertiesOptions, BlobClient};
use std::{env, error::Error};

#[recorded::test(live)]
async fn test_get_blob_properties() -> Result<(), Box<dyn Error>> {
// Setup
let storage_account_name = env::var("AZURE_STORAGE_ACCOUNT_NAME")
.expect("Failed to get environment variable: AZURE_STORAGE_ACCOUNT_NAME");
let endpoint = format!("https://{}.blob.core.windows.net/", storage_account_name);
let credential = DefaultAzureCredential::new()?;

// Act
let blob_client = BlobClient::new(
&endpoint,
String::from("testcontainer"),
String::from("test_blob.txt"),
credential,
None,
)?;
blob_client
.get_blob_properties(Some(BlobBlobClientGetPropertiesOptions::default()))
.await?;

// Assert
Ok(())
}

#[recorded::test(live)]
async fn test_get_blob_properties_invalid_container() -> Result<(), Box<dyn Error>> {
// Setup
let storage_account_name = env::var("AZURE_STORAGE_ACCOUNT_NAME")
.expect("Failed to get environment variable: AZURE_STORAGE_ACCOUNT_NAME");
let endpoint = format!("https://{}.blob.core.windows.net/", storage_account_name);
let credential = DefaultAzureCredential::new()?;

// Act
let blob_client = BlobClient::new(
&endpoint,
String::from("missingcontainer"),
String::from("test_blob.txt"),
credential,
None,
)?;
let response = blob_client
.get_blob_properties(Some(BlobBlobClientGetPropertiesOptions::default()))
.await;

// Assert
assert_eq!(
String::from("HttpResponse(NotFound, \"ContainerNotFound\")"),
response.unwrap_err().kind().to_string()
);

Ok(())
}

0 comments on commit 319a7c5

Please sign in to comment.