diff --git a/sdk/storage/azure_storage_blob/Cargo.toml b/sdk/storage/azure_storage_blob/Cargo.toml index ecbcea4d73..83e952baee 100644 --- a/sdk/storage/azure_storage_blob/Cargo.toml +++ b/sdk/storage/azure_storage_blob/Cargo.toml @@ -11,6 +11,20 @@ rust-version.workspace = true [dependencies] azure_storage_common.workspace = true +async-trait.workspace = true +async-std = { workspace = true } +azure_core = { workspace = true } +azure_identity = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +time = { workspace = true } +typespec_client_core = { workspace = true, features = ["reqwest"] } +typespec_derive = { workspace = true } +blob_storage = { path = "blob_storage" } +uuid = { workspace = true } + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [lints] workspace = true diff --git a/sdk/storage/azure_storage_blob/blob_storage/Cargo.toml b/sdk/storage/azure_storage_blob/blob_storage/Cargo.toml index bdf0ce20b8..cb727bbf1f 100644 --- a/sdk/storage/azure_storage_blob/blob_storage/Cargo.toml +++ b/sdk/storage/azure_storage_blob/blob_storage/Cargo.toml @@ -9,8 +9,15 @@ rust-version.workspace = true [dependencies] async-std = { workspace = true } +async-trait.workspace = true azure_core = { workspace = true } +azure_identity = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } time = { workspace = true } -typespec_client_core = { workspace = true } +typespec_client_core = { workspace = true, features = ["reqwest"] } +typespec_derive = { workspace = true } +uuid = { workspace = true } + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/clients/blob_client.rs b/sdk/storage/azure_storage_blob/blob_storage/src/clients/blob_client.rs new file mode 100644 index 0000000000..dbdbf55927 --- /dev/null +++ b/sdk/storage/azure_storage_blob/blob_storage/src/clients/blob_client.rs @@ -0,0 +1,557 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use crate::blob_blob::{BlobBlobDownloadOptions, BlobBlobGetPropertiesOptions}; +use crate::blob_block_blob::{ + BlobBlockBlobCommitBlockListOptions, BlobBlockBlobStageBlockOptions, BlobBlockBlobUploadOptions, +}; +use crate::blob_client::BlobClientOptions; +use crate::clients::units::*; +use crate::models::BlockLookupList; +use crate::policies::storage_headers_policy::StorageHeadersPolicy; +use crate::BlobClient as GeneratedBlobClient; +use azure_core::credentials::TokenCredential; +use azure_core::headers::HeaderName; +use azure_core::{ + AsClientOptions, BearerTokenCredentialPolicy, Context, Error, Method, Policy, Request, + RequestContent, Response, Result, Url, +}; +use azure_identity::DefaultAzureCredentialBuilder; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::Arc; +use uuid::Uuid; + +pub struct BlobClient { + pub(crate) blob_type: PhantomData, + pub(crate) endpoint: String, + pub(crate) container_name: String, + pub(crate) blob_name: String, + pub(crate) credential: Option>, + pub(crate) client: GeneratedBlobClient, +} + +impl BlobClient { + const VERSION_ID: &'static str = ("2024-08-04"); + + pub fn new( + endpoint: String, + container_name: String, + blob_name: String, + credential: Option>, + mut options: Option, + ) -> Result { + let mut options = BlobClientOptions::default(); + + // Fold in StorageHeadersPolicy policy via ClientOptions + let mut per_call_policies = options.client_options.per_call_policies().clone(); + let storage_headers_policy = Arc::new(StorageHeadersPolicy::new()); + per_call_policies.push(storage_headers_policy); + options + .client_options + .set_per_call_policies(per_call_policies); + + // Conditionally add authentication if provided + if credential.is_some() { + let oauth_token_policy = BearerTokenCredentialPolicy::new( + credential.clone().unwrap(), + ["https://storage.azure.com/.default"], + ); + let mut per_try_policies = options.client_options.per_call_policies().clone(); + per_try_policies.push(Arc::new(oauth_token_policy) as Arc); + options + .client_options + .set_per_try_policies(per_try_policies); + } + + let client = + GeneratedBlobClient::with_no_credential(endpoint.clone(), Some(options.clone()))?; + + Ok(Self { + blob_type: PhantomData::, + endpoint: endpoint.clone(), + container_name: container_name.clone(), + blob_name: blob_name.clone(), + credential, + client: client, + }) + } + + pub async fn as_append_blob(&self) -> BlobClient { + BlobClient { + blob_type: PhantomData::, + endpoint: self.endpoint.clone(), + container_name: self.container_name.clone(), + blob_name: self.blob_name.clone(), + credential: self.credential.clone(), + client: GeneratedBlobClient { + endpoint: self.client.endpoint.clone(), + pipeline: self.client.pipeline.clone(), + }, + } + } + + pub async fn as_block_blob(&self) -> BlobClient { + BlobClient { + blob_type: PhantomData::, + endpoint: self.endpoint.clone(), + container_name: self.container_name.clone(), + blob_name: self.blob_name.clone(), + credential: self.credential.clone(), + client: GeneratedBlobClient { + endpoint: self.client.endpoint.clone(), + pipeline: self.client.pipeline.clone(), + }, + } + } + + pub async fn download_blob( + &self, + offset: Option, + length: Option, + mut options: Option>, + ) -> Result>> { + // This hard-coded value still works, even though this is technically a bug for version_id + let version = String::from("80bc3c5e-3bb7-95f6-6c57-8ceb2c9155"); + + // If length and offset is provided, calculate and build range String + if length.is_some() { + // If length is provided, offset must be provided + if offset.is_none() { + // Traditionally in other SDKs, we would do input validation that if length provided, offset is provided. Raise Err? + }; + + let range_str = format!( + "bytes={}-{}", + offset.unwrap(), + offset.unwrap() + length.unwrap() - 1 // Service uses inclusive end index + ); + options.as_mut().unwrap().range = Some(range_str) + } + + self.client + .get_blob_blob_client() + .download( + self.container_name.clone(), + self.blob_name.clone(), + version, //blob version + String::from(Self::VERSION_ID), //svc version + options, + ) + .await + } + + fn url_encode(input: &str) -> String { + // URL-encode the input string + input + .chars() + .map(|c| match c { + ' ' => "%20".to_string(), + '&' => "\u{0026}".to_string(), + _ => c.to_string(), + }) + .collect() + } + + fn hashmap_to_query_string(params: HashMap) -> String { + // Convert the customer-provided HashMap to a valid query String + // Fun Fact: HashMaps insertions are NOT ordered. None of the built-in collections track order of insertion. + let mut query_string = String::new(); + for (key, value) in ¶ms { + query_string.push_str(&format!("{}={}", key, value)); + if !query_string.is_empty() { + query_string.push('&'); + } + } + + // Remove extra & sign + let mut chars = query_string.chars(); + chars.next_back(); + query_string = String::from(chars.as_str()); + + // URL-encode the query String + Self::url_encode(&query_string) + } + + pub async fn upload_blob( + &self, + data: RequestContent>, + overwrite: Option, + tags: Option>, + mut options: Option>, + ) -> Result> { + //For now, this will only be Block Blob hot-path + + // Check if they want overwrite, by default overwrite=False + if overwrite.is_none() || overwrite.unwrap() == false { + options.as_mut().unwrap().if_none_match = Some(String::from("*")); + } + + // Parse tags if any, convert to server-acceptable format + match tags { + Some(tag) => { + let parsed_tags = Self::hashmap_to_query_string(tag); + (options.as_mut().unwrap()).blob_tags_string = Some(parsed_tags) + } + None => {} + } + + self.client + .get_blob_block_blob_client() + .upload( + self.container_name.clone(), + self.blob_name.clone(), + data, + String::from(Self::VERSION_ID), + options, + ) + .await + } + + pub async fn get_blob_properties( + &self, + options: Option>, + ) -> Result> { + // This hard-coded value still works, even though this is technically a bug for version_id + let version = String::from("80bc3c5e-3bb7-95f6-6c57-8ceb2c9155"); + self.client + .get_blob_blob_client() + .get_properties( + self.container_name.clone(), + self.blob_name.clone(), + version, //blob version + String::from(Self::VERSION_ID), //svc version + options, + ) + .await + } +} + +impl BlobClient { + pub async fn append_block(&self) { + todo!() + } +} + +impl BlobClient { + // TODO: Still not working, likely something wrong with path building and possibly encoding + pub async fn stage_block( + &self, + block_id: String, + data: Vec, + options: Option>, + ) -> Result> { + let content_length = i64::try_from(data.len()).unwrap(); + self.client + .get_blob_block_blob_client() + .stage_block( + RequestContent::from(data), + self.container_name.clone(), + self.blob_name.clone(), + block_id, + content_length, + String::from(BlobClient::VERSION_ID), + options, + ) + .await + } + + // TODO: Still not working, likely something wrong with path building and possibly encoding + pub async fn commit_block_list( + &self, + blocks: BlockLookupList, + options: Option>, + ) -> Result> { + self.client + .get_blob_block_blob_client() + .commit_block_list( + RequestContent::try_from(blocks)?, + self.container_name.clone(), + self.blob_name.clone(), + String::from(BlobClient::VERSION_ID), + options, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use azure_core::Model; + + use super::*; + + #[tokio::test] + async fn test_download_blob() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let blob_client = BlobClient::new( + String::from("https://vincenttranpublicac.blob.core.windows.net/"), + String::from("public"), + String::from("hello.txt"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + let response = blob_client + .download_blob(None, None, Some(BlobBlobDownloadOptions::default())) + .await + .unwrap(); + print!("{:?}", response); + print!( + "\n{:?}", + response.into_body().collect_string().await.unwrap() + ); + } + + #[tokio::test] + async fn test_download_blob_ranged() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let blob_client = BlobClient::new( + String::from("https://vincenttranpublicac.blob.core.windows.net/"), + String::from("public"), + String::from("hello.txt"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + let response = blob_client + .download_blob(Some(0), Some(4), Some(BlobBlobDownloadOptions::default())) + .await + .unwrap(); + assert_eq!(response.into_body().collect_string().await.unwrap(), "rust") + } + + #[tokio::test] + // Need az login + async fn test_upload_blob() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let blob_client = BlobClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + String::from("acontainer108f32e8"), + String::from("goodbye.txt"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + + let data = b"hello world".to_vec(); + let rq = RequestContent::from(data); + let response = blob_client + .upload_blob(rq, None, None, Some(BlobBlockBlobUploadOptions::default())) + .await; + + match response { + Ok(response) => {} + Err(error) => { + assert_eq!(error.http_status(), Some(azure_core::StatusCode::Conflict)) + } + } + } + + #[tokio::test] + // Need az login + async fn test_upload_blob_overwrite_true() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let blob_client = BlobClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + String::from("acontainer108f32e8"), + String::from("goodbye.txt"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + + let data = b"hello world".to_vec(); + let rq = RequestContent::from(data); + let response = blob_client + .upload_blob( + rq, + Some(true), + None, + Some(BlobBlockBlobUploadOptions::default()), + ) + .await + .unwrap(); + } + + #[tokio::test] + // Need az login + async fn test_upload_blob_overwrite_true_with_tags() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let blob_client = BlobClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + String::from("acontainer108f32e8"), + String::from("i-got-tags.txt"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + + // Create tags + let mut tags_hashmap = HashMap::new(); + tags_hashmap.insert("tag1 name".to_string(), "my tag".to_string()); + tags_hashmap.insert("tag2".to_string(), "secondtag".to_string()); + tags_hashmap.insert("tag3".to_string(), "thirdtag".to_string()); + + let data = b"hello world".to_vec(); + let rq = RequestContent::from(data); + let response = blob_client + .upload_blob( + rq, + Some(true), + Some(tags_hashmap), + Some(BlobBlockBlobUploadOptions::default()), + ) + .await + .unwrap(); + print!("{:?}", response); + print!( + "\n{:?}", + response.into_body().collect_string().await.unwrap() + ); + } + + #[tokio::test] + // Need az login + // This unexpectedly is putting ?comp=block into the blob name, also seems to committing as well + async fn test_put_block_list() { + let credential: Arc = + DefaultAzureCredentialBuilder::default().build().unwrap(); + let blob_client = BlobClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + String::from("acontainer108f32e8"), + String::from("puttheblock"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + + // Stage Blocks + let data1 = b"AAA".to_vec(); + let data2 = b"BBB".to_vec(); + let data3 = b"CCC".to_vec(); + let response = blob_client + .stage_block( + String::from("1"), + data1, + Some(BlobBlockBlobStageBlockOptions::default()), + ) + .await; + + print!("{:?}", response); + + // blob_client.stage_block( + // String::from("2"), + // data2, + // Some(BlobBlockBlobStageBlockOptions::default()), + // ); + // blob_client.stage_block( + // String::from("3"), + // data3, + // Some(BlobBlockBlobStageBlockOptions::default()), + // ); + + // // Commit Block List + // let block_id_vec = vec![String::from("1"), String::from("2"), String::from("3")]; + // let block_lookup_list = BlockLookupList { + // committed: None, + // latest: Some(block_id_vec), + // uncommitted: None, + // }; + + // let response = blob_client + // .commit_block_list( + // block_lookup_list, + // Some(BlobBlockBlobCommitBlockListOptions::default()), + // ) + // .await; + // print!("{:?}", response); + } + + #[tokio::test] + async fn test_download_blob_if_tags_match() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let blob_client = BlobClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + String::from("options-bag-testing"), + String::from("i_have_tags.txt"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + + // Build an BlobBlobDownloadOptions that contains if_tags_match with the matching condition to {tagged: yes} + // These are expected as: ""='>, + client: GeneratedBlobClient, +} + +impl ContainerClient { + const VERSION_ID: &'static str = ("2024-08-04"); + + pub fn new( + endpoint: String, + container_name: String, + credential: Option>, + mut options: Option, + ) -> Result { + let mut options = BlobClientOptions::default(); + + // Fold in StorageHeadersPolicy policy via ClientOptions + let mut per_call_policies = options.client_options.per_call_policies().clone(); + let storage_headers_policy = Arc::new(StorageHeadersPolicy::new()); + per_call_policies.push(storage_headers_policy); + options + .client_options + .set_per_call_policies(per_call_policies); + + // Conditionally add authentication if provided + if credential.is_some() { + let oauth_token_policy = BearerTokenCredentialPolicy::new( + credential.clone().unwrap(), + ["https://storage.azure.com/.default"], + ); + let mut per_try_policies = options.client_options.per_call_policies().clone(); + per_try_policies.push(Arc::new(oauth_token_policy) as Arc); + options + .client_options + .set_per_try_policies(per_try_policies); + } + + let client = + GeneratedBlobClient::with_no_credential(endpoint.clone(), Some(options.clone()))?; + + Ok(Self { + endpoint: endpoint.clone(), + container_name: container_name.clone(), + credential, + client: client, + }) + } + + pub async fn create_container( + &self, + options: Option>, + ) -> Result> { + self.client + .get_blob_container_client() + .create( + self.container_name.clone(), + String::from(Self::VERSION_ID), //svc version + options, + ) + .await + } + + pub async fn get_container_properties( + &self, + options: Option>, + ) -> Result> { + self.client + .get_blob_container_client() + .get_properties( + self.container_name.clone(), + String::from(Self::VERSION_ID), //svc version + options, + ) + .await + } + + pub async fn get_account_info( + &self, + options: Option>, + ) -> Result> { + self.client + .get_blob_container_client() + .get_account_info( + self.container_name.clone(), + String::from(Self::VERSION_ID), //svc version + options, + ) + .await + } + + pub async fn list_blobs( + &self, + options: Option>, + ) -> Result> { + self.client + .get_blob_container_client() + .list_blob_flat_segment( + self.container_name.clone(), + String::from(Self::VERSION_ID), //svc version + options, + ) + .await + } +} +#[cfg(test)] +mod tests { + use super::*; + #[tokio::test] + // Don't forget to az-login + async fn test_get_container_properties_auth() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let container_client = ContainerClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + String::from("acontainer108f32e8"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + let response = container_client + .get_container_properties(Some(BlobContainerGetPropertiesOptions::default())) + .await + .unwrap(); + print!("{:?}", response); + print!( + "\n{:?}", + response.into_body().collect_string().await.unwrap() + ); + } + + #[tokio::test] + // Don't forget to az-login + // TODO: Look into the return type, how do we make it more comprehensible + async fn test_list_blobs_auth() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let container_client = ContainerClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + String::from("acontainer108f32e8"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + let response = container_client + .list_blobs(Some(BlobContainerListBlobFlatSegmentOptions::default())) + .await + .unwrap(); + print!("{:?}", response); + print!( + "\n{:?}", + response.into_body().collect_string().await.unwrap() + ); + } + + #[tokio::test] + // Don't forget to az-login + async fn test_create_container() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let container_client = ContainerClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + String::from("mynewcontainere"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + let response = container_client + .create_container(Some(BlobContainerCreateOptions::default())) + .await; + println!("{:?}", response); + + match response { + Ok(x) => { + println!("Created: {:?}", x); + } + Err(e) => { + assert_eq!(e.http_status(), Some(StatusCode::Conflict)) + } + } + } + + #[tokio::test] + // Don't forget to az-login + async fn test_get_account_info_auth() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let container_client = ContainerClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + String::from("acontainer108f32e8"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + let response = container_client + .get_account_info(Some(BlobContainerGetAccountInfoOptions::default())) + .await + .unwrap(); + print!("{:?}", response); + print!( + "\n{:?}", + response.into_body().collect_string().await.unwrap() + ); + } +} diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/clients/blob_service_client.rs b/sdk/storage/azure_storage_blob/blob_storage/src/clients/blob_service_client.rs new file mode 100644 index 0000000000..11bff34d4b --- /dev/null +++ b/sdk/storage/azure_storage_blob/blob_storage/src/clients/blob_service_client.rs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use crate::blob_blob::BlobBlobGetPropertiesOptions; +use crate::blob_client::BlobClientOptions; +use crate::blob_container::{ + BlobContainer, BlobContainerCreateOptions, BlobContainerGetAccountInfoOptions, + BlobContainerGetPropertiesOptions, +}; +use crate::blob_service::{ + BlobServiceGetPropertiesOptions, BlobServiceListContainersSegmentOptions, +}; +use crate::clients::units::*; +use crate::models::{ListContainersSegmentResponse, StorageServiceProperties}; +use crate::policies::storage_headers_policy::StorageHeadersPolicy; +use crate::BlobClient as GeneratedBlobClient; +use azure_core::credentials::TokenCredential; +use azure_core::headers::HeaderName; +use azure_core::{ + AsClientOptions, BearerTokenCredentialPolicy, Context, Method, Policy, Request, Response, + Result, Url, +}; +use azure_identity::DefaultAzureCredentialBuilder; +use std::marker::PhantomData; +use std::sync::Arc; +use uuid::Uuid; + +use super::blob_client::BlobClient; +pub struct BlobServiceClient { + endpoint: String, + credential: Option>, + client: GeneratedBlobClient, +} + +impl BlobServiceClient { + const VERSION_ID: &'static str = ("2024-08-04"); + + pub fn new( + endpoint: String, + credential: Option>, + mut options: Option, + ) -> Result { + let mut options = BlobClientOptions::default(); + + // Fold in StorageHeadersPolicy policy via ClientOptions + let mut per_call_policies = options.client_options.per_call_policies().clone(); + let storage_headers_policy = Arc::new(StorageHeadersPolicy::new()); + per_call_policies.push(storage_headers_policy); + options + .client_options + .set_per_call_policies(per_call_policies); + + // Conditionally add authentication if provided + if credential.is_some() { + let oauth_token_policy = BearerTokenCredentialPolicy::new( + credential.clone().unwrap(), + ["https://storage.azure.com/.default"], + ); + let mut per_try_policies = options.client_options.per_call_policies().clone(); + per_try_policies.push(Arc::new(oauth_token_policy) as Arc); + options + .client_options + .set_per_try_policies(per_try_policies); + } + + let client = + GeneratedBlobClient::with_no_credential(endpoint.clone(), Some(options.clone()))?; + + Ok(Self { + endpoint: endpoint.clone(), + credential, + client: client, + }) + } + + pub async fn get_service_properties( + &self, + options: Option>, + ) -> Result> { + self.client + .get_blob_service_client() + .get_properties( + String::from(Self::VERSION_ID), //svc version + options, + ) + .await + } + + pub fn get_blob_client( + &self, + container_name: String, + blob_name: String, + options: Option, + ) -> BlobClient { + BlobClient { + blob_type: PhantomData::, + endpoint: self.client.endpoint.clone().to_string(), + container_name: container_name, + blob_name: blob_name, + credential: self.credential.clone(), + client: GeneratedBlobClient { + endpoint: self.client.endpoint.clone(), + pipeline: self.client.pipeline.clone(), + }, + } + } + + pub async fn list_containers( + &self, + options: Option>, + ) -> Result> { + self.client + .get_blob_service_client() + .list_containers_segment( + String::from(Self::VERSION_ID), //svc version + options, + ) + .await + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + // Don't forget to az-login + async fn test_get_service_properties() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let service_client = BlobServiceClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + + let response = service_client + .get_service_properties(Some(BlobServiceGetPropertiesOptions::default())) + .await + .unwrap(); + print!("{:?}", response); + print!( + "\n{:?}", + response.into_body().collect_string().await.unwrap() + ); + } + + #[tokio::test] + // Don't forget to az-login + async fn test_get_blob_client() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let service_client = BlobServiceClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + Some(credential.clone()), + Some(BlobClientOptions::default()), + ) + .unwrap(); + + let blob_client = service_client.get_blob_client( + String::from("acontainer108f32e8"), + String::from("hello.txt"), + Some(BlobClientOptions::default()), + ); + + let response = blob_client + .get_blob_properties(Some(BlobBlobGetPropertiesOptions::default())) + .await + .unwrap(); + print!("{:?}", response); + print!( + "\n{:?}", + response.into_body().collect_string().await.unwrap() + ); + } + + #[tokio::test] + // Don't forget to az-login + async fn test_list_containers() { + let credential = DefaultAzureCredentialBuilder::default().build().unwrap(); + let service_client = BlobServiceClient::new( + String::from("https://vincenttranstock.blob.core.windows.net/"), + Some(credential), + Some(BlobClientOptions::default()), + ) + .unwrap(); + + let response = service_client + .list_containers(Some(BlobServiceListContainersSegmentOptions::default())) + .await + .unwrap(); + print!("{:?}", response); + print!( + "\n{:?}", + response.into_body().collect_string().await.unwrap() + ); + } +} diff --git a/sdk/storage/azure_storage_blob/src/clients/mod.rs b/sdk/storage/azure_storage_blob/blob_storage/src/clients/mod.rs similarity index 93% rename from sdk/storage/azure_storage_blob/src/clients/mod.rs rename to sdk/storage/azure_storage_blob/blob_storage/src/clients/mod.rs index cb2f783338..fe276f69b9 100644 --- a/sdk/storage/azure_storage_blob/src/clients/mod.rs +++ b/sdk/storage/azure_storage_blob/blob_storage/src/clients/mod.rs @@ -4,3 +4,4 @@ mod blob_client; mod blob_container_client; mod blob_service_client; +mod units; diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/clients/units.rs b/sdk/storage/azure_storage_blob/blob_storage/src/clients/units.rs new file mode 100644 index 0000000000..e451ceb78d --- /dev/null +++ b/sdk/storage/azure_storage_blob/blob_storage/src/clients/units.rs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +pub trait BlobKind {} +impl BlobKind for Unset {} +impl BlobKind for Block {} +impl BlobKind for Page {} +impl BlobKind for Append {} +pub struct Unset; +pub struct Block; +pub struct Page; +pub struct Append; diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_blob.rs b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_blob.rs index cfa5b45fad..eea3d1d4b8 100644 --- a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_blob.rs +++ b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_blob.rs @@ -1395,7 +1395,7 @@ pub struct BlobBlobDownloadOptions<'a> { if_unmodified_since: Option, lease_id: Option, method_options: ClientMethodOptions<'a>, - range: Option, + pub(crate) range: Option, range_content_crc64: Option, range_content_md5: Option, request_id: Option, diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_block_blob.rs b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_block_blob.rs index ae3210e657..b39b175261 100644 --- a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_block_blob.rs +++ b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_block_blob.rs @@ -34,15 +34,26 @@ impl BlobBlockBlob { let options = options.unwrap_or_default(); let mut ctx = options.method_options.context(); let mut url = self.endpoint.clone(); + //let mut path = String::from("/{containerName}/{blob}?comp=blocklist"); let mut path = String::from("/?comp=blocklist/{containerName}/{blob}"); path = path.replace("{blob}", &blob.into()); path = path.replace("{containerName}", &container_name.into()); url.set_path(&path); + + // Generated Code Issue + // Issue: Some characters, '?' are being %-encoded when they shouldn't be. This is due to the use of set_path() + // [Proposed Change Start] + // Before URL is sent to request, replace "%3F" -> "?" + let mut url = Url::parse(&url.as_str().replace("%3F", "?"))?; + // [Proposed Change End] + if let Some(timeout) = options.timeout { url.query_pairs_mut() .append_pair("timeout", &timeout.to_string()); } + let mut request = Request::new(url, Method::Put); + request.insert_header("accept", "application/json"); if let Some(transactional_content_md5) = options.transactional_content_md5 { request.insert_header("content-md5", transactional_content_md5); @@ -121,7 +132,15 @@ impl BlobBlockBlob { request.insert_header("x-ms-tags", blob_tags_string); } request.insert_header("x-ms-version", version.into()); + + // Generated Code Issue + // Issue: MissingRequiredHeader -- "x-ms-blob-type" + // [Proposed Change Start + // request.insert_header("x-ms-blob-type", "BlockBlob"); + // [Proposed Change End]] + request.set_body(blocks); + println!("\n\n{:?}\n\n", request.url().clone()); self.pipeline.send(&mut ctx, &mut request).await } @@ -300,13 +319,24 @@ impl BlobBlockBlob { let mut path = String::from("/{containerName}/{blob}?comp=block"); path = path.replace("{blob}", &blob.into()); path = path.replace("{containerName}", &container_name.into()); + url.set_path(&path); + url.query_pairs_mut() .append_pair("blockid", &block_id.into()); if let Some(timeout) = options.timeout { url.query_pairs_mut() .append_pair("timeout", &timeout.to_string()); } + + // Generated Code Issue + // Issue: Some characters, '?' are being %-encoded when they shouldn't be. This is due to the use of set_path() + // [Proposed Change Start] + // Before URL is sent to request, replace "%3F" -> "?" + let mut url = Url::parse(&url.as_str().replace("%3F", "?"))?; + // [Proposed Change End] + + println!("What URL gets added to request: {:?}", url); let mut request = Request::new(url, Method::Put); request.insert_header("accept", "application/json"); request.insert_header("content-length", content_length.to_string()); @@ -336,6 +366,13 @@ impl BlobBlockBlob { request.insert_header("x-ms-lease-id", lease_id); } request.insert_header("x-ms-version", version.into()); + + // Generated Code Issue + // Issue: MissingRequiredHeader -- "x-ms-blob-type" + // [Proposed Change Start + request.insert_header("x-ms-blob-type", "BlockBlob"); + // [Proposed Change End] + request.set_body(body); self.pipeline.send(&mut ctx, &mut request).await } @@ -443,6 +480,14 @@ impl BlobBlockBlob { url.query_pairs_mut() .append_pair("timeout", &timeout.to_string()); } + + // Generated Code Issue + // Issue: Some characters, '?' are being %-encoded when they shouldn't be. This is due to the use of set_path() + // [Proposed Change Start] + // Before URL is sent to request, replace "%3F" -> "?" + let url = Url::parse(&url.as_str().replace("%3F", "?"))?; + // [Proposed Change End] + let mut request = Request::new(url, Method::Put); request.insert_header("accept", "application/json"); if let Some(transactional_content_md5) = options.transactional_content_md5 { @@ -526,6 +571,13 @@ impl BlobBlockBlob { } request.insert_header("x-ms-version", version.into()); request.set_body(body); + + // Generated Code Issue + // Issue: MissingRequiredHeader -- "x-ms-blob-type" + // [Proposed Change Start + request.insert_header("x-ms-blob-type", "BlockBlob"); + // [Proposed Change End] + self.pipeline.send(&mut ctx, &mut request).await } } @@ -673,14 +725,14 @@ pub struct BlobBlockBlobUploadOptions<'a> { blob_content_language: Option, blob_content_md5: Option, blob_content_type: Option, - blob_tags_string: Option, + pub(crate) blob_tags_string: Option, // NYI: Generated Code Change to make this visible to the crate encryption_algorithm: Option, encryption_key: Option, encryption_key_sha256: Option, encryption_scope: Option, if_match: Option, if_modified_since: Option, - if_none_match: Option, + pub(crate) if_none_match: Option, // NYI: Generated Code Change to make this visible to the crate if_tags: Option, if_unmodified_since: Option, immutability_policy_expiry: Option, diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_client.rs b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_client.rs index a8cd2d0db9..8d18adc993 100644 --- a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_client.rs +++ b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_client.rs @@ -14,13 +14,15 @@ use azure_core::{ClientOptions, Pipeline, Policy, Result, RetryOptions, Transpor use std::sync::Arc; pub struct BlobClient { - endpoint: Url, - pipeline: Pipeline, + //NYI GENERATED CODE SUPPORT: endpoint & pipeline being pub(crate) + pub(crate) endpoint: Url, + pub(crate) pipeline: Pipeline, } #[derive(Clone, Debug)] pub struct BlobClientOptions { - client_options: ClientOptions, + // NYI GENERATED CODE SUPPORT: client_options being pub(crate) + pub(crate) client_options: ClientOptions, } impl BlobClient { @@ -29,7 +31,8 @@ impl BlobClient { options: Option, ) -> Result { let mut endpoint = Url::parse(endpoint.as_ref())?; - endpoint.query_pairs_mut().clear(); + // NYI GENERATED CODE SUPPORTED: Remove call to query_pairs_mut().clear() + // endpoint.query_pairs_mut().clear(); let options = options.unwrap_or_default(); Ok(Self { endpoint, diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_container.rs b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_container.rs index 72e8b9609d..cb3e20567d 100644 --- a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_container.rs +++ b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_container.rs @@ -149,6 +149,14 @@ impl BlobContainer { url.query_pairs_mut() .append_pair("timeout", &timeout.to_string()); } + + // Generated Code Issue + // Issue: Some characters, '?' are being %-encoded when they shouldn't be. This is due to the use of set_path() + // [Proposed Change Start] + // Before URL is sent to request, replace "%3F" -> "?" + let url = Url::parse(&url.as_str().replace("%3F", "?"))?; + // [Proposed Change End] + let mut request = Request::new(url, Method::Put); request.insert_header("accept", "application/json"); if let Some(access) = options.access { @@ -167,6 +175,12 @@ impl BlobContainer { ); } request.insert_header("x-ms-version", version.into()); + + // Generated Code Issue + // Issue: Hitting "LengthRequired" error + // [Proposed Change Start] + request.insert_header("content-length", "0"); + // [Proposed Change End] self.pipeline.send(&mut ctx, &mut request).await } @@ -295,6 +309,14 @@ impl BlobContainer { let mut path = String::from("/{containerName}?restype=account&comp=properties"); path = path.replace("{containerName}", &container_name.into()); url.set_path(&path); + + // Generated Code Issue + // Issue: Some characters, '?' are being %-encoded when they shouldn't be. This is due to the use of set_path() + // [Proposed Change Start] + // Before URL is sent to request, replace "%3F" -> "?" + let url = Url::parse(&url.as_str().replace("%3F", "?"))?; + // [Proposed Change End] + let mut request = Request::new(url, Method::Get); request.insert_header("accept", "application/json"); if let Some(request_id) = options.request_id { @@ -314,14 +336,27 @@ impl BlobContainer { ) -> Result> { let options = options.unwrap_or_default(); let mut ctx = options.method_options.context(); + println!("\nWhat is endpoint: {}\n", self.endpoint.as_str()); let mut url = self.endpoint.clone(); + println!("\nStep 1: {}\n", url.as_str()); let mut path = String::from("/{containerName}?restype=container"); path = path.replace("{containerName}", &container_name.into()); + url.set_path(&path); + println!("\nStep 2: {}\n", url.as_str()); + if let Some(timeout) = options.timeout { url.query_pairs_mut() .append_pair("timeout", &timeout.to_string()); } + + // Generated Code Issue + // Issue: Some characters, '?' are being %-encoded when they shouldn't be. This is due to the use of set_path() + // [Proposed Change Start] + // Before URL is sent to request, replace "%3F" -> "?" + let url = Url::parse(&url.as_str().replace("%3F", "?"))?; + // [Proposed Change End] + let mut request = Request::new(url, Method::Get); request.insert_header("accept", "application/json"); if let Some(request_id) = options.request_id { @@ -371,6 +406,14 @@ impl BlobContainer { url.query_pairs_mut() .append_pair("timeout", &timeout.to_string()); } + + // Generated Code Issue + // Issue: Some characters, '?' are being %-encoded when they shouldn't be. This is due to the use of set_path() + // [Proposed Change Start] + // Before URL is sent to request, replace "%3F" -> "?" + let url = Url::parse(&url.as_str().replace("%3F", "?"))?; + // [Proposed Change End] + let mut request = Request::new(url, Method::Get); request.insert_header("accept", "application/json"); if let Some(request_id) = options.request_id { diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_service.rs b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_service.rs index 35314284db..07a75440c6 100644 --- a/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_service.rs +++ b/sdk/storage/azure_storage_blob/blob_storage/src/generated/clients/blob_service.rs @@ -96,6 +96,14 @@ impl BlobService { url.query_pairs_mut() .append_pair("timeout", &timeout.to_string()); } + + // Generated Code Issue + // Issue: Some characters, '?' are being %-encoded when they shouldn't be. This is due to the use of set_path() + // [Proposed Change Start] + // Before URL is sent to request, replace "%3F" -> "?" + let url = Url::parse(&url.as_str().replace("%3F", "?"))?; + // [Proposed Change End] + let mut request = Request::new(url, Method::Get); request.insert_header("accept", "application/json"); if let Some(request_id) = options.request_id { @@ -181,6 +189,14 @@ impl BlobService { url.query_pairs_mut() .append_pair("timeout", &timeout.to_string()); } + + // Generated Code Issue + // Issue: Some characters, '?' are being %-encoded when they shouldn't be. This is due to the use of set_path() + // [Proposed Change Start] + // Before URL is sent to request, replace "%3F" -> "?" + let url = Url::parse(&url.as_str().replace("%3F", "?"))?; + // [Proposed Change End] + let mut request = Request::new(url, Method::Get); request.insert_header("accept", "application/json"); if let Some(request_id) = options.request_id { diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/generated/models.rs b/sdk/storage/azure_storage_blob/blob_storage/src/generated/models.rs index e57b9481a2..4f7dee537a 100644 --- a/sdk/storage/azure_storage_blob/blob_storage/src/generated/models.rs +++ b/sdk/storage/azure_storage_blob/blob_storage/src/generated/models.rs @@ -12,7 +12,7 @@ use async_std::task::block_on; use azure_core::{RequestContent, Response}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use typespec_client_core::Model; +use typespec_derive::Model; /// Represents an access policy. #[derive(Clone, Debug, Default, Deserialize, Model, Serialize)] diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/lib.rs b/sdk/storage/azure_storage_blob/blob_storage/src/lib.rs index ec2d89e66b..73229932a8 100644 --- a/sdk/storage/azure_storage_blob/blob_storage/src/lib.rs +++ b/sdk/storage/azure_storage_blob/blob_storage/src/lib.rs @@ -3,7 +3,9 @@ // 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. +pub mod clients; mod generated; +pub mod policies; pub use crate::generated::clients::*; diff --git a/sdk/storage/azure_storage_blob/src/clients/blob_client.rs b/sdk/storage/azure_storage_blob/blob_storage/src/policies/mod.rs similarity index 70% rename from sdk/storage/azure_storage_blob/src/clients/blob_client.rs rename to sdk/storage/azure_storage_blob/blob_storage/src/policies/mod.rs index d194bed477..ad1d1d9586 100644 --- a/sdk/storage/azure_storage_blob/src/clients/blob_client.rs +++ b/sdk/storage/azure_storage_blob/blob_storage/src/policies/mod.rs @@ -1,2 +1,4 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + +pub(crate) mod storage_headers_policy; diff --git a/sdk/storage/azure_storage_blob/blob_storage/src/policies/storage_headers_policy.rs b/sdk/storage/azure_storage_blob/blob_storage/src/policies/storage_headers_policy.rs new file mode 100644 index 0000000000..19e6f863e8 --- /dev/null +++ b/sdk/storage/azure_storage_blob/blob_storage/src/policies/storage_headers_policy.rs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use async_trait::async_trait; +use azure_core::{Context, Policy, PolicyResult, Request}; +use std::sync::Arc; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub struct StorageHeadersPolicy {} + +impl StorageHeadersPolicy { + pub(crate) fn new() -> Self { + Self {} + } +} + +#[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], + ) -> PolicyResult { + // TODO: Check if the header is already set (that means cx set), so don't set if so + let request_id = Uuid::new_v4().to_string(); + request.insert_header("x-ms-client-request-id", request_id); + + next[0].send(ctx, request, &next[1..]).await + } +} diff --git a/sdk/storage/azure_storage_blob/src/clients/blob_container_client.rs b/sdk/storage/azure_storage_blob/src/clients/blob_container_client.rs deleted file mode 100644 index d194bed477..0000000000 --- a/sdk/storage/azure_storage_blob/src/clients/blob_container_client.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. diff --git a/sdk/storage/azure_storage_blob/src/clients/blob_service_client.rs b/sdk/storage/azure_storage_blob/src/clients/blob_service_client.rs deleted file mode 100644 index d194bed477..0000000000 --- a/sdk/storage/azure_storage_blob/src/clients/blob_service_client.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. diff --git a/sdk/storage/azure_storage_blob/src/lib.rs b/sdk/storage/azure_storage_blob/src/lib.rs index 3ae259b188..d194bed477 100644 --- a/sdk/storage/azure_storage_blob/src/lib.rs +++ b/sdk/storage/azure_storage_blob/src/lib.rs @@ -1,4 +1,2 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -mod clients;