azure_storage_blob/clients/
blob_container_client.rs1pub use crate::generated::clients::{BlobContainerClient, BlobContainerClientOptions};
5
6use crate::{models::StorageErrorCode, BlobClient};
7use azure_core::{
8 credentials::TokenCredential,
9 error::ErrorKind,
10 http::{
11 policies::{auth::BearerTokenAuthorizationPolicy, Policy},
12 Pipeline, StatusCode, Url,
13 },
14 tracing, Result,
15};
16use std::sync::Arc;
17
18impl BlobContainerClient {
19 #[tracing::new("Storage.Blob.Container")]
28 pub fn new(
29 container_url: Url,
30 credential: Option<Arc<dyn TokenCredential>>,
31 options: Option<BlobContainerClientOptions>,
32 ) -> Result<Self> {
33 if container_url.cannot_be_a_base() {
35 return Err(azure_core::Error::with_message(
36 azure_core::error::ErrorKind::Other,
37 format!("{container_url} is not a valid base URL"),
38 ));
39 }
40
41 let mut options = options.unwrap_or_default();
42 super::apply_client_defaults(&mut options.client_options);
43
44 let mut per_retry_policies: Vec<Arc<dyn Policy>> = Vec::default();
45 if let Some(token_credential) = credential {
46 if !container_url.scheme().starts_with("https") {
47 return Err(azure_core::Error::with_message(
48 azure_core::error::ErrorKind::Other,
49 format!("{container_url} must use https"),
50 ));
51 }
52 per_retry_policies.push(Arc::new(BearerTokenAuthorizationPolicy::new(
53 token_credential,
54 vec!["https://storage.azure.com/.default"],
55 )));
56 }
57
58 let pipeline = Pipeline::new(
59 option_env!("CARGO_PKG_NAME"),
60 option_env!("CARGO_PKG_VERSION"),
61 options.client_options.clone(),
62 Vec::default(),
63 per_retry_policies,
64 None,
65 );
66
67 Ok(Self {
68 endpoint: container_url,
69 version: options.version,
70 pipeline,
71 })
72 }
73
74 pub fn blob_client(&self, blob_name: &str) -> BlobClient {
80 let mut blob_url = self.url().clone();
81 blob_url
82 .path_segments_mut()
83 .expect("Invalid endpoint URL: Cannot append blob_name to the blob endpoint.")
85 .extend([blob_name]);
86
87 BlobClient {
88 endpoint: blob_url,
89 pipeline: self.pipeline.clone(),
90 version: self.version.clone(),
91 tracer: self.tracer.clone(),
92 }
93 }
94
95 pub fn url(&self) -> &Url {
97 &self.endpoint
98 }
99
100 pub async fn exists(&self) -> Result<bool> {
104 match self.get_properties(None).await {
105 Ok(_) => Ok(true),
106 Err(e) if e.http_status() == Some(StatusCode::NotFound) => match e.kind() {
107 ErrorKind::HttpResponse {
108 error_code: Some(error_code),
109 ..
110 } if error_code == StorageErrorCode::ContainerNotFound.as_ref() => Ok(false),
111 _ => Err(e),
113 },
114 Err(e) => Err(e),
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use azure_core::{
123 http::{
124 headers::Headers, pager::PagerContinuation, AsyncRawResponse, ClientOptions,
125 StatusCode, Transport,
126 },
127 Bytes,
128 };
129 use azure_core_test::http::MockHttpClient;
130 use futures::{FutureExt as _, TryStreamExt as _};
131 use std::sync::Arc;
132
133 const LIST_BLOBS_PAGE: &[u8] = br#"<?xml version="1.0" encoding="utf-8"?>
134<EnumerationResults ServiceEndpoint="https://example.blob.core.windows.net/" ContainerName="container">
135 <Blobs>
136 <Blob>
137 <Name>blob1</Name>
138 <Properties>
139 <BlobType>BlockBlob</BlobType>
140 </Properties>
141 </Blob>
142 </Blobs>
143 <NextMarker>page-2</NextMarker>
144</EnumerationResults>"#;
145
146 #[test]
147 fn from_url_rejects_cannot_be_a_base_url() {
148 let url = Url::parse("data:text/plain,hello").unwrap();
149 assert!(BlobContainerClient::new(url, None, None).is_err());
150 }
151
152 #[test]
153 fn from_url_accepts_http_without_credential() {
154 let url = Url::parse("http://127.0.0.1:10000/devstoreaccount1/container").unwrap();
155 let container = BlobContainerClient::new(url, None, None).unwrap();
156 assert_eq!(
157 container.blob_client("blob").url().path(),
158 "/devstoreaccount1/container/blob"
159 );
160 }
161
162 #[test]
163 fn from_url_accepts_https_custom_hostname() {
164 let url = Url::parse("https://cdn.contoso.com/container").unwrap();
166 assert!(BlobContainerClient::new(url, None, None).is_ok());
167 }
168
169 #[tokio::test]
170 async fn list_blobs_page_keeps_body_for_into_model() -> Result<()> {
171 let mock_client = Arc::new(MockHttpClient::new(|req| {
172 assert_eq!(req.url().path(), "/container");
173 assert!(req
174 .url()
175 .query()
176 .is_some_and(|query| query.contains("comp=list")));
177 async move {
178 Ok(AsyncRawResponse::from_bytes(
179 StatusCode::Ok,
180 Headers::new(),
181 Bytes::from_static(LIST_BLOBS_PAGE),
182 ))
183 }
184 .boxed()
185 }));
186 let client = BlobContainerClient::new(
187 Url::parse("https://example.blob.core.windows.net/container").unwrap(),
188 None,
189 Some(BlobContainerClientOptions {
190 client_options: ClientOptions {
191 transport: Some(Transport::new(mock_client)),
192 ..Default::default()
193 },
194 ..Default::default()
195 }),
196 )?;
197
198 let mut pages = client.list_blobs(None)?.into_pages();
199 let page = pages.try_next().await?.expect("expected a page");
200
201 assert!(matches!(
202 pages.continuation(),
203 Some(PagerContinuation::Token(token)) if token == "page-2"
204 ));
205
206 let page = page.into_model()?;
207 assert_eq!(page.next_marker.as_deref(), Some("page-2"));
208 assert_eq!(page.blob_items.len(), 1);
209 assert_eq!(page.blob_items[0].name.as_deref(), Some("blob1"));
210
211 Ok(())
212 }
213}