azure_storage_blobs/clients/
container_client.rs1use crate::{clients::*, container::operations::*, prelude::PublicAccess};
2use azure_core::{
3 error::{Error, ErrorKind},
4 headers::Headers,
5 prelude::*,
6 Body, Method, Request, Response, StatusCode, Url,
7};
8use azure_storage::{
9 prelude::BlobSasPermissions,
10 shared_access_signature::{
11 service_sas::{BlobSharedAccessSignature, BlobSignedResource, UserDeligationKey},
12 SasToken,
13 },
14 CloudLocation, StorageCredentials, StorageCredentialsInner,
15};
16use std::ops::Deref;
17use time::OffsetDateTime;
18
19#[derive(Debug, Clone)]
20pub struct ContainerClient {
21 service_client: BlobServiceClient,
22 container_name: String,
23}
24
25impl ContainerClient {
26 pub(crate) fn new(service_client: BlobServiceClient, container_name: String) -> Self {
28 Self {
29 service_client,
30 container_name,
31 }
32 }
33
34 pub fn from_sas_url(url: &Url) -> azure_core::Result<Self> {
35 let cloud_location: CloudLocation = url.try_into()?;
36 let credentials: StorageCredentials = url.try_into()?;
37
38 let container = url.path().split_terminator('/').nth(1).ok_or_else(|| {
39 azure_core::Error::with_message(azure_core::error::ErrorKind::DataConversion, || {
40 "unable to find storage container from url"
41 })
42 })?;
43
44 let client =
45 ClientBuilder::with_location(cloud_location, credentials).container_client(container);
46 Ok(client)
47 }
48
49 pub fn create(&self) -> CreateBuilder {
51 CreateBuilder::new(self.clone())
52 }
53
54 pub fn delete(&self) -> DeleteBuilder {
56 DeleteBuilder::new(self.clone())
57 }
58
59 pub fn get_acl(&self) -> GetACLBuilder {
61 GetACLBuilder::new(self.clone())
62 }
63
64 pub fn set_acl(&self, public_access: PublicAccess) -> SetACLBuilder {
66 SetACLBuilder::new(self.clone(), public_access)
67 }
68
69 pub fn get_properties(&self) -> GetPropertiesBuilder {
71 GetPropertiesBuilder::new(self.clone())
72 }
73
74 pub fn list_blobs(&self) -> ListBlobsBuilder {
76 ListBlobsBuilder::new(self.clone())
77 }
78
79 pub fn acquire_lease<LD: Into<LeaseDuration>>(
81 &self,
82 lease_duration: LD,
83 ) -> AcquireLeaseBuilder {
84 AcquireLeaseBuilder::new(self.clone(), lease_duration.into())
85 }
86
87 pub fn break_lease(&self) -> BreakLeaseBuilder {
89 BreakLeaseBuilder::new(self.clone())
90 }
91
92 pub async fn exists(&self) -> azure_core::Result<bool> {
94 match self.get_properties().await {
95 Ok(_) => Ok(true),
96 Err(err)
97 if err
98 .as_http_error()
99 .map(|e| e.status() == StatusCode::NotFound)
100 .unwrap_or_default() =>
101 {
102 Ok(false)
103 }
104 Err(err) => Err(err),
105 }
106 }
107
108 pub fn container_lease_client(&self, lease_id: LeaseId) -> ContainerLeaseClient {
109 ContainerLeaseClient::new(self.clone(), lease_id)
110 }
111
112 pub fn blob_client<BN: Into<String>>(&self, blob_name: BN) -> BlobClient {
113 BlobClient::new(self.clone(), blob_name.into())
114 }
115
116 pub fn service_client(&self) -> BlobServiceClient {
117 self.service_client.clone()
118 }
119
120 pub fn container_name(&self) -> &str {
121 &self.container_name
122 }
123
124 pub async fn user_delegation_shared_access_signature(
125 &self,
126 permissions: BlobSasPermissions,
127 user_delegation_key: &UserDeligationKey,
128 ) -> azure_core::Result<BlobSharedAccessSignature> {
129 let creds = self.credentials().0.read().await;
130 if !matches!(creds.deref(), StorageCredentialsInner::TokenCredential(_)) {
131 return Err(Error::message(
132 ErrorKind::Credential,
133 "User delegation access signature generation requires Token authentication",
134 ));
135 };
136
137 let service_client = self.service_client();
138
139 let account = service_client.account();
140
141 let canonicalized_resource = format!("/blob/{}/{}", account, self.container_name());
142 Ok(BlobSharedAccessSignature::new(
143 user_delegation_key.clone(),
144 canonicalized_resource,
145 permissions,
146 user_delegation_key.signed_expiry,
147 BlobSignedResource::Container,
148 ))
149 }
150
151 pub async fn shared_access_signature(
153 &self,
154 permissions: BlobSasPermissions,
155 expiry: OffsetDateTime,
156 ) -> azure_core::Result<BlobSharedAccessSignature> {
157 let creds = self.service_client.credentials().0.read().await;
158 let StorageCredentialsInner::Key(account, key) = creds.deref() else {
159 return Err(Error::message(
160 ErrorKind::Credential,
161 "Shared access signature generation - SAS can be generated with access_key clients",
162 ));
163 };
164
165 let canonicalized_resource = format!("/blob/{}/{}", account, self.container_name());
166 Ok(BlobSharedAccessSignature::new(
167 key.clone(),
168 canonicalized_resource,
169 permissions,
170 expiry,
171 BlobSignedResource::Container,
172 ))
173 }
174
175 pub fn generate_signed_container_url<T>(&self, signature: &T) -> azure_core::Result<Url>
176 where
177 T: SasToken,
178 {
179 let mut url = self.url()?;
180 url.set_query(Some(&signature.token()?));
181 Ok(url)
182 }
183
184 pub fn url(&self) -> azure_core::Result<Url> {
186 let mut url = self.service_client.url()?;
187 url.path_segments_mut()
188 .map_err(|()| Error::message(ErrorKind::DataConversion, "Invalid url"))?
189 .push(self.container_name());
190 Ok(url)
191 }
192
193 pub(crate) fn credentials(&self) -> &StorageCredentials {
194 self.service_client.credentials()
195 }
196
197 pub(crate) async fn send(
198 &self,
199 context: &mut Context,
200 request: &mut Request,
201 ) -> azure_core::Result<Response> {
202 self.service_client.send(context, request).await
203 }
204
205 pub(crate) fn finalize_request(
206 url: Url,
207 method: Method,
208 headers: Headers,
209 request_body: Option<Body>,
210 ) -> azure_core::Result<Request> {
211 BlobServiceClient::finalize_request(url, method, headers, request_body)
212 }
213}
214
215#[cfg(test)]
216#[cfg(feature = "test_integration")]
217mod integration_tests {
218 use super::*;
219 use futures::StreamExt;
220
221 fn get_emulator_client(container_name: &str) -> ContainerClient {
222 ClientBuilder::emulator().container_client(container_name)
223 }
224
225 #[tokio::test]
226 async fn test_create_delete() {
227 let container_name = uuid::Uuid::new_v4().to_string();
228 let container_client = get_emulator_client(&container_name);
229
230 container_client
231 .create()
232 .await
233 .expect("create container should succeed");
234 container_client
235 .delete()
236 .await
237 .expect("delete container should succeed");
238 }
239
240 #[tokio::test]
241 async fn test_list_blobs() {
242 let container_name = uuid::Uuid::new_v4().to_string();
243 let container_client = get_emulator_client(&container_name);
244
245 container_client
246 .create()
247 .await
248 .expect("create container should succeed");
249
250 let md5 = md5::compute("world");
251 container_client
252 .blob_client("hello.txt")
253 .put_block_blob("world")
254 .await
255 .expect("put block blob should succeed");
256 let list = container_client
257 .list_blobs()
258 .into_stream()
259 .next()
260 .await
261 .expect("list blobs next() should return value")
262 .expect("list blobs should succeed");
263 let blobs: Vec<_> = list.blobs.blobs().collect();
264 assert_eq!(blobs.len(), 1);
265 assert_eq!(blobs[0].name, "hello.txt");
266 assert_eq!(
267 blobs[0]
268 .properties
269 .content_md5
270 .as_ref()
271 .expect("has content_md5")
272 .as_slice(),
273 &md5.0
274 );
275
276 container_client
277 .delete()
278 .await
279 .expect("delete container should succeed");
280 }
281}