Skip to main content

headless_lms_chatbot/
azure_blob_storage.rs

1use std::collections::HashMap;
2
3use secrecy::ExposeSecret;
4
5use crate::prelude::*;
6use anyhow::Context;
7use azure_core::prelude::Metadata;
8use azure_storage::StorageCredentials;
9use azure_storage_blobs::prelude::*;
10use bytes::Bytes;
11use futures::StreamExt;
12use headless_lms_base::config::{ApplicationConfiguration, AzureBlobStorageConfiguration};
13
14/// A client for interacting with Azure Blob Storage.
15pub struct AzureBlobClient {
16    container_client: ContainerClient,
17    pub container_name: String,
18}
19
20impl AzureBlobClient {
21    pub async fn new(
22        app_config: &ApplicationConfiguration,
23        container_name: &str,
24    ) -> anyhow::Result<Self> {
25        let azure_configuration = app_config
26            .azure_configuration
27            .as_ref()
28            .context("Azure configuration is missing")?;
29        let AzureBlobStorageConfiguration {
30            storage_account,
31            access_key,
32        } = azure_configuration
33            .blob_storage_config
34            .clone()
35            .context("Azure Blob Storage configuration is missing")?;
36
37        let container_name = container_name.to_string();
38
39        // Azure SDK takes ownership of the key string; expose only here at the SDK boundary.
40        let storage_credentials = StorageCredentials::access_key(
41            &storage_account,
42            access_key.expose_secret().to_string(),
43        );
44        let blob_service_client = BlobServiceClient::new(storage_account, storage_credentials);
45        let container_client = blob_service_client.container_client(container_name.clone());
46
47        Ok(AzureBlobClient {
48            container_client,
49            container_name,
50        })
51    }
52
53    /// Ensures the container used to store the blobs exists. If it does not, the container is created.
54    pub async fn ensure_container_exists(&self) -> anyhow::Result<()> {
55        if self.container_client.exists().await? {
56            return Ok(());
57        }
58
59        info!(
60            "Azure blob storage container '{}' does not exist. Creating...",
61            self.container_client.container_name()
62        );
63        self.container_client
64            .create()
65            .public_access(PublicAccess::None)
66            .await?;
67        Ok(())
68    }
69
70    /// Uploads a file to the specified container.
71    pub async fn upload_file(
72        &self,
73        blob_path: &str,
74        file_bytes: &[u8],
75        metadata: Option<HashMap<String, Bytes>>,
76    ) -> anyhow::Result<()> {
77        let blob_client = self.container_client.blob_client(blob_path);
78
79        let mut put_blob = blob_client.put_block_blob(file_bytes.to_vec());
80
81        if let Some(meta) = metadata {
82            let mut m = Metadata::new();
83            for (key, value) in meta {
84                m.insert(key, value);
85            }
86            put_blob = put_blob.metadata(m);
87        }
88
89        put_blob.await?;
90
91        info!("Blob '{}' uploaded successfully.", blob_path);
92        Ok(())
93    }
94
95    /// Deletes a file (blob) from the specified container.
96    pub async fn delete_file(&self, path: &str) -> anyhow::Result<()> {
97        let blob_client = self.container_client.blob_client(path);
98
99        blob_client.delete().await?;
100
101        info!("Blob '{}' deleted successfully.", path);
102        Ok(())
103    }
104
105    pub async fn list_files_with_prefix(&self, prefix: &str) -> anyhow::Result<Vec<String>> {
106        let mut result = Vec::new();
107        let prefix_owned = prefix.to_string();
108        let response = self.container_client.list_blobs().prefix(prefix_owned);
109        let mut stream = response.into_stream();
110
111        while let Some(list) = stream.next().await {
112            let list = list?;
113            let blobs: Vec<_> = list.blobs.blobs().collect();
114            for blob in blobs {
115                result.push(blob.name.clone());
116            }
117        }
118        Ok(result)
119    }
120}