azure_storage_blobs/clients/
container_client.rs

1use 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    /// Create a new `ContainerClient` from a `BlobServiceClient` and a container name
27    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    /// Create a container
50    pub fn create(&self) -> CreateBuilder {
51        CreateBuilder::new(self.clone())
52    }
53
54    /// Delete a container
55    pub fn delete(&self) -> DeleteBuilder {
56        DeleteBuilder::new(self.clone())
57    }
58
59    /// Get a container acl
60    pub fn get_acl(&self) -> GetACLBuilder {
61        GetACLBuilder::new(self.clone())
62    }
63
64    /// Set a container acl
65    pub fn set_acl(&self, public_access: PublicAccess) -> SetACLBuilder {
66        SetACLBuilder::new(self.clone(), public_access)
67    }
68
69    /// Get a container's properties
70    pub fn get_properties(&self) -> GetPropertiesBuilder {
71        GetPropertiesBuilder::new(self.clone())
72    }
73
74    /// List the blobs in a container
75    pub fn list_blobs(&self) -> ListBlobsBuilder {
76        ListBlobsBuilder::new(self.clone())
77    }
78
79    /// Acquite a lease on a container
80    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    /// Break the lease on a container
88    pub fn break_lease(&self) -> BreakLeaseBuilder {
89        BreakLeaseBuilder::new(self.clone())
90    }
91
92    /// Check whether the container exists.
93    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    /// Create a shared access signature.
152    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    /// Full URL for the container.
185    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}