Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Storage] get_blob_properties for BlobClient #2014

Merged
merged 25 commits into from
Feb 26, 2025
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e8b4a53
Initial get_blob_properties POC
vincenttran-msft Jan 22, 2025
9980d27
Refactor off Storage feedback
vincenttran-msft Jan 23, 2025
cff7ca0
Update dict
vincenttran-msft Jan 23, 2025
09e37ba
Storage feedback round2
vincenttran-msft Jan 24, 2025
c95b015
Refactor models import
vincenttran-msft Jan 24, 2025
4fc3e43
fmt
vincenttran-msft Jan 25, 2025
31d42cd
Refactor, cleanup lib.rs
vincenttran-msft Jan 25, 2025
6c26536
Put back models mod in lib.rs
vincenttran-msft Jan 25, 2025
ff2002a
Solving CI issueS
vincenttran-msft Jan 25, 2025
147e390
Feedback roudn
vincenttran-msft Jan 28, 2025
1c0d630
fmt
vincenttran-msft Jan 28, 2025
ef3281b
PR feedback
vincenttran-msft Jan 28, 2025
0b5846c
refactor src/lib.rs
vincenttran-msft Jan 29, 2025
5cddd2c
revert
vincenttran-msft Jan 29, 2025
a9fcde0
Comment out test to be fixed with intro of new APIs
vincenttran-msft Jan 31, 2025
7848d5e
PR feedback
vincenttran-msft Feb 11, 2025
882a44b
Slight refactor
vincenttran-msft Feb 11, 2025
47801f7
Adjust codeowners
vincenttran-msft Feb 12, 2025
6c7c975
refactor
vincenttran-msft Feb 14, 2025
1b3bc9e
Add exhaustive list of headers to header_names()
vincenttran-msft Feb 21, 2025
4e8f4a2
Change azure_core_test dev-dependencies import
vincenttran-msft Feb 21, 2025
0657a68
Merge branch 'main' into vincenttran/get_blob_props_mvp
vincenttran-msft Feb 25, 2025
95c6a70
Revert lockfile, only changes from build against working dir
vincenttran-msft Feb 25, 2025
0195c7a
nit
vincenttran-msft Feb 25, 2025
c5634d0
Address PR feedback
vincenttran-msft Feb 26, 2025
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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -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
10 changes: 8 additions & 2 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
@@ -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
@@ -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
@@ -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
@@ -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(())
}
Loading